Compare commits

..

240 Commits

Author SHA1 Message Date
dijunkun
eee6c588bd [fix] fix X11 odd-size captures by aligning dimensions to even values 2026-03-24 00:07:44 +08:00
dijunkun
eca68f6c7a [fix] fix keyboard input injection on Linux Wayland sessions 2026-03-23 23:14:05 +08:00
dijunkun
f4e28d8774 [fix] fix mac-to-windows symbol key input 2026-03-23 22:30:58 +08:00
dijunkun
21b179e01c [fix] release pressed modifier keys when stream window loses focus 2026-03-23 21:33:01 +08:00
dijunkun
83cacf6f51 [fix] split mouse and keyboard from data channel and use reliable transimission 2026-03-23 21:14:07 +08:00
dijunkun
13c37f01b1 [fix] fix cross-platform key capture and mapping issues 2026-03-23 20:42:48 +08:00
dijunkun
511831ced3 [fix] fix Wayland reconnect black screen by keeping capturer warm and also fix Wayland mouse control 2026-03-23 05:18:56 +08:00
dijunkun
518e1afa58 [feat] add Linux screen capture fallback support for DRM and Wayland 2026-03-22 21:33:50 +08:00
dijunkun
43d03ac081 [fix] fix font path lookup for wqy/dejavu by switching from OpenType to TrueType 2026-03-22 00:39:19 +08:00
dijunkun
f7f62c5fe0 [fix] update MiniRTC: refactor IceAgent to improve stability 2026-03-20 15:58:59 +08:00
dijunkun
2bbddbca6b [fix] fix Linux audio fallback when audio devices are unavaliable 2026-03-20 15:14:45 +08:00
dijunkun
f0f8f27f4c [fix] fix blocking join() in Linux clipboard monitor thread during shutdown 2026-03-20 15:02:34 +08:00
dijunkun
262af263f2 [fix] move keyboard capturer to a background thread and use poll-based X11 event handling to avoid main-thread blocking 2026-03-20 14:56:40 +08:00
dijunkun
38b7775b1b [fix] fix restart/shutdown races in process monitor 2026-03-20 14:50:42 +08:00
dijunkun
56c0bca62f [chore] adjust hyperlink spacing and alignment 2026-03-20 14:47:33 +08:00
dijunkun
4b1b09fd5b [fix] fix Linux fonts: use opentype instead of truetype 2026-03-20 13:01:13 +08:00
dijunkun
1d6425bbf4 [fix] update MiniRTC: fix compiler warnings by adding missing override specifiers 2026-03-20 04:36:58 +08:00
dijunkun
5ec6552d25 [fix] fix macOS intel CI build failure caused by python 3.13 PGO mismatch 2026-03-20 03:50:19 +08:00
dijunkun
79e4a0790a [fix] fix issue where wgc_plugin was not compiled 2026-03-20 02:59:31 +08:00
dijunkun
1d3cac54ab [feat] load wgc from wgc_plugin.dll at runtime and drop direct'windowsapp' linking, refs #74 2026-03-20 01:36:36 +08:00
dijunkun
2f26334775 [feat] unify UI font loading across platforms and prefer PingFang on macOS 2026-03-19 21:58:14 +08:00
dijunkun
9270d528e3 [feat] update miniRTC: fix compiler warnings caused by missing override specifiers 2026-03-19 21:57:52 +08:00
dijunkun
91db3a7e34 [feat] add Russian language support 2026-03-19 20:04:30 +08:00
dijunkun
d017561e54 [fix] fix typo ImGuiChildFlags_Border to ImGuiChildFlags_Borders 2026-03-19 16:16:51 +08:00
dijunkun
8e8a85bae3 [feat] upgrade actions/checkout and actions/cache to v5 for Node 24 compatibility 2026-03-19 15:03:58 +08:00
dijunkun
bea89e9111 [feat] crossdesk server image supports Linux ARM64, refs #72 2026-03-19 10:06:57 +08:00
dijunkun
499ce0190a [fix] process mouse events only from the stream window 2026-03-11 16:00:29 +08:00
dijunkun
91bde91238 [feat] probe presence before connect and show warning if offline 2026-03-10 17:46:44 +08:00
dijunkun
3e31ba102d [fix] prevent sending connection requests to offline devices 2026-03-10 10:53:58 +08:00
dijunkun
263c5eefd3 [fix] fix update button lag in release mode by using non-blocking URL opener. 2026-03-10 10:39:05 +08:00
dijunkun
b230b851e4 [fix] fix cannot close connection from Server Window when the peer is a web client 2026-03-10 00:39:00 +08:00
dijunkun
ff32477ffe [fix] update MiniRTC: fix crash on disconnect 2026-03-10 00:35:33 +08:00
dijunkun
c6c60decdb [fix] fix incorrect online status of recently connections 2026-03-09 22:52:05 +08:00
dijunkun
7505adeca8 [feat] update MiniRTC 2026-03-09 22:50:42 +08:00
dijunkun
754f1fba88 [feat] show 'Receiving screen' text before the remote frame arrives 2026-03-09 22:37:50 +08:00
dijunkun
8be46b870a [feat] add cancel button during connecting 2026-03-09 21:35:21 +08:00
dijunkun
81cb1d6c0b [fix] disable clipboard sharing when not in control mode 2026-03-05 17:46:27 +08:00
dijunkun
319416f1b7 [feat] update MiniRTC: optimize video quality and smoothness 2026-03-05 17:30:05 +08:00
dijunkun
d679c6251b [feat] update MiniRTC 2026-03-04 10:46:21 +08:00
dijunkun
a14baafda7 [fix] fix keyboard event loss due to start_keyboard_capturer_ flag improper setting, fixes #65 2026-03-04 10:36:39 +08:00
dijunkun
cfdc7d3106 [fix] update MiniRTC: fix bandwidth degradation caused by ALR-triggered resolution downgrade during static frames 2026-03-03 10:58:38 +08:00
dijunkun
33d51b8ce5 [fix] reset to initial monitor on connection close via ResetToInitialMonitor to fix black screen 2026-03-02 15:42:44 +08:00
dijunkun
b13dac2093 [feat] refine display of recent connections presence tooltip 2026-03-02 10:48:16 +08:00
dijunkun
a605c95e5a [fix] fix window rounding inconsistency under different DPI scales 2026-03-02 10:38:06 +08:00
dijunkun
9a5553a636 [chore] update fonts 2026-03-02 10:17:06 +08:00
dijunkun
ef02403da6 [fix] fix incorrect sizing of the online status indicator on high-DPI displays 2026-03-01 16:47:09 +08:00
dijunkun
adfab363c1 [feat] add online presence check before connecting and show offline warning dialog 2026-03-01 16:29:11 +08:00
dijunkun
123d4cf595 [fix] update MiniRTC: fix the macOS hardware decode fail when the server using openH264 encode 2026-03-01 15:40:50 +08:00
dijunkun
19feb8ff49 [feat] show device online/offline status in recent connection tooltip 2026-02-28 17:25:41 +08:00
dijunkun
9223bf9d2d [feat] add online status indicators for recent connections 2026-02-28 17:06:44 +08:00
dijunkun
11b5f87841 [feat] update MiniRTC: add signaling send/receive API support 2026-02-28 17:04:47 +08:00
dijunkun
cea59fb453 [feat] update MiniRTC 2026-02-27 17:53:51 +08:00
ZongYangBigPolo
3a179bf480 [feat] add macOS installer icon and optimize packaging script (#70) 2026-02-27 17:37:54 +08:00
dijunkun
b9c53024f1 [feat] set video quality to HIGH and enable hardware codec by default 2026-02-27 17:24:04 +08:00
dijunkun
62b37ad698 [fix] resolve failures in the WGC→DXGI→GDI fallback chain 2026-02-27 16:33:57 +08:00
dijunkun
de56cd5d3b [feat] update MiniRTC 2026-02-27 16:30:08 +08:00
dijunkun
8d9d78185a [fix] fix issue where client display list was incorrectly merged into the server display list 2026-02-27 16:27:37 +08:00
dijunkun
b10a6512fe [feat] add Windows DXGI/GDI screen capture with WGC→DXGI→GDI fallback support 2026-02-27 13:55:41 +08:00
dijunkun
a94da8802f [feat] make MainWindow and ServerWindow use rounded corners 2026-02-26 18:06:07 +08:00
dijunkun
4e6f82d00c [feat] restrict the dragging range of the ControlWindow 2026-02-26 15:55:04 +08:00
dijunkun
5e2ad99ec0 [feat] update MiniRTC to resolve color distortion in the OpenH264 decoder 2026-02-25 17:48:30 +08:00
dijunkun
8ab50ea362 [feat] add video resolution and conection mode in NetTrafficStats window 2026-02-25 15:33:17 +08:00
dijunkun
25e9958a69 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-02-24 17:56:21 +08:00
dijunkun
410ea8b96b [feat] update MiniRTC 2026-02-24 17:56:02 +08:00
dijunkun
e656664cad [chore] adjust file transfer save path button pos 2026-02-24 14:36:03 +08:00
dijunkun
0e6cee0961 [fix] fix stream window rendering height 2026-02-24 14:31:34 +08:00
dijunkun
42506b8c1d [ci] combine Linux amd64 and arm64 builds into a single job using matrix 2026-02-13 02:31:58 +08:00
dijunkun
e35365d162 [feat] disable and style minimize_to_tray checkbox for non-Windows platforms 2026-02-13 02:29:52 +08:00
dijunkun
bf1c0f796d [fix] fix Linux system certificate loading failure 2026-02-13 01:56:49 +08:00
dijunkun
547532b28c [fix] fix server window scaling issues on high-DPI displays 2026-02-13 01:26:10 +08:00
dijunkun
a91e23abf6 [fix] fix raw pointer issues when closing connections 2026-02-13 01:12:21 +08:00
dijunkun
2b324f636b [fix] fix macOS system certificate loading failure 2026-02-12 22:49:54 +08:00
dijunkun
103b8372e4 [chore] rename packaged executable to CrossDesk.exe in NSIS and portable artifacts 2026-02-12 16:45:41 +08:00
dijunkun
f7f1724bf1 [feat] optimize hyperlink opening by replacing system start with CreateProcessW-based URL launch on Winodws 2026-02-12 16:22:57 +08:00
dijunkun
5d70e11f17 [feat] support Windows x64 portable build, refs #54 2026-02-12 16:03:06 +08:00
dijunkun
fb7ae90d46 [feat] add configurable file transfer save path in settings window, refs #63 2026-02-12 14:30:14 +08:00
dijunkun
957792a7a0 [feat] remove client certificate dependency 2026-02-11 16:23:43 +08:00
dijunkun
2e8ce6a2f0 [fix] reset default cert fingerprint if mismatch 2026-02-05 18:59:28 +08:00
dijunkun
9927a56b78 [feat] update MiniRTC 2026-02-05 18:05:35 +08:00
dijunkun
db3da52f83 [feat] clear cached fingerprint when verification fails 2026-02-05 17:15:59 +08:00
dijunkun
19a7c6978a [feat] update MiniRTC to resolve websocket reconnection and post task issues 2026-01-28 09:45:19 +08:00
dijunkun
b5e9ba03a1 [fix] double-buffer video frames and handle stream cleanup on main thread 2026-01-28 09:44:54 +08:00
dijunkun
cb5f8b91ad [feat] update update-notification icon 2026-01-27 21:11:26 +08:00
dijunkun
f627f60f1a [feat] use tooltips to display server-side file transfer status information 2026-01-27 17:50:21 +08:00
dijunkun
e9fce5b8b8 [feat] display remote controller hostname instead of remote id 2026-01-26 22:52:58 +08:00
dijunkun
a7820a79db [fix] fix incorrect peer_ usage in SendReliableDataFrame 2026-01-26 21:47:10 +08:00
dijunkun
b6a52dbcd4 [feat] add support for displaying multiple controller info and file transfer to controllers 2026-01-26 17:47:31 +08:00
dijunkun
7bbd10a50c [fix] fix rendering issues in stream and server windows when the main window is minimized 2026-01-22 17:56:00 +08:00
dijunkun
ee08b231db [fix] fix height when server window is restored from collapsed state 2026-01-20 23:58:43 +08:00
dijunkun
619e54dc0e [feat] add controller info and file transfer in server window 2026-01-20 21:22:20 +08:00
dijunkun
9b69596af1 [fix] fix stream window size recalculation 2026-01-20 01:33:27 +08:00
dijunkun
f6e169c013 [feat] add support for server window resizing and dragging 2026-01-20 01:22:14 +08:00
dijunkun
fd242d50c1 [feat] show server window in the bottom-right corner of the screen 2026-01-19 17:42:22 +08:00
dijunkun
d6d8ecd6c5 [feat] add server window 2026-01-19 00:47:34 +08:00
dijunkun
669fac7f50 [feat] support drag-and-drop file sending, refs #63 2026-01-14 18:13:22 +08:00
dijunkun
92d670916e [fix] fix incorrect data send function used for control data 2026-01-13 18:13:54 +08:00
dijunkun
0155413c12 [feat] use reliable transmission to send control info 2026-01-12 17:28:19 +08:00
dijunkun
8468be6532 [fix] update MiniRTC 2026-01-12 17:27:49 +08:00
dijunkun
78c82f778a [chore] remove SetOnFileComplete() in class FileReceiver 2025-12-29 12:48:14 +08:00
dijunkun
ab89a3d41a [fix] fix clipboard copy from remote device to controller 2025-12-29 12:46:19 +08:00
dijunkun
5f320af6e6 [fix] fix macOS clipboard monitoring compilation error using changeCount polling 2025-12-29 01:01:08 +08:00
dijunkun
17b7ba6b72 [feat] support clipboard sharing, refs #35 2025-12-29 00:53:14 +08:00
dijunkun
c70ebdfe15 [fix] correct file send rate calculation by using data channel feedback 2025-12-29 00:53:14 +08:00
dijunkun
a3e564f160 [feat] support multi-file transfer, refs #45 2025-12-29 00:52:58 +08:00
dijunkun
eea37424c9 [feat] use receiver ack to calculate file transfer progress 2025-12-25 18:13:18 +08:00
dijunkun
b322181853 [feat] add file transfer window to show file sending info 2025-12-24 18:12:50 +08:00
dijunkun
3ad66f5e0b [fix] fix file transfer 2025-12-24 01:45:59 +08:00
dijunkun
4035e0dd13 [feat] upgrade MiniRTC and expose source ID in data callbacks 2025-12-23 00:53:47 +08:00
dijunkun
832b820096 [fix] update MiniRTC to latest version and add logging for sent file size 2025-12-22 18:33:53 +08:00
dijunkun
d337971de0 [feat] update MiniRTC api calling 2025-12-19 18:22:34 +08:00
dijunkun
a967dc72d7 [feat] use SendReliableDataFrame() instead of SendDataFrame() 2025-12-19 01:46:16 +08:00
dijunkun
5066fcda48 [feat] implement file transfer module 2025-12-18 18:30:51 +08:00
dijunkun
e7bdf42694 [feat] add file transfer button to control bar 2025-12-18 01:54:03 +08:00
dijunkun
875fea88ee [chore] adjust main window title font size 2025-12-18 01:32:38 +08:00
dijunkun
b2654ea9db [feat] use fingerprint-based verification for both default and self-hosted servers 2025-12-10 03:46:03 +08:00
dijunkun
8f8e415262 [chore] update README 2025-12-10 03:38:05 +08:00
dijunkun
5ff624f7b2 [feat] use fingerprint-based verification for TLS connection 2025-12-10 03:28:28 +08:00
dijunkun
e09243f1ec [chore] update README 2025-12-10 00:23:04 +08:00
dijunkun
f5941c7eda [chore] update README 2025-12-10 00:20:26 +08:00
dijunkun
3c2ebe602e [fix] do not save password info into logs 2025-12-10 00:20:10 +08:00
dijunkun
2f64172ead [feat] use new client id and password if server switched 2025-12-10 00:09:49 +08:00
Junkun Di
a83206a346 [ci] update issue templates 2025-12-09 17:50:57 +08:00
dijunkun
46e769976f [chore] update README 2025-12-09 00:36:00 +08:00
dijunkun
58c24b798e [chore] update README: refresh self-hosted server setup guide 2025-12-09 00:19:25 +08:00
dijunkun
5cc31e5ba3 [fix] fix self-hosted server configuration being reset when disabling self-hosted mode 2025-12-08 18:27:18 +08:00
dijunkun
74fe9bebf5 [fix] fix server settings window height 2025-12-08 17:42:02 +08:00
dijunkun
1f6a2182be [fix] resolve tab bar dragging issue 2025-12-06 18:46:04 +08:00
dijunkun
1a883f0d6c [fix] fix menu BeginPopup & EndPopup pairing 2025-12-05 17:53:17 +08:00
dijunkun
a560b4ca70 [feat] make thumbnail save asynchronous to prevent UI blocking 2025-12-05 17:07:55 +08:00
dijunkun
46f45ed216 [fix] fix tab close button not working in stream window 2025-12-05 15:18:35 +08:00
dijunkun
5c23f1c5e8 [fix] correct tab bar scaling and layout 2025-12-05 15:17:07 +08:00
dijunkun
70ae02549f [fix] correct title bar display 2025-12-05 04:28:33 +08:00
dijunkun
68de995c64 [fix] correct hardware codec setting item display 2025-12-05 00:57:33 +08:00
dijunkun
ed5ddb96fd [refactor] update notification window rendering for high-DPI scaling support 2025-12-04 19:00:37 +08:00
dijunkun
436dfafc2a [refactor] update about window rendering for high-DPI scaling support 2025-12-04 17:51:03 +08:00
dijunkun
5221b193e5 [refactor] update settings window rendering for high-DPI scaling support 2025-12-04 17:23:14 +08:00
dijunkun
fafced23c2 [refactor] update stream window rendering for high-DPI scaling support 2025-12-04 02:11:06 +08:00
dijunkun
1e48b645ca [refactor] update recent connections panel rendering for high-DPI scaling support 2025-12-04 00:14:09 +08:00
dijunkun
49ed0200e7 [refactor] update connection status window rendering for high-DPI scaling support 2025-12-03 21:54:10 +08:00
dijunkun
24873afe64 [refactor] update remote peer panel rendering for high-DPI scaling support 2025-12-03 21:17:11 +08:00
dijunkun
d21e1bd422 [refactor] update remote peer panel rendering for high-DPI scaling support 2025-12-03 17:36:19 +08:00
dijunkun
be044c248b [refactor] update local peer panel rendering for high-DPI scaling support 2025-12-03 17:12:35 +08:00
dijunkun
49cbbc3363 [refactor] update status bar rendering for high-DPI scaling support 2025-12-03 12:45:01 +08:00
dijunkun
1e20cb806b [refactor] update title bar rendering for high-DPI scaling support 2025-12-03 04:20:49 +08:00
dijunkun
2e52818f6f fix: correct array deletion and improve state management in WGC screen capturer 2025-12-01 23:06:46 +08:00
dijunkun
b50f386713 [fix] fix system_chinese_font_ usage to avoid dangling font pointer after closing stream window 2025-12-01 23:03:45 +08:00
dijunkun
280e011ae4 [fix] update RecentConnectionsWindow layout 2025-12-01 17:16:28 +08:00
dijunkun
8d09bf53c3 [fix] fix UpdateNotificationWindow dpi scaling 2025-12-01 13:52:39 +08:00
dijunkun
131b4f1795 [fix] resolve compilation errors on Linux 2025-12-01 13:07:36 +08:00
dijunkun
7d3ecf789d [fix] fix control bar dpi scaling 2025-12-01 11:30:35 +08:00
dijunkun
37797bf873 [fix] fix DPI scaling issues 2025-12-01 04:54:30 +08:00
dijunkun
91d42b6561 [fix] macOS: fix audio capture, refs #29 2025-11-30 17:13:02 +08:00
dijunkun
feb9f2f460 [revert] revert to the pre-lock version 2025-11-28 11:44:08 +08:00
Junkun Di
9c1753c78c [chroe] add issue templates 2025-11-28 11:34:22 +08:00
dijunkun
7370ff5b30 [chore] add HelloGitHub badge 2025-11-28 11:10:41 +08:00
dijunkun
f6eda34dbd [fix] fix dead lock during connecting 2025-11-28 10:02:57 +08:00
dijunkun
5d9a0a3ea5 [fix] fix dead lock during peer init 2025-11-28 09:32:21 +08:00
dijunkun
3d8249bffa [fix] use lock and null pointer checks to prevent crashes 2025-11-28 00:41:29 +08:00
dijunkun
82f32cbe8f [fix] fix incorrect peer reference in AddVideoStream call 2025-11-27 23:53:27 +08:00
dijunkun
56da2f99f3 [chore] update Linux log path configuration 2025-11-27 23:20:14 +08:00
dijunkun
e6c72fe558 [feat] remove screen capture and accessibility permission when reinstall 2025-11-27 17:35:15 +08:00
dijunkun
a964c6bbf5 [fix] call log after log initialzation, refs #36, #29 2025-11-27 16:09:08 +08:00
dijunkun
239da373d0 [feat] attempt to rejoin once per second 2025-11-27 04:20:29 +08:00
dijunkun
217cfb091d [feat] display version info on startup 2025-11-27 04:18:08 +08:00
dijunkun
3c3c7b9ae0 [fix] fix crash when screen recording permission is not granted on macOS, refs #29 2025-11-27 03:32:16 +08:00
dijunkun
f14bdb7fe8 [feat] optimize UpdateNotificationWindow UI appearance 2025-11-27 03:07:55 +08:00
dijunkun
c0a98f97c3 [feat] optimize RequestPermissionWindow UI appearance 2025-11-27 02:40:33 +08:00
dijunkun
0ab6686eb8 [feat] use DrawToggleSwitch to request permission 2025-11-26 23:32:04 +08:00
dijunkun
76b475450b [feat] request macOS system permissions by showing a prompt on startup 2025-11-26 18:18:40 +08:00
dijunkun
5d1e1b5667 [fix] remove permissions on uninstall and request permissions during installation for MacOS 2025-11-26 16:11:49 +08:00
dijunkun
c3b8b1374a [chore] update README 2025-11-25 16:30:14 +08:00
dijunkun
7c940d6b15 [ci] fix tag error in update-version-json.yml 2025-11-25 03:30:28 +08:00
dijunkun
86501b05dd [feat] show notification window if there is a new version avaliable 2025-11-25 01:20:16 +08:00
dijunkun
01ebed9b37 [ci] upload release notes to version.json 2025-11-25 01:19:12 +08:00
dijunkun
2188adb1f1 [chore] disable CROSSDESK_DEBUG in keyboard and mouse control 2025-11-24 16:47:33 +08:00
dijunkun
51409e16c8 [chore] update README 2025-11-23 01:54:54 +08:00
dijunkun
6ca3b58ae2 [fix] update MiniRTC to resolve occasional crash during connection shutdown, refs #29 2025-11-23 01:47:48 +08:00
dijunkun
692e176e34 [ci] use github.sha instead of hashFiles for xmake dependency cache 2025-11-23 00:35:26 +08:00
dijunkun
4fb7acaa61 [feat] set enable TURN by default 2025-11-23 00:02:04 +08:00
dijunkun
c0d6429a54 [fix] resolve missing mouse cursor display when web client connects to Linux devices, refs #30 2025-11-22 23:57:49 +08:00
dijunkun
07c7c7f179 [chore] update README 2025-11-21 02:40:51 +08:00
dijunkun
c5ceeb0d80 [ci] use China timezone for build date in version number 2025-11-21 02:33:39 +08:00
dijunkun
5ce0a891df [fix] resolve failures in connection destruction 2025-11-21 01:50:08 +08:00
dijunkun
f94ef49210 [fix] release keyboard hook after remote control disconnects, refs #23 2025-11-21 00:56:01 +08:00
dijunkun
5d0a4d1385 [fix] fix mouse wheel and touchpad swipe commands, refs #9, #23 2025-11-21 00:55:19 +08:00
dijunkun
dd482cee60 [fix] use static linking of libffi in glib to avoid version conflicts, fixes #16 2025-11-20 17:20:15 +08:00
dijunkun
e3c2edfb1c [fix] fix daemon not working on Linux 2025-11-20 15:35:37 +08:00
dijunkun
f3901d09ea [feat] add tooltip for the daemon option in the settings windows 2025-11-20 14:47:54 +08:00
Junkun Di
2b12749477 [chore] update README_EN.md 2025-11-20 13:05:14 +08:00
Junkun Di
759488f675 [chore] update README.md 2025-11-20 13:04:32 +08:00
dijunkun
4bb4240a9e [chore] update README 2025-11-20 11:15:34 +08:00
dijunkun
1457247a6a [feat] add build option USE_CUDA to enable or disable CUDA-based hardware codec acceleration and USE_CUDA=false by default 2025-11-19 23:15:56 +08:00
dijunkun
97ab9bfca5 [feat] add daemon support with automatic restart on crash 2025-11-19 22:09:51 +08:00
dijunkun
4dd3c3e073 [fix] clean display names by removing non-alphanumeric characters 2025-11-18 17:05:51 +08:00
dijunkun
4ba4f17a6b [feat] capture cursor when connected to a web client 2025-11-18 16:24:28 +08:00
dijunkun
f5d0291b5a [fix] fix crash when Unhook KeyboardCapturer on MacOS 2025-11-18 14:07:18 +08:00
dijunkun
1a64c1afef [feat] support auto-start on boot 2025-11-18 13:50:15 +08:00
dijunkun
18f4973d0a [fix] remove duplicate 'v' prefix in GitHub release name 2025-11-14 16:10:00 +08:00
dijunkun
37ede5861c [fix] update MiniRTC to fix crash when multiple peers join to remote server 2025-11-14 14:37:15 +08:00
dijunkun
497454ac51 [ci] remove 'v' prefix from Debian package version 2025-11-13 15:04:24 +08:00
dijunkun
4ebb7a6a4d [chore] update version format 2025-11-13 14:32:35 +08:00
dijunkun
7d1910df71 [ci] append build date to artifact names and write download URLs to version.json 2025-11-13 14:13:54 +08:00
dijunkun
52e70a26f3 [chore] update README 2025-11-13 00:06:14 +08:00
dijunkun
adb6cee326 [fix] fix version check issue 2025-11-12 00:21:18 +08:00
dijunkun
941b5e5cdc [ci] update version.json when new tag pushed 2025-11-12 00:11:32 +08:00
dijunkun
c602dea58f [chore] update README 2025-11-11 22:30:17 +08:00
dijunkun
e9ced9fa4f [ci] switch macos-13 to macos-15-intel due to the macOS-13 based runner images are being deprecated 2025-11-11 22:28:35 +08:00
dijunkun
6ab5e7487f [chore] update README 2025-11-11 21:59:50 +08:00
dijunkun
e3143f3e7a [feat] add automatic version check 2025-11-11 15:38:31 +08:00
dijunkun
538c17d182 [feat] disable AV1 encoding when connected to a Web client 2025-11-11 10:52:28 +08:00
dijunkun
2ad32ec2b4 [feat] add VideoQuality configuration option to limit maximum video resolution 2025-11-11 10:07:35 +08:00
dijunkun
b28f1dca81 [feat] update MiniRTC module 2025-11-11 01:32:40 +08:00
dijunkun
6947f7e1c3 Merge branch 'self-hosted-server' into libdatachannel 2025-11-11 01:18:24 +08:00
dijunkun
b1df10c0de [fix] fix frame rate and video quality settings not being applied correctly, fixes #24 2025-11-11 01:11:33 +08:00
dijunkun
3817b222fd Merge branch 'libdatachannel' of https://github.com/kunkundi/crossdesk into libdatachannel 2025-11-11 01:00:19 +08:00
dijunkun
910cc9b587 [fix] stop devices control and capture when client peer destroyed 2025-11-11 00:59:54 +08:00
dijunkun
2ee3e93afe [feat] do not control mouse in debug mode 2025-11-11 00:56:23 +08:00
dijunkun
8875c6a6a1 [feat] update MiniRTC module 2025-11-11 00:53:10 +08:00
dijunkun
28062f5574 [config] use 60 fps streaming by default 2025-11-10 17:03:14 +08:00
dijunkun
017af3eea4 [feat] support multiple web clients connecting simultaneously 2025-11-10 16:54:56 +08:00
dijunkun
78eb069cc8 [feat] enable speaker capturer by default 2025-11-10 15:02:21 +08:00
dijunkun
0d591f769d [feat] send control commands via JSON 2025-11-06 17:31:06 +08:00
dijunkun
d4726355a7 [feat] update minirtc: support web streaming by using libdatachannel 2025-11-06 02:54:03 +08:00
dijunkun
b78c9cf7d1 [feat] optimize Windows display device names 2025-11-05 20:36:04 +08:00
dijunkun
b3132db785 [fix] resolve issue where configuration settings were not saved 2025-11-03 10:55:19 +08:00
dijunkun
43db021326 [feat] use CROSSDESK_DEBUG compile-time variable to control config file path 2025-10-31 17:31:50 +08:00
dijunkun
2c622bc76e [ci] update GitHub Actions to use macos-13 runner instead of macos-15 for Intel builds 2025-10-27 22:31:23 +08:00
dijunkun
b790c7d08e [chore] update README 2025-10-27 22:11:30 +08:00
dijunkun
0ca90d2516 [chore] move minirtc into submodules 2025-10-27 21:36:11 +08:00
dijunkun
401bfe4483 [refactor] add namespace 'crossdesk' to codebase 2025-10-27 21:09:39 +08:00
dijunkun
3b34c26555 [feat] enable custom configuration of Coturn server port 2025-10-27 16:04:40 +08:00
dijunkun
b668b3c936 [chore] update README 2025-10-27 13:42:49 +08:00
dijunkun
cc19ec125a [ci] update close-issue.yml 2025-10-27 11:04:36 +08:00
dijunkun
ffa77fdf44 Merge branch 'run-in-bg' into self-hosted-server 2025-10-27 10:41:16 +08:00
dijunkun
47cf806532 [ci] add permissions to close-issue.yml 2025-10-25 17:06:14 +08:00
dijunkun
911dce2e71 [feat] optimize certificate selection table 2025-10-24 17:39:25 +08:00
dijunkun
9f80d4f69d Merge branch 'self-hosted-server' into run-in-bg 2025-10-24 14:16:18 +08:00
dijunkun
f733fe9e49 Merge branch 'self-hosted-server' into run-in-bg 2025-10-24 11:05:07 +08:00
dijunkun
698bf72a6c Merge branch 'self-hosted-server' into run-in-bg 2025-10-24 10:05:54 +08:00
dijunkun
b2ab940f20 [feat] use no close select table 2025-10-23 17:55:30 +08:00
140 changed files with 20127 additions and 5065 deletions

35
.github/ISSUE_TEMPLATE/问题反馈.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: 问题反馈
about: 请在此提交问题报告,以便持续优化产品。
title: ''
labels: bug
assignees: kunkundi
---
**描述问题**
清晰简洁地描述遇到的错误。
**复现步骤**
复现该问题的步骤:
1. 前往 '...'
2. 点击 '....'
3. 出现错误
**预期行为**
清晰简洁地描述你期望发生的行为。
**截图**
如果适用,请添加截图以帮助说明问题。
**桌面端信息(请填写以下内容):**
- 操作系统: [例如 Windows 11]
- 版本: [例如 v1.1.10]
**移动端信息(请填写以下内容):**
- 设备: [例如 iPhone 17]
- 操作系统: [例如 iOS 26.1]
- 浏览器: [例如 系统浏览器、Safari]
**补充信息**
在此添加与问题相关的其他上下文内容。

28
.github/ISSUE_TEMPLATE/需求建议.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: 需求建议
about: 请在此提交功能需求或改进建议,以便后续迭代参考。
title: ''
labels: enhancement
assignees: kunkundi
---
**功能/改进建议描述**
清晰简洁地描述希望新增的功能或改进的内容。
**使用场景 / 背景**
说明该功能或改进的使用场景,以及解决后带来的价值。
**预期效果**
描述你认为最理想的功能表现或改进效果。
**参考示例(可选)**
提供类似功能截图、参考链接或其他说明,帮助更好理解需求。
**优先级(可选)**
- [ ]
- [ ]
- [ ]
**补充信息(可选)**
其他相关信息或特殊要求。

View File

@@ -15,80 +15,28 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
# Linux amd64
build-linux-amd64:
name: Build on Ubuntu 22.04 amd64
runs-on: ubuntu-22.04
container:
image: crossdesk/ubuntu20.04:latest
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
- name: Set legal Debian version
shell: bash
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="0.0.0-${VERSION_NUM}-${SHORT_SHA}"
else
LEGAL_VERSION="${VERSION_NUM}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build CrossDesk
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ./scripts/linux/pkg_amd64.sh
./scripts/linux/pkg_amd64.sh ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}.deb
# Linux arm64
build-linux-arm64:
name: Build on Ubuntu 22.04 arm64
runs-on: ubuntu-22.04-arm
build-linux:
name: Build Linux (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
image: crossdesk/ubuntu20.04:latest
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64
runner: ubuntu-22.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
@@ -96,18 +44,21 @@ jobs:
- name: Set legal Debian version
shell: bash
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="0.0.0-${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: recursive
@@ -116,22 +67,16 @@ jobs:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
@@ -160,15 +105,17 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${VERSION#v}-${SHORT_SHA}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.xmake/packages
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ hashFiles('**/xmake.lua') }}
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-
@@ -176,29 +123,23 @@ jobs:
run: brew install xmake
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Initialize submodules
run: git submodule update --init --recursive
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${VERSION_NUM} -y
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package CrossDesk app
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
@@ -223,13 +164,15 @@ jobs:
$version = $version -replace '^v', ''
$version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
echo "VERSION_NUM=$version-$SHORT_SHA" >> $env:GITHUB_ENV
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: D:\xmake_global\.xmake\packages
key: ${{ runner.os }}-xmake-deps-intel-${{ hashFiles('**/xmake.lua') }}
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
restore-keys: |
${{ runner.os }}-xmake-deps-intel-
@@ -278,7 +221,7 @@ jobs:
Copy-Item $source $target -Force
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -290,14 +233,8 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} -y
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path certs
[System.IO.File]::WriteAllBytes('certs\crossdesk.cn_root.crt', [Convert]::FromBase64String('${{ secrets.CROSSDESK_CERT_BASE64 }}'))
- name: Package
shell: pwsh
@@ -305,25 +242,39 @@ jobs:
cd "${{ github.workspace }}\scripts\windows"
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Package Portable
shell: pwsh
run: |
$portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux-amd64, build-linux-arm64, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
@@ -332,17 +283,26 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${VERSION#v}-${SHORT_SHA}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
- name: Rename artifacts
run: |
mkdir -p release
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_NUM }}.pkg
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_NUM }}.pkg
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_NUM }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_NUM }}.deb
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_NUM }}.exe
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
- name: List release files
run: ls -lh release/
@@ -350,8 +310,8 @@ jobs:
- name: Upload to Versioned GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.VERSION_NUM }}
name: Release v${{ steps.version.outputs.VERSION_NUM }}
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
draft: false
prerelease: false
files: release/*
@@ -385,3 +345,52 @@ jobs:
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}
- name: Generate version.json
run: |
cat > version.json << EOF
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"releaseName": "",
"releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
"downloads": {
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
},
"macos-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
},
"linux-amd64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
},
"linux-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
}
}
}
EOF
cat version.json
- name: Upload version.json to server
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: version.json
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}

View File

@@ -5,6 +5,11 @@ on:
# run every day at midnight
- cron: "0 0 * * *"
permissions:
issues: write
pull-requests: write
contents: read
jobs:
close_inactive_issues:
runs-on: ubuntu-latest
@@ -53,6 +58,14 @@ jobs:
// check inactivity
if (now - lastActivityTime > inactivePeriod) {
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: "This issue has been automatically closed due to inactivity for 7 days."
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,

View File

@@ -1,49 +0,0 @@
name: Update GitHub Pages Downloads
on:
push:
tags:
- "v*"
jobs:
update-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout CrossDesk repo
uses: actions/checkout@v4
- name: Set version number
id: version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION_NUM#v}-${SHORT_SHA}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
- name: Checkout Pages repo
uses: actions/checkout@v4
with:
repository: kunkundi/kunkundi.github.io
token: ${{ secrets.GH_PAGES_PAT }}
path: pages
- name: Update download links
run: |
cd pages
sed -E -i "s/crossdesk-win-x64-[0-9]+\.[0-9]+\.[0-9]+\.exe/crossdesk-win-x64-${VERSION_NUM}.exe/g" index.html
sed -E -i "s/crossdesk-macos-x64-[0-9]+\.[0-9]+\.[0-9]+\.pkg/crossdesk-macos-x64-${VERSION_NUM}.pkg/g" index.html
sed -E -i "s/crossdesk-macos-arm64-[0-9]+\.[0-9]+\.[0-9]+\.pkg/crossdesk-macos-arm64-${VERSION_NUM}.pkg/g" index.html
sed -E -i "s/crossdesk-linux-amd64-[0-9]+\.[0-9]+\.[0-9]+\.deb/crossdesk-linux-amd64-${VERSION_NUM}.deb/g" index.html
sed -E -i "s/crossdesk-linux-arm64-[0-9]+\.[0-9]+\.[0-9]+\.deb/crossdesk-linux-arm64-${VERSION_NUM}.deb/g" index.html
- name: Commit & Push changes
run: |
cd pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add index.html
git commit -m "Update download links to v${VERSION_NUM}" || echo "No changes to commit"
git push origin main
env:
VERSION_NUM: ${{ env.VERSION_NUM }}

View File

@@ -0,0 +1,144 @@
name: Update version.json from Release
on:
release:
types: [published, edited]
permissions:
contents: write
jobs:
update-version-json:
name: Update version.json with release information
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: version
run: |
TAG_NAME="${{ github.event.release.tag_name }}"
VERSION_ONLY="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
# Extract date from tag if available (format: v1.2.3-20251113-abc)
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
DATE_STR="${BASH_REMATCH[1]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
else
# Use release published date
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
fi
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Get release information
id: release_info
run: |
# Use jq to properly escape JSON
RELEASE_BODY="${{ github.event.release.body }}"
RELEASE_NAME="${{ github.event.release.name }}"
# Handle empty values
if [ -z "$RELEASE_BODY" ]; then
RELEASE_BODY=""
fi
if [ -z "$RELEASE_NAME" ]; then
RELEASE_NAME=""
fi
# Save to temporary files for proper handling
echo -n "$RELEASE_BODY" > /tmp/release_body.txt
echo -n "$RELEASE_NAME" > /tmp/release_name.txt
# Use jq to escape JSON strings
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
- name: Download current version.json from server
id: download_version
continue-on-error: true
run: |
# Try to download current version.json from server
curl -f -s "https://version.crossdesk.cn/version.json" -o version.json || echo "Failed to download, will create new one"
- name: Generate or update version.json
run: |
# If version.json exists, try to preserve downloads section
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
else
DOWNLOADS_JSON=""
fi
else
DOWNLOADS_JSON=""
fi
# If downloads is empty, use default structure
if [ -z "$DOWNLOADS_JSON" ]; then
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
{
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"macos-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"linux-amd64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb"
},
"linux-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb"
}
}
DOWNLOADS_EOF
)
fi
# Generate version.json using cat and heredoc
cat > version.json << EOF
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
"downloads": ${DOWNLOADS_JSON}
}
EOF
cat version.json
- name: Upload version.json to server
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: version.json
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}

3
.gitignore vendored
View File

@@ -6,5 +6,4 @@ build/
.DS_Store
# VSCode cache
.vscode
continuous-desk.code-workspace
.vscode

4
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "thirdparty/minirtc"]
path = thirdparty/minirtc
[submodule "submodules/minirtc"]
path = submodules/minirtc
url = https://github.com/kunkundi/minirtc.git

242
README.md
View File

@@ -1,8 +1,9 @@
# CrossDesk
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="FeaturedHelloGitHub" /></a>
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/self-hosted-server)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)]()
@@ -11,14 +12,27 @@
[ [English](README_EN.md) / 中文 ]
PC 客户端
![sup_example](https://github.com/user-attachments/assets/eeb64fbe-1f07-4626-be1c-b77396beb905)
Web 客户端
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
## 简介
CrossDesk 是一个轻量级的跨平台远程桌面软件。
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)视频软硬编解码H264/AV1音频编解码[Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
## 系统要求
| 平台 | 最低版本 |
|----------------|---------------------------|
| **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
## 使用
@@ -33,6 +47,12 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
### Web 客户端
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
输入 **远程设备 ID****密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
## 如何编译
依赖:
@@ -42,7 +62,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
Linux环境下需安装以下包
```
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
```
编译
@@ -57,7 +77,14 @@ git submodule update
xmake b -vy crossdesk
```
编译选项
```
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
运行
```
xmake r crossdesk
@@ -65,13 +92,14 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
进入容器,下载工程后执行:
```
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -93,6 +121,7 @@ set CUDA_PATH=path_to_cuda_installdir
```
重新执行:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -140,161 +169,96 @@ xmake r -d crossdesk
## 自托管服务器
推荐使用Docker部署CrossDesk Server。
```
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=9099 \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:latest
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
上述命令中,用户需注意的参数如下:
**参数**
- EXTERNAL_IP服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
- INTERNAL_IP服务器内网 IP
- CROSSDESK_SERVER_PORT自托管服务使用的端口对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
- MIN_PORT/MAX_PORTCOTURN 服务使用的端口范围例如MIN_PORT=50000, MAX_PORT=60000范围可根据客户端数量调整。
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
- /path/to/your/certs证书文件目录
- /path/to/your/dbCrossDesk Server 设备管理数据库
- /path/to/your/logs日志目录
**示例**
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
**注意**
- **/path/to/your/ 是示例路径,请替换为你自己的实际路径。挂载的目录必须事先创建好,否则容器会报错。**
- **服务器需开放端口3478/udp3478/tcp30000-60000/udpCROSSDESK_SERVER_PORT/tcp443/tcp。**
- **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
## 证书文件
客户端需加载根证书文件,服务端需加载服务器私钥和服务器证书文件。
如果已有SSL证书的用户可以忽略下面的证书生成步骤。
对于无证书的用户,可使用下面的脚本自行生成证书文件
```
# 创建证书生成脚本
vim generate_certs.sh
```
拷贝到脚本中
```
#!/bin/bash
set -e
# 检查参数
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <SERVER_IP>"
exit 1
fi
SERVER_IP="$1"
# 文件名
ROOT_KEY="crossdesk.cn_root.key"
ROOT_CERT="crossdesk.cn_root.crt"
SERVER_KEY="crossdesk.cn.key"
SERVER_CSR="crossdesk.cn.csr"
SERVER_CERT="crossdesk.cn_bundle.crt"
FULLCHAIN_CERT="crossdesk.cn_fullchain.crt"
# 证书主题
SUBJ="/C=CN/ST=Zhejiang/L=Hangzhou/O=CrossDesk/OU=CrossDesk/CN=$SERVER_IP"
# 1. 生成根证书
echo "Generating root private key..."
openssl genrsa -out "$ROOT_KEY" 4096
echo "Generating self-signed root certificate..."
openssl req -x509 -new -nodes -key "$ROOT_KEY" -sha256 -days 3650 -out "$ROOT_CERT" -subj "$SUBJ"
# 2. 生成服务器私钥
echo "Generating server private key..."
openssl genrsa -out "$SERVER_KEY" 2048
# 3. 生成服务器 CSR
echo "Generating server CSR..."
openssl req -new -key "$SERVER_KEY" -out "$SERVER_CSR" -subj "$SUBJ"
# 4. 生成临时 OpenSSL 配置文件,加入 SAN
SAN_CONF="san.cnf"
cat > $SAN_CONF <<EOL
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = CN
ST = Zhejiang
L = Hangzhou
O = CrossDesk
OU = CrossDesk
CN = $SERVER_IP
[ req_ext ]
subjectAltName = IP:$SERVER_IP
EOL
# 5. 用根证书签发服务器证书(包含 SAN
echo "Signing server certificate with root certificate..."
openssl x509 -req -in "$SERVER_CSR" -CA "$ROOT_CERT" -CAkey "$ROOT_KEY" -CAcreateserial \
-out "$SERVER_CERT" -days 3650 -sha256 -extfile "$SAN_CONF" -extensions req_ext
# 6. 生成完整链证书
cat "$SERVER_CERT" "$ROOT_CERT" > "$FULLCHAIN_CERT"
# 7. 清理中间文件
rm -f "$ROOT_CERT.srl" "$SAN_CONF" "$ROOT_KEY" "$SERVER_CSR" "FULLCHAIN_CERT"
echo "Generation complete. Deployment files:"
echo " Client root certificate: $ROOT_CERT"
echo " Server private key: $SERVER_KEY"
echo " Server certificate: $SERVER_CERT"
```
执行
```
chmod +x generate_certs.sh
./generate_certs.sh 服务器公网IP
# 例如 ./generate_certs.sh 111.111.111.111
```
输出如下:
```
Generating root private key...
Generating self-signed root certificate...
Generating server private key...
Generating server CSR...
Signing server certificate with root certificate...
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = CrossDesk, OU = CrossDesk, CN = xxx.xxx.xxx.xxx
cleaning up intermediate files...
Generation complete. Deployment files::
Client root certificate:: crossdesk.cn_root.crt
Server private key: crossdesk.cn.key
Server certificate: crossdesk.cn_bundle.crt
**权限注意**:如果 Docker 自动创建的目录权限不足(属于 root容器内用户无法写入会导致
- 证书生成失败,容器启动脚本会报错退出
- 数据库目录创建失败,程序会抛出异常并崩溃
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
**解决方案**:在启动容器前手动设置权限
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
#### 服务端
**crossdesk.cn.key****crossdesk.cn_bundle.crt** 放置到 **/path/to/your/certs** 目录下。
#### 客户端
1. 点击右上角设置进入设置页面。<br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
3. 点击点击**自托管服务器配置**。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
5. 在**证书文件路径**选择框中找到 **crossdesk.cn_root.crt** 的存放路径,选中 **crossdesk.cn_root.crt**,点击确认。<br><br>
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
7. 勾选使用**自托管服务器配置**,点击确认配置生效。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
Windows 平台使用**管理员权限**打开 PowerShell 执行
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
# 常见问题
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。

View File

@@ -1,8 +1,9 @@
# CrossDesk
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="FeaturedHelloGitHub" /></a>
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/self-hosted-server)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)]()
@@ -11,14 +12,29 @@
[ [中文](README.md) / English ]
PC Client
![sup_example](https://github.com/user-attachments/assets/3f17d8f3-7c4a-4b63-bae4-903363628687)
Web Client
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
# Intro
CrossDesk is a lightweight cross-platform remote desktop software.
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
## System Requirements
| Platform | Minimum Version |
|-----------|-----------------|
| **Windows** | Windows 10 or later (64-bit) |
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
| **Linux** | Ubuntu 22.04 or later *(older versions can be built manually for compatibility)* |
## Usage
Enter the remote desktop ID in the menu bars “Remote ID” field and click “→” to initiate a remote connection.
@@ -33,6 +49,13 @@ Before connecting, you can customize configuration options in the settings, such
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
### Web Client
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
## How to build
Requirements:
@@ -42,7 +65,7 @@ Requirements:
Following packages need to be installed on Linux:
```
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
```
Build:
@@ -57,7 +80,14 @@ git submodule update
xmake b -vy crossdesk
```
Build options:
```
--USE_CUDA=true/false: enable CUDA acceleration codec, default: false
--CROSSDESK_VERSION=xxx: set the version number
# example:
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
Run:
```
xmake r crossdesk
@@ -65,7 +95,7 @@ xmake r crossdesk
#### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment** installed, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
After entering the container, download the project and run:
@@ -73,6 +103,7 @@ After entering the container, download the project and run:
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -95,6 +126,7 @@ set CUDA_PATH=path_to_cuda_installdir:
```
Then re-run:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -149,157 +181,96 @@ It is recommended to deploy CrossDesk Server using Docker.
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=150.158.81.30 \
-e INTERNAL_IP=10.0.4.3 \
-e CROSSDESK_SERVER_PORT=9099 \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:latest
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
The parameters you need to pay attention to are as follows:
- **EXTERNAL_IP**: The server's public IP, corresponding to the **Server Address** in the CrossDesk client **Self-Hosted Server Configuration**.
**Parameters**
- **EXTERNAL_IP**: The servers public IP. This corresponds to **Server Address** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **INTERNAL_IP**: The servers internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
- **INTERNAL_IP**: The server's internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted server, corresponding to the **Server Port** in the CrossDesk client **Self-Hosted Server Configuration**.
- **/path/to/your/certs**: Directory for certificate files.
- **/path/to/your/db**: CrossDesk Server device management database.
- **/path/to/your/logs**: Log directory.
**Note**:
- **/path/to/your/ is an example path; please replace it with your actual path. The mounted directories must be created in advance, otherwise the container will fail.**
- **The server must open the following ports: 3478/udp, 3478/tcp, 30000-60000/udp, CROSSDESK_SERVER_PORT/tcp, 443/tcp.**
## Certificate Files
The client needs to load the root certificate, and the server needs to load the server private key and server certificate.
If you already have an SSL certificate, you can skip the following certificate generation steps.
For users without a certificate, you can use the script below to generate the certificate files:
```
# Create certificate generation script
vim generate_certs.sh
```
Copy the following into the script:
```
#!/bin/bash
set -e
# Check arguments
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <SERVER_IP>"
exit 1
fi
SERVER_IP="$1"
# Filenames
ROOT_KEY="crossdesk.cn_root.key"
ROOT_CERT="crossdesk.cn_root.crt"
SERVER_KEY="crossdesk.cn.key"
SERVER_CSR="crossdesk.cn.csr"
SERVER_CERT="crossdesk.cn_bundle.crt"
FULLCHAIN_CERT="crossdesk.cn_fullchain.crt"
# Certificate subject
SUBJ="/C=CN/ST=Zhejiang/L=Hangzhou/O=CrossDesk/OU=CrossDesk/CN=$SERVER_IP"
# 1. Generate root certificate
echo "Generating root private key..."
openssl genrsa -out "$ROOT_KEY" 4096
echo "Generating self-signed root certificate..."
openssl req -x509 -new -nodes -key "$ROOT_KEY" -sha256 -days 3650 -out "$ROOT_CERT" -subj "$SUBJ"
# 2. Generate server private key
echo "Generating server private key..."
openssl genrsa -out "$SERVER_KEY" 2048
# 3. Generate server CSR
echo "Generating server CSR..."
openssl req -new -key "$SERVER_KEY" -out "$SERVER_CSR" -subj "$SUBJ"
# 4. Create temporary OpenSSL config file with SAN
SAN_CONF="san.cnf"
cat > $SAN_CONF <<EOL
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = CN
ST = Zhejiang
L = Hangzhou
O = CrossDesk
OU = CrossDesk
CN = $SERVER_IP
[ req_ext ]
subjectAltName = IP:$SERVER_IP
EOL
# 5. Sign server certificate with root certificate (including SAN)
echo "Signing server certificate with root certificate..."
openssl x509 -req -in "$SERVER_CSR" -CA "$ROOT_CERT" -CAkey "$ROOT_KEY" -CAcreateserial \
-out "$SERVER_CERT" -days 3650 -sha256 -extfile "$SAN_CONF" -extensions req_ext
# 6. Generate full chain certificate
cat "$SERVER_CERT" "$ROOT_CERT" > "$FULLCHAIN_CERT"
# 7. Clean up intermediate files
rm -f "$ROOT_CERT.srl" "$SAN_CONF" "$ROOT_KEY" "$SERVER_CSR" "FULLCHAIN_CERT"
echo "Generation complete. Deployment files:"
echo " Client root certificate: $ROOT_CERT"
echo " Server private key: $SERVER_KEY"
echo " Server certificate: $SERVER_CERT"
```
Execute:
```
chmod +x generate_certs.sh
./generate_certs.sh EXTERNAL_IP
# example ./generate_certs.sh 111.111.111.111
```
Expected output:
```
Generating root private key...
Generating self-signed root certificate...
Generating server private key...
Generating server CSR...
Signing server certificate with root certificate...
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = CrossDesk, OU = CrossDesk, CN = xxx.xxx.xxx.xxx
cleaning up intermediate files...
Generation complete. Deployment files::
Client root certificate:: crossdesk.cn_root.crt
Server private key: crossdesk.cn.key
Server certificate: crossdesk.cn_bundle.crt
**Example**:
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### Server Side
**Notes**
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORTMAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you dont mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`.
**Permission Notice**
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
- Certificate generation failure, leading to startup script errors and container exit.
- Database directory creation failure, causing the program to throw exceptions and crash.
- Log directory creation failure, preventing logs from being written (though the program may continue running).
**Solution:** Manually set permissions before starting the container:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
### Server Side
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
#### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. Click **Self-Hosted Server Configuration**.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. In the **Certificate File Path** selection, locate and select the **crossdesk.cn_root.crt** file.<br><br>
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
4. Check the option to use **Self-Hosted Server Configuration**.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
On Windows, open PowerShell with **administrator privileges** and execute:
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
# FAQ
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -9,24 +9,24 @@ ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
DEB_DIR="${PKG_NAME}-${APP_VERSION}"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -35,14 +35,16 @@ done
cat > "$DEBIAN_DIR/control" << EOF
Package: $PKG_NAME
Version: $APP_VERSION
Version: $DEB_VERSION
Architecture: $ARCHITECTURE
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
libpipewire-0.3-0, xdg-desktop-portal,
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
Recommends: nvidia-cuda-toolkit
Priority: optional
Section: utils
@@ -50,7 +52,7 @@ EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
[Desktop Entry]
Version=$APP_VERSION
Version=$DEB_VERSION
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME
@@ -68,7 +70,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -82,32 +83,9 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
chmod +x "$DEBIAN_DIR/postinst"
dpkg-deb --build "$DEB_DIR"
@@ -117,4 +95,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"

View File

@@ -9,24 +9,24 @@ ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
DEB_DIR="${PKG_NAME}-${APP_VERSION}"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/arm64/release/crossdesk "$BIN_DIR"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -35,21 +35,23 @@ done
cat > "$DEBIAN_DIR/control" << EOF
Package: $PKG_NAME
Version: $APP_VERSION
Version: $DEB_VERSION
Architecture: $ARCHITECTURE
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
libpipewire-0.3-0, xdg-desktop-portal,
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
Priority: optional
Section: utils
EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
[Desktop Entry]
Version=$APP_VERSION
Version=$DEB_VERSION
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME
@@ -67,7 +69,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -81,30 +82,6 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
@@ -117,4 +94,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"

91
scripts/macosx/pkg_arm64.sh Normal file → Executable file
View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -86,39 +83,95 @@ pkgbuild \
--component "${APP_BUNDLE}" \
build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p scripts
mkdir -p build_pkg_scripts
cat > scripts/postinstall <<'EOF'
cat > build_pkg_scripts/postinstall <<'EOF'
#!/bin/bash
set -e
IDENTIFIER="cn.crossdesk.app"
# 获取当前登录用户
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
# 重置录屏权限
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
# 重置辅助功能权限
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
# 重置摄像头权限(如果需要)
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
# 重置麦克风权限(如果需要)
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
fi
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 为所有用户清除权限(可选,如果需要)
# 遍历所有用户目录并清除权限
for USER_DIR in /Users/*; do
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
USER_NAME=$(basename "$USER_DIR")
# 跳过系统用户
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
# 删除 TCC 数据库中的相关条目(需要管理员权限)
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
if [ -f "$TCC_DB" ]; then
# 使用 sqlite3 删除相关权限记录(如果可用)
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
fi
fi
fi
fi
done
exit 0
EOF
chmod +x scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
chmod +x build_pkg_scripts/postinstall
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
rm -rf build_pkg_temp scripts ${APP_BUNDLE}
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"

91
scripts/macosx/pkg_x64.sh Normal file → Executable file
View File

@@ -11,9 +11,6 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -86,39 +83,95 @@ pkgbuild \
--component "${APP_BUNDLE}" \
build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p scripts
mkdir -p build_pkg_scripts
cat > scripts/postinstall <<'EOF'
cat > build_pkg_scripts/postinstall <<'EOF'
#!/bin/bash
set -e
IDENTIFIER="cn.crossdesk.app"
# 获取当前登录用户
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
# 重置录屏权限
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
# 重置辅助功能权限
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
# 重置摄像头权限(如果需要)
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
# 重置麦克风权限(如果需要)
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
fi
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 为所有用户清除权限(可选,如果需要)
# 遍历所有用户目录并清除权限
for USER_DIR in /Users/*; do
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
USER_NAME=$(basename "$USER_DIR")
# 跳过系统用户
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
# 删除 TCC 数据库中的相关条目(需要管理员权限)
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
if [ -f "$TCC_DB" ]; then
# 使用 sqlite3 删除相关权限记录(如果可用)
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
fi
fi
fi
fi
done
exit 0
EOF
chmod +x scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
chmod +x build_pkg_scripts/postinstall
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
rm -rf build_pkg_temp scripts ${APP_BUNDLE}
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"

View File

@@ -0,0 +1,2 @@
// Application icon (IDI_ICON1 = 1, which is the default app icon resource ID)
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"

View File

@@ -12,9 +12,6 @@
; Installer icon path
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
; Certificate path
!define CERT_FILE "${__FILEDIR__}\..\..\certs\crossdesk.cn_root.crt"
; Compression settings
SetCompressor /FINAL lzma
@@ -49,7 +46,7 @@ ShowInstDetails show
Section "MainSection"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -75,10 +72,7 @@ installApp:
SetOverwrite ifnewer
; Main application executable path
File /oname=crossdesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Copy icon file to installation directory
File "${MUI_ICON}"
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -88,33 +82,23 @@ installApp:
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\crossdesk.ico"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
SectionEnd
; After installation
Section -Post
ExecWait '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\mt.exe" -manifest "$INSTDIR\crossdesk.manifest" -outputresource:"$INSTDIR\crossdesk.exe";1'
SectionEnd
Section "Cert"
SetOutPath "$APPDATA\CrossDesk\certs"
File /r "${CERT_FILE}"
SectionEnd
Section -AdditionalIcons
; Desktop shortcut
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
; Start menu shortcut
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
SectionEnd
Section "Uninstall"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -137,7 +121,7 @@ cancelUninstall:
uninstallApp:
; Delete main executable and uninstaller
Delete "$INSTDIR\crossdesk.exe"
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory
@@ -160,5 +144,5 @@ SectionEnd
; ------ Functions ------
Function LaunchApp
Exec "$INSTDIR\crossdesk.exe"
Exec "$INSTDIR\CrossDesk.exe"
FunctionEnd

338
src/app/daemon.cpp Normal file
View File

@@ -0,0 +1,338 @@
#include "daemon.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <iostream>
#include <thread>
#include <vector>
#ifdef _WIN32
#include <process.h>
#include <tchar.h>
#include <windows.h>
#elif __APPLE__
#include <fcntl.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#endif
#ifndef _WIN32
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
#endif
namespace {
constexpr int kRestartDelayMs = 1000;
#ifndef _WIN32
constexpr int kWaitPollIntervalMs = 200;
#endif
} // namespace
// get executable file path
static std::string GetExecutablePath() {
#ifdef _WIN32
char path[32768];
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
if (length > 0 && length < sizeof(path)) {
return std::string(path);
}
#elif __APPLE__
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char resolved_path[PATH_MAX];
if (realpath(path, resolved_path) != nullptr) {
return std::string(resolved_path);
}
return std::string(path);
}
#else
char path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (count != -1) {
path[count] = '\0';
return std::string(path);
}
#endif
return "";
}
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
void Daemon::stop() {
running_.store(false);
#ifndef _WIN32
stop_requested_ = 1;
#endif
}
bool Daemon::isRunning() const {
#ifndef _WIN32
return running_.load() && (stop_requested_ == 0);
#else
return running_.load();
#endif
}
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
running_.store(true);
return runWithRestart(loop);
#elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI
stop_requested_ = 0;
running_.store(true);
return runWithRestart(loop);
#else
// linux: Daemonize first, then run with restart monitoring
stop_requested_ = 0;
// check if running from terminal before fork
bool from_terminal =
(isatty(STDIN_FILENO) != 0) || (isatty(STDOUT_FILENO) != 0);
// first fork: detach from terminal
pid_t pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process" << std::endl;
return false;
}
if (pid > 0) _exit(0);
if (setsid() < 0) {
std::cerr << "Failed to create new session" << std::endl;
return false;
}
pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process (second fork)" << std::endl;
return false;
}
if (pid > 0) _exit(0);
umask(0);
chdir("/");
// redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
if (!from_terminal) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
}
if (fd > 2) close(fd);
}
// set up signal handlers
signal(SIGTERM, [](int) { stop_requested_ = 1; });
signal(SIGINT, [](int) { stop_requested_ = 1; });
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
running_.store(true);
return runWithRestart(loop);
#endif
}
#ifdef _WIN32
static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
try {
loop();
return 0; // normal exit
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
return 1; // c++ exception
} catch (...) {
std::cerr << "Unknown exception caught" << std::endl;
return 1; // other exception
}
}
static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
__try {
return RunLoopCatchCpp(loop);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// catch system-level crashes (access violation, divide by zero, etc.)
DWORD code = GetExceptionCode();
std::cerr << "System crash detected (SEH exception code: 0x" << std::hex
<< code << std::dec << ")" << std::endl;
return 2; // System crash
}
}
#endif
// run with restart logic: parent monitors child process and restarts on crash
bool Daemon::runWithRestart(MainLoopFunc loop) {
int restart_count = 0;
std::string exe_path = GetExecutablePath();
if (exe_path.empty()) {
std::cerr
<< "Failed to get executable path, falling back to direct execution"
<< std::endl;
while (isRunning()) {
try {
loop();
break;
} catch (...) {
restart_count++;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
}
}
return true;
}
while (isRunning()) {
#ifdef _WIN32
// windows: use CreateProcess to create child process
STARTUPINFOA si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
std::string cmd_line = "\"" + exe_path + "\" --child";
std::vector<char> cmd_line_buf(cmd_line.begin(), cmd_line.end());
cmd_line_buf.push_back('\0');
BOOL success = CreateProcessA(
nullptr, // executable file path (specified in command line)
cmd_line_buf.data(), // command line arguments
nullptr, // process security attributes
nullptr, // thread security attributes
FALSE, // don't inherit handles
0, // creation flags
nullptr, // environment variables (inherit from parent)
nullptr, // current directory
&si, // startup info
&pi // process information
);
if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
restart_count++;
continue;
}
while (isRunning()) {
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
if (wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result == WAIT_FAILED) {
std::cerr << "Failed waiting child process, error: " << GetLastError()
<< std::endl;
break;
}
}
if (!isRunning()) {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, 3000);
}
DWORD exit_code = 0;
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (!isRunning() || exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
#else
// linux: use fork + exec to create child process
pid_t pid = fork();
if (pid == 0) {
execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr);
_exit(1); // exec failed
} else if (pid > 0) {
int status = 0;
pid_t waited_pid = -1;
while (isRunning()) {
waited_pid = waitpid(pid, &status, WNOHANG);
if (waited_pid == pid) {
break;
}
if (waited_pid < 0 && errno != EINTR) {
break;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kWaitPollIntervalMs));
}
if (!isRunning() && waited_pid != pid) {
kill(pid, SIGTERM);
waited_pid = waitpid(pid, &status, 0);
}
if (waited_pid < 0) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
continue;
}
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (!isRunning() || exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else if (WIFSIGNALED(status)) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else {
restart_count++;
std::cerr << "Child process exited with unknown status, restarting... "
"(attempt "
<< restart_count << ")" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
} else {
std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
restart_count++;
}
#endif
}
return true;
}

37
src/app/daemon.h Normal file
View File

@@ -0,0 +1,37 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-19
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <atomic>
#include <csignal>
#include <functional>
#include <string>
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
Daemon(const std::string& name);
bool start(MainLoopFunc loop);
void stop();
bool isRunning() const;
private:
std::string name_;
bool runWithRestart(MainLoopFunc loop);
#ifndef _WIN32
static volatile std::sig_atomic_t stop_requested_;
#endif
std::atomic<bool> running_;
};
#endif

View File

@@ -1,17 +1,63 @@
#ifdef _WIN32
#ifdef DESK_PORT_DEBUG
#ifdef CROSSDESK_DEBUG
#pragma comment(linker, "/subsystem:\"console\"")
#else
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#endif
#endif
#include "rd_log.h"
#include <cstring>
#include <memory>
#include <string>
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "render.h"
int main([[maybe_unused]] int argc, [[maybe_unused]] char *argv[]) {
Render render;
render.Run();
int main(int argc, char* argv[]) {
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
}
if (is_child) {
// child process: run render directly
crossdesk::Render render;
render.Run();
return 0;
}
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
}
if (enable_daemon) {
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
crossdesk::Render render;
render.Run();
daemon.stop();
};
// start daemon and return result
bool success = daemon.start(main_loop);
return success ? 0 : 1;
}
// run without daemon: direct execution
crossdesk::Render render;
render.Run();
return 0;
}

302
src/autostart/autostart.cpp Normal file
View File

@@ -0,0 +1,302 @@
#include "autostart.h"
#include <cstdlib>
#include <filesystem>
#include <fstream>
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <limits.h>
#include <mach-o/dyld.h>
#include <unistd.h>
#elif defined(__linux__)
#include <linux/limits.h>
#include <unistd.h>
#endif
namespace crossdesk {
static std::string get_home_dir() {
const char* home = std::getenv("HOME");
if (!home) {
return "";
}
return std::string(home);
}
static bool file_exists(const std::string& path) {
return std::filesystem::exists(path) &&
std::filesystem::is_regular_file(path);
}
static std::string GetExecutablePath() {
#ifdef _WIN32
char path[32768];
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
if (length > 0 && length < sizeof(path)) {
return std::string(path);
}
#elif defined(__APPLE__)
char path[1024];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char resolved_path[PATH_MAX];
if (realpath(path, resolved_path) != nullptr) {
return std::string(resolved_path);
}
return std::string(path);
}
#elif defined(__linux__)
char path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (count != -1) {
path[count] = '\0';
return std::string(path);
}
#endif
return "";
}
// Windows
#ifdef _WIN32
static constexpr const char* WINDOWS_RUN_KEY =
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
static bool windows_enable(const std::string& appName,
const std::string& exePath) {
if (exePath.empty() || !std::filesystem::exists(exePath)) {
return false;
}
HKEY hKey = nullptr;
// Use KEY_WRITE to ensure we have write permission
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
std::string regValue = exePath;
if (!exePath.empty() && exePath.find(' ') != std::string::npos) {
if (exePath.front() != '"' || exePath.back() != '"') {
regValue = "\"" + exePath + "\"";
}
}
// Ensure we close the key even if RegSetValueExA fails
result = RegSetValueExA(hKey, appName.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(regValue.c_str()),
static_cast<DWORD>(regValue.size() + 1));
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
static bool windows_disable(const std::string& appName) {
HKEY hKey = nullptr;
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
result = RegDeleteValueA(hKey, appName.c_str());
RegCloseKey(hKey);
// Return true even if the value doesn't exist (already disabled)
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
}
static bool windows_exists(const std::string& appName) {
HKEY hKey = nullptr;
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_READ, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
result = RegQueryValueExA(hKey, appName.c_str(), nullptr, nullptr, nullptr,
nullptr);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
#endif
// Linux
#if defined(__linux__)
static std::string linux_desktop_path(const std::string& appName) {
std::string home = get_home_dir();
if (home.empty()) {
return "";
}
return home + "/.config/autostart/" + appName + ".desktop";
}
static bool linux_enable(const std::string& appName,
const std::string& exePath) {
std::string home = get_home_dir();
if (home.empty()) {
return false;
}
std::filesystem::path dir =
std::filesystem::path(home) / ".config" / "autostart";
// Create directory if it doesn't exist
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
return false;
}
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << "[Desktop Entry]\n";
file << "Type=Application\n";
file << "Exec=" << exePath << "\n";
file << "Hidden=false\n";
file << "NoDisplay=false\n";
file << "X-GNOME-Autostart-enabled=true\n";
file << "Terminal=false\n";
file << "StartupNotify=false\n";
file << "Name=" << appName << "\n";
file.close();
return file.good();
}
static bool linux_disable(const std::string& appName) {
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::remove(path, ec) && !ec;
}
static bool linux_exists(const std::string& appName) {
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
return file_exists(path);
}
#endif
// macOS
#ifdef __APPLE__
static std::string mac_plist_path(const std::string& appName) {
std::string home = get_home_dir();
if (home.empty()) {
return "";
}
return home + "/Library/LaunchAgents/" + appName + ".plist";
}
static bool mac_enable(const std::string& appName, const std::string& exePath) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
// Ensure LaunchAgents directory exists
std::filesystem::path dir =
std::filesystem::path(get_home_dir()) / "Library" / "LaunchAgents";
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
return false;
}
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>)"
<< appName << R"(</string>
<key>ProgramArguments</key>
<array>
<string>)"
<< exePath << R"(</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>)";
file.close();
return file.good();
}
static bool mac_disable(const std::string& appName) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::remove(path, ec) && !ec;
}
static bool mac_exists(const std::string& appName) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
return file_exists(path);
}
#endif
bool EnableAutostart(const std::string& appName) {
std::string exePath = GetExecutablePath();
if (exePath.empty()) {
return false;
}
#ifdef _WIN32
return windows_enable(appName, exePath);
#elif __APPLE__
return mac_enable(appName, exePath);
#else
return linux_enable(appName, exePath);
#endif
}
bool DisableAutostart(const std::string& appName) {
#ifdef _WIN32
return windows_disable(appName);
#elif __APPLE__
return mac_disable(appName);
#else
return linux_disable(appName);
#endif
}
bool IsAutostartEnabled(const std::string& appName) {
#ifdef _WIN32
return windows_exists(appName);
#elif __APPLE__
return mac_exists(appName);
#else
return linux_exists(appName);
#endif
}
} // namespace crossdesk

21
src/autostart/autostart.h Normal file
View File

@@ -0,0 +1,21 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-18
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _AUTOSTART_H_
#define _AUTOSTART_H_
#include <string>
namespace crossdesk {
bool EnableAutostart(const std::string& appName);
bool DisableAutostart(const std::string& appName);
bool IsAutostartEnabled(const std::string& appName);
} // namespace crossdesk
#endif

View File

@@ -9,6 +9,8 @@
#include <string>
namespace crossdesk {
class DisplayInfo {
public:
DisplayInfo(std::string name, int left, int top, int right, int bottom)
@@ -40,5 +42,5 @@ class DisplayInfo {
int width = 0;
int height = 0;
};
} // namespace crossdesk
#endif

View File

@@ -1,5 +1,8 @@
#include "platform.h"
#include <cstdlib>
#include <cstring>
#include "rd_log.h"
#ifdef _WIN32
@@ -19,6 +22,8 @@
#include <unistd.h>
#endif
namespace crossdesk {
std::string GetMac() {
char mac_addr[16];
int len = 0;
@@ -39,21 +44,21 @@ std::string GetMac() {
#elif __APPLE__
std::string if_name = "en0";
struct ifaddrs *addrs;
struct ifaddrs *cursor;
const struct sockaddr_dl *dlAddr;
struct ifaddrs* addrs;
struct ifaddrs* cursor;
const struct sockaddr_dl* dlAddr;
if (!getifaddrs(&addrs)) {
cursor = addrs;
while (cursor != 0) {
const struct sockaddr_dl *socAddr =
(const struct sockaddr_dl *)cursor->ifa_addr;
const struct sockaddr_dl* socAddr =
(const struct sockaddr_dl*)cursor->ifa_addr;
if ((cursor->ifa_addr->sa_family == AF_LINK) &&
(socAddr->sdl_type == IFT_ETHER) &&
strcmp(if_name.c_str(), cursor->ifa_name) == 0) {
dlAddr = (const struct sockaddr_dl *)cursor->ifa_addr;
const unsigned char *base =
(const unsigned char *)&dlAddr->sdl_data[dlAddr->sdl_nlen];
dlAddr = (const struct sockaddr_dl*)cursor->ifa_addr;
const unsigned char* base =
(const unsigned char*)&dlAddr->sdl_data[dlAddr->sdl_nlen];
for (int i = 0; i < dlAddr->sdl_alen; i++) {
len +=
snprintf(mac_addr + len, sizeof(mac_addr) - len, "%.2X", base[i]);
@@ -77,8 +82,8 @@ std::string GetMac() {
close(sock);
return "";
}
struct ifreq *it = ifc.ifc_req;
const struct ifreq *const end = it + (ifc.ifc_len / sizeof(struct ifreq));
struct ifreq* it = ifc.ifc_req;
const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq));
for (; it != end; ++it) {
std::strcpy(ifr.ifr_name, it->ifr_name);
if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) {
@@ -106,7 +111,7 @@ std::string GetHostName() {
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
LOG_ERROR("WSAStartup failed");
return "";
}
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
@@ -122,4 +127,26 @@ std::string GetHostName() {
}
#endif
return hostname;
}
}
bool IsWaylandSession() {
#if defined(__linux__) && !defined(__APPLE__)
const char* session_type = std::getenv("XDG_SESSION_TYPE");
if (session_type) {
if (std::strcmp(session_type, "wayland") == 0 ||
std::strcmp(session_type, "Wayland") == 0) {
return true;
}
if (std::strcmp(session_type, "x11") == 0 ||
std::strcmp(session_type, "X11") == 0) {
return false;
}
}
const char* wayland_display = std::getenv("WAYLAND_DISPLAY");
return wayland_display && wayland_display[0] != '\0';
#else
return false;
#endif
}
} // namespace crossdesk

View File

@@ -9,7 +9,11 @@
#include <iostream>
namespace crossdesk {
std::string GetMac();
std::string GetHostName();
bool IsWaylandSession();
#endif
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,63 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk

View File

@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,279 @@
#include "wayland_portal_shared.h"
#include <chrono>
#include <mutex>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <dbus/dbus.h>
#endif
#include "rd_log.h"
namespace crossdesk {
namespace {
std::mutex& SharedSessionMutex() {
static std::mutex mutex;
return mutex;
}
SharedWaylandPortalSessionInfo& SharedSessionInfo() {
static SharedWaylandPortalSessionInfo info;
return info;
}
bool& SharedSessionActive() {
static bool active = false;
return active;
}
int& SharedSessionRefs() {
static int refs = 0;
return refs;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalSessionInterface =
"org.freedesktop.portal.Session";
constexpr int kPortalCloseWaitMs = 100;
void LogCloseDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
struct SessionClosedState {
std::string session_handle;
bool received = false;
};
DBusHandlerResult HandleSessionClosedSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
(void)connection;
auto* state = static_cast<SessionClosedState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalSessionInterface, "Closed")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->session_handle != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
bool BeginSessionClosedWatch(DBusConnection* connection,
const std::string& session_handle,
SessionClosedState* state,
std::string* match_rule_out) {
if (!connection || session_handle.empty() || !state || !match_rule_out) {
return false;
}
state->session_handle = session_handle;
state->received = false;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalSessionInterface) +
"',member='Closed',path='" + session_handle + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogCloseDbusError("dbus_bus_add_match(Session.Closed)", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_add_filter(connection, HandleSessionClosedSignal, state,
nullptr);
*match_rule_out = match_rule;
return true;
}
void EndSessionClosedWatch(DBusConnection* connection, SessionClosedState* state,
const std::string& match_rule) {
if (!connection || !state || match_rule.empty()) {
return;
}
dbus_connection_remove_filter(connection, HandleSessionClosedSignal, state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
}
void WaitForSessionClosed(DBusConnection* connection, SessionClosedState* state,
int timeout_ms = kPortalCloseWaitMs) {
if (!connection || !state) {
return;
}
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!state->received && std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
}
#endif
} // namespace
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info) {
if (!info.connection || info.session_handle.empty() || info.stream_id == 0) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (SharedSessionActive()) {
const auto& active_info = SharedSessionInfo();
if (active_info.session_handle != info.session_handle &&
SharedSessionRefs() > 0) {
return false;
}
}
const bool same_session =
SharedSessionActive() &&
SharedSessionInfo().session_handle == info.session_handle;
SharedSessionInfo() = info;
SharedSessionActive() = true;
if (!same_session || SharedSessionRefs() <= 0) {
SharedSessionRefs() = 1;
}
return true;
}
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out) {
if (!out) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
const auto& info = SharedSessionInfo();
if (require_pointer && !info.pointer_granted) {
return false;
}
++SharedSessionRefs();
*out = info;
return true;
}
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out) {
if (connection_out) {
*connection_out = nullptr;
}
if (session_handle_out) {
session_handle_out->clear();
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
if (SharedSessionRefs() > 0) {
--SharedSessionRefs();
}
if (SharedSessionRefs() > 0) {
return true;
}
if (connection_out) {
*connection_out = SharedSessionInfo().connection;
}
if (session_handle_out) {
*session_handle_out = SharedSessionInfo().session_handle;
}
SharedSessionInfo() = SharedWaylandPortalSessionInfo{};
SharedSessionActive() = false;
SharedSessionRefs() = 0;
return true;
}
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!connection) {
return;
}
if (!session_handle.empty()) {
SessionClosedState close_state;
std::string close_match_rule;
const bool watching_closed = BeginSessionClosedWatch(
connection, session_handle, &close_state, &close_match_rule);
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, session_handle.c_str(), kPortalSessionInterface,
"Close");
if (message) {
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
connection, message, 1000, &error);
if (!reply && dbus_error_is_set(&error)) {
LogCloseDbusError(close_action, &error);
dbus_error_free(&error);
}
if (reply) {
dbus_message_unref(reply);
}
dbus_message_unref(message);
}
if (watching_closed) {
WaitForSessionClosed(connection, &close_state);
if (!close_state.received) {
LOG_WARN("Timed out waiting for portal session to close: {}",
session_handle);
LOG_WARN("Forcing local teardown without waiting for Session.Closed: {}",
session_handle);
EndSessionClosedWatch(connection, &close_state, close_match_rule);
} else {
EndSessionClosedWatch(connection, &close_state, close_match_rule);
LOG_INFO("Portal session closed: {}", session_handle);
}
}
}
dbus_connection_close(connection);
dbus_connection_unref(connection);
#else
(void)connection;
(void)session_handle;
(void)close_action;
#endif
}
} // namespace crossdesk

View File

@@ -0,0 +1,37 @@
/*
* Shared Wayland portal session state used by the Linux Wayland capturer and
* mouse controller so they can reuse one RemoteDesktop session.
*/
#ifndef _WAYLAND_PORTAL_SHARED_H_
#define _WAYLAND_PORTAL_SHARED_H_
#include <cstdint>
#include <string>
struct DBusConnection;
namespace crossdesk {
struct SharedWaylandPortalSessionInfo {
DBusConnection* connection = nullptr;
std::string session_handle;
uint32_t stream_id = 0;
int width = 0;
int height = 0;
bool pointer_granted = false;
};
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info);
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out);
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out);
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,25 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-01-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_UTIL_MAC_H_
#define _WINDOW_UTIL_MAC_H_
struct SDL_Window;
namespace crossdesk {
// Best-effort: keep an SDL window above normal windows on macOS.
// No-op on non-macOS builds.
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
// Best-effort: exclude an SDL window from the Window menu and window cycling.
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
// affects the Window menu and Cmd-` window cycling.
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,64 @@
#include "window_util_mac.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
namespace crossdesk {
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
if (!window) {
return nil;
}
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
return nil;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
void* cocoa_window_ptr =
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
if (!cocoa_window_ptr) {
return nil;
}
return (__bridge NSWindow*)cocoa_window_ptr;
#endif
}
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)always_on_top;
return;
}
// Keep above normal windows.
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
[ns_window setLevel:level];
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
[ns_window setCollectionBehavior:behavior];
}
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)excluded;
return;
}
[ns_window setExcludedFromWindowsMenu:excluded];
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
behavior |= NSWindowCollectionBehaviorTransient;
[ns_window setCollectionBehavior:behavior];
}
} // namespace crossdesk
#endif // __APPLE__

View File

@@ -1,10 +1,12 @@
#include "config_center.h"
ConfigCenter::ConfigCenter(const std::string& config_path,
const std::string& cert_file_path)
: config_path_(config_path),
cert_file_path_(cert_file_path),
cert_file_path_default_(cert_file_path) {
#include "autostart.h"
#include "rd_log.h"
namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
Load();
}
@@ -18,8 +20,14 @@ int ConfigCenter::Load() {
return -1;
}
language_ = static_cast<LANGUAGE>(
ini_.GetLongValue(section_, "language", static_cast<long>(language_)));
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
@@ -36,17 +44,51 @@ int ConfigCenter::Load() {
enable_turn_ = ini_.GetBoolValue(section_, "enable_turn", enable_turn_);
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
server_host_ = ini_.GetValue(section_, "server_host", server_host_.c_str());
server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "server_port", server_port_));
cert_file_path_ =
ini_.GetValue(section_, "cert_file_path", cert_file_path_.c_str());
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
} else {
signal_server_host_ = "";
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
} else {
signal_server_port_ = 0;
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
} else {
coturn_server_port_ = 0;
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
} else {
file_transfer_save_path_ = "";
}
return 0;
}
@@ -61,13 +103,25 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
ini_.SetBoolValue(section_, "enable_turn", enable_turn_);
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
ini_.SetValue(section_, "server_host", server_host_.c_str());
ini_.SetLongValue(section_, "server_port", static_cast<long>(server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// only save when self hosted
if (enable_self_hosted_) {
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -152,9 +206,9 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
return 0;
}
int ConfigCenter::SetServerHost(const std::string& server_host) {
server_host_ = server_host;
ini_.SetValue(section_, "server_host", server_host_.c_str());
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -162,9 +216,10 @@ int ConfigCenter::SetServerHost(const std::string& server_host) {
return 0;
}
int ConfigCenter::SetServerPort(int server_port) {
server_port_ = server_port;
ini_.SetLongValue(section_, "server_port", static_cast<long>(server_port_));
int ConfigCenter::SetServerPort(int signal_server_port) {
signal_server_port_ = signal_server_port;
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -172,9 +227,10 @@ int ConfigCenter::SetServerPort(int server_port) {
return 0;
}
int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
cert_file_path_ = cert_file_path;
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
coturn_server_port_ = coturn_server_port;
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -184,6 +240,38 @@ int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// load from config if self hosted is enabled
if (enable_self_hosted_) {
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "signal_server_port", 0));
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -193,6 +281,47 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
enable_minimize_to_tray_ = enable_minimize_to_tray;
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
if (enable_autostart) {
success = EnableAutostart("CrossDesk");
} else {
success = DisableAutostart("CrossDesk");
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
if (!success) {
LOG_ERROR("SetAutostart failed");
return -1;
}
return 0;
}
int ConfigCenter::SetDaemon(bool enable_daemon) {
enable_daemon_ = enable_daemon;
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
@@ -220,22 +349,46 @@ bool ConfigCenter::IsEnableTurn() const { return enable_turn_; }
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
std::string ConfigCenter::GetServerHost() const { return server_host_; }
int ConfigCenter::GetServerPort() const { return server_port_; }
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
std::string ConfigCenter::GetDefaultServerHost() const {
return server_host_default_;
std::string ConfigCenter::GetSignalServerHost() const {
return signal_server_host_;
}
int ConfigCenter::GetDefaultServerPort() const { return server_port_default_; }
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
std::string ConfigCenter::GetDefaultCertFilePath() const {
return cert_file_path_default_;
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
int ConfigCenter::GetDefaultSignalServerPort() const {
return server_port_default_;
}
int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
return file_transfer_save_path_;
}
} // namespace crossdesk

View File

@@ -11,17 +11,17 @@
#include "SimpleIni.h"
namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
public:
explicit ConfigCenter(
const std::string& config_path = "config.ini",
const std::string& cert_file_path = "crossdesk.cn_root.crt");
explicit ConfigCenter(const std::string& config_path = "config.ini");
~ConfigCenter();
// write config
@@ -32,11 +32,14 @@ class ConfigCenter {
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp);
int SetServerHost(const std::string& server_host);
int SetServerPort(int server_port);
int SetCertFilePath(const std::string& cert_file_path);
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetFileTransferSavePath(const std::string& path);
// read config
@@ -47,38 +50,44 @@ class ConfigCenter {
bool IsHardwareVideoCodec() const;
bool IsEnableTurn() const;
bool IsEnableSrtp() const;
std::string GetServerHost() const;
int GetServerPort() const;
std::string GetCertFilePath() const;
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetDefaultServerHost() const;
int GetDefaultServerPort() const;
std::string GetDefaultCertFilePath() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
std::string GetFileTransferSavePath() const;
int Load();
int Save();
private:
std::string config_path_;
std::string cert_file_path_;
CSimpleIniA ini_;
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_30;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
bool enable_turn_ = false;
bool enable_turn_ = true;
bool enable_srtp_ = false;
std::string server_host_ = "api.crossdesk.cn";
int server_port_ = 9099;
std::string server_host_default_ = "api.crossdesk.cn";
std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn";
int signal_server_port_ = 0;
int server_port_default_ = 9099;
std::string cert_file_path_default_ = "";
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
std::string file_transfer_save_path_ = "";
};
#endif
} // namespace crossdesk
#endif

View File

@@ -9,7 +9,13 @@
#include <stdio.h>
#include <nlohmann/json.hpp>
#include <string>
#include "display_info.h"
using json = nlohmann::json;
namespace crossdesk {
typedef enum {
mouse = 0,
@@ -53,7 +59,7 @@ typedef struct {
int* bottom;
} HostInfo;
typedef struct {
struct RemoteAction {
ControlType type;
union {
Mouse m;
@@ -62,7 +68,111 @@ typedef struct {
bool a;
int d;
};
} RemoteAction;
// parse
std::string to_json() const { return ToJson(*this); }
bool from_json(const std::string& json_str) {
RemoteAction temp;
if (!FromJson(json_str, temp)) return false;
*this = temp;
return true;
}
static std::string ToJson(const RemoteAction& a) {
json j;
j["type"] = a.type;
switch (a.type) {
case ControlType::mouse:
j["mouse"] = {
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
break;
case ControlType::keyboard:
j["keyboard"] = {{"key_value", a.k.key_value}, {"flag", a.k.flag}};
break;
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
case ControlType::display_id:
j["display_id"] = a.d;
break;
case ControlType::host_infomation: {
json displays = json::array();
for (size_t idx = 0; idx < a.i.display_num; idx++) {
displays.push_back(
{{"name", a.i.display_list ? a.i.display_list[idx] : ""},
{"left", a.i.left ? a.i.left[idx] : 0},
{"top", a.i.top ? a.i.top[idx] : 0},
{"right", a.i.right ? a.i.right[idx] : 0},
{"bottom", a.i.bottom ? a.i.bottom[idx] : 0}});
}
j["host_info"] = {{"host_name", a.i.host_name},
{"display_num", a.i.display_num},
{"displays", displays}};
break;
}
}
return j.dump();
}
static bool FromJson(const std::string& json_str, RemoteAction& out) {
try {
json j = json::parse(json_str);
out.type = (ControlType)j.at("type").get<int>();
switch (out.type) {
case ControlType::mouse:
out.m.x = j.at("mouse").at("x").get<float>();
out.m.y = j.at("mouse").at("y").get<float>();
out.m.s = j.at("mouse").at("s").get<int>();
out.m.flag = (MouseFlag)j.at("mouse").at("flag").get<int>();
break;
case ControlType::keyboard:
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break;
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
out.d = j.at("display_id").get<int>();
break;
case ControlType::host_infomation: {
std::string host_name =
j.at("host_info").at("host_name").get<std::string>();
strncpy(out.i.host_name, host_name.c_str(), sizeof(out.i.host_name));
out.i.host_name[sizeof(out.i.host_name) - 1] = '\0';
out.i.host_name_size = host_name.size();
out.i.display_num = j.at("host_info").at("display_num").get<size_t>();
auto displays = j.at("host_info").at("displays");
out.i.display_list =
(char**)malloc(out.i.display_num * sizeof(char*));
out.i.left = (int*)malloc(out.i.display_num * sizeof(int));
out.i.top = (int*)malloc(out.i.display_num * sizeof(int));
out.i.right = (int*)malloc(out.i.display_num * sizeof(int));
out.i.bottom = (int*)malloc(out.i.display_num * sizeof(int));
for (size_t idx = 0; idx < out.i.display_num; idx++) {
std::string name = displays[idx].at("name").get<std::string>();
out.i.display_list[idx] = (char*)malloc(name.size() + 1);
strcpy(out.i.display_list[idx], name.c_str());
out.i.left[idx] = displays[idx].at("left").get<int>();
out.i.top[idx] = displays[idx].at("top").get<int>();
out.i.right[idx] = displays[idx].at("right").get<int>();
out.i.bottom[idx] = displays[idx].at("bottom").get<int>();
}
break;
}
}
return true;
} catch (const std::exception& e) {
printf("Failed to parse RemoteAction JSON: %s\n", e.what());
return false;
}
}
};
// int key_code, bool is_down
typedef void (*OnKeyAction)(int, bool, void*);
@@ -79,5 +189,5 @@ class DeviceController {
// virtual int Hook();
// virtual int Unhook();
};
} // namespace crossdesk
#endif

View File

@@ -11,6 +11,8 @@
#include "keyboard_capturer.h"
#include "mouse_controller.h"
namespace crossdesk {
class DeviceControllerFactory {
public:
enum Device { Mouse = 0, Keyboard };
@@ -30,5 +32,5 @@ class DeviceControllerFactory {
}
}
};
} // namespace crossdesk
#endif

View File

@@ -1,15 +1,39 @@
#include "keyboard_capturer.h"
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h"
#include "platform.h"
#include "rd_log.h"
namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static KeySym NormalizeKeySym(KeySym key_sym) {
if (key_sym >= XK_a && key_sym <= XK_z) {
return key_sym - XK_a + XK_A;
}
return key_sym;
}
static int KeyboardEventHandler(Display* display, XEvent* event) {
(void)display;
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0);
int key_code = XKeysymToKeycode(display, keySym);
KeySym key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 0));
auto key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
if (key_it == x11KeySymToVkCode.end()) {
key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 1));
key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
}
if (key_it == x11KeySymToVkCode.end()) {
return 0;
}
int key_code = key_it->second;
bool is_key_down = (event->xkey.type == KeyPress);
if (g_on_key_action) {
@@ -19,7 +43,14 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
return 0;
}
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
KeyboardCapturer::KeyboardCapturer()
: display_(nullptr),
root_(0),
running_(false),
use_wayland_portal_(false),
wayland_init_attempted_(false),
dbus_connection_(nullptr) {
XInitThreads();
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Failed to open X display.");
@@ -27,33 +58,111 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
}
KeyboardCapturer::~KeyboardCapturer() {
Unhook();
CleanupWaylandPortal();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
}
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
}
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
XSelectInput(display_, DefaultRootWindow(display_),
KeyPressMask | KeyReleaseMask);
while (running_) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
if (running_) {
return 0;
}
root_ = DefaultRootWindow(display_);
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
XFlush(display_);
running_ = true;
const int x11_fd = ConnectionNumber(display_);
event_thread_ = std::thread([this, x11_fd]() {
while (running_) {
while (running_ && XPending(display_) > 0) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
if (!running_) {
break;
}
struct pollfd pfd = {x11_fd, POLLIN, 0};
int poll_ret = poll(&pfd, 1, 50);
if (poll_ret < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERROR("poll for X11 events failed.");
running_ = false;
break;
}
if (poll_ret == 0) {
continue;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
LOG_ERROR("poll got invalid X11 event fd state.");
running_ = false;
break;
}
if ((pfd.revents & POLLIN) == 0) {
continue;
}
}
});
return 0;
}
int KeyboardCapturer::Unhook() {
running_ = false;
if (event_thread_.joinable()) {
event_thread_.join();
}
g_on_key_action = nullptr;
g_user_ptr = nullptr;
if (display_ && root_ != 0) {
XSelectInput(display_, root_, 0);
XFlush(display_);
}
return 0;
}
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
if (IsWaylandSession()) {
if (!use_wayland_portal_ && !wayland_init_attempted_) {
wayland_init_attempted_ = true;
if (InitWaylandPortal()) {
use_wayland_portal_ = true;
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
} else {
LOG_WARN("Wayland keyboard control init failed, falling back to X11/XTest backend");
}
}
if (use_wayland_portal_) {
return SendWaylandKeyboardCommand(key_code, is_down);
}
}
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
@@ -67,3 +176,4 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
}
return 0;
}
} // namespace crossdesk

View File

@@ -11,22 +11,48 @@
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <thread>
#include "device_controller.h"
struct DBusConnection;
struct DBusMessageIter;
namespace crossdesk {
class KeyboardCapturer : public DeviceController {
public:
KeyboardCapturer();
virtual ~KeyboardCapturer();
public:
virtual int Hook(OnKeyAction on_key_action, void *user_ptr);
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
private:
Display *display_;
Window root_;
bool running_;
};
bool InitWaylandPortal();
void CleanupWaylandPortal();
int SendWaylandKeyboardCommand(int key_code, bool is_down);
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
bool SendWaylandPortalVoidCall(const char* method_name,
const std::function<void(DBusMessageIter*)>&
append_args);
#endif
private:
Display* display_;
Window root_;
std::atomic<bool> running_;
std::thread event_thread_;
bool use_wayland_portal_ = false;
bool wayland_init_attempted_ = false;
DBusConnection* dbus_connection_ = nullptr;
std::string wayland_session_handle_;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,711 @@
#include "keyboard_capturer.h"
#include <chrono>
#include <cstring>
#include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <dbus/dbus.h>
#endif
#include "rd_log.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
extern std::map<int, int> vkCodeToX11KeySym;
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
namespace {
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop";
constexpr const char* kPortalRemoteDesktopInterface =
"org.freedesktop.portal.RemoteDesktop";
constexpr const char* kPortalRequestInterface =
"org.freedesktop.portal.Request";
constexpr const char* kPortalRequestPathPrefix =
"/org/freedesktop/portal/desktop/request/";
constexpr const char* kPortalSessionPathPrefix =
"/org/freedesktop/portal/desktop/session/";
constexpr uint32_t kRemoteDesktopDeviceKeyboard = 1u;
constexpr uint32_t kKeyboardReleased = 0u;
constexpr uint32_t kKeyboardPressed = 1u;
int NormalizeFallbackKeysym(int keysym) {
if (keysym >= XK_A && keysym <= XK_Z) {
return keysym - XK_A + XK_a;
}
return keysym;
}
std::string MakeToken(const char* prefix) {
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
return std::string(prefix) + "_" + std::to_string(now);
}
void LogDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
void AppendDictEntryString(DBusMessageIter* dict, const char* key,
const std::string& value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
const char* value_cstr = value.c_str();
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryUint32(DBusMessageIter* dict, const char* key,
uint32_t value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendEmptyOptionsDict(DBusMessageIter* iter) {
DBusMessageIter options;
dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &options);
dbus_message_iter_close_container(iter, &options);
}
bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) {
if (!variant || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(variant);
if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) {
const char* temp = nullptr;
dbus_message_iter_get_basic(variant, &temp);
if (temp && temp[0] != '\0') {
*value = temp;
return true;
}
}
return false;
}
bool ReadUint32Like(DBusMessageIter* iter, uint32_t* value) {
if (!iter || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(iter);
if (type == DBUS_TYPE_UINT32) {
uint32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = temp;
return true;
}
if (type == DBUS_TYPE_INT32) {
int32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
if (temp < 0) {
return false;
}
*value = static_cast<uint32_t>(temp);
return true;
}
return false;
}
std::string BuildSessionHandleFromRequestPath(
const std::string& request_path, const std::string& session_handle_token) {
if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 ||
session_handle_token.empty()) {
return "";
}
const size_t sender_start = strlen(kPortalRequestPathPrefix);
const size_t token_sep = request_path.find('/', sender_start);
if (token_sep == std::string::npos || token_sep <= sender_start) {
return "";
}
const std::string sender =
request_path.substr(sender_start, token_sep - sender_start);
if (sender.empty()) {
return "";
}
return std::string(kPortalSessionPathPrefix) + sender + "/" +
session_handle_token;
}
struct PortalResponseState {
std::string request_path;
bool received = false;
DBusMessage* message = nullptr;
};
DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
(void)connection;
auto* state = static_cast<PortalResponseState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->request_path != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (state->message) {
dbus_message_unref(state->message);
state->message = nullptr;
}
state->message = dbus_message_ref(message);
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
DBusMessage* WaitForPortalResponse(DBusConnection* connection,
const std::string& request_path,
int timeout_ms = 120000) {
if (!connection || request_path.empty()) {
return nullptr;
}
PortalResponseState state;
state.request_path = request_path;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalRequestInterface) +
"',member='Response',path='" + request_path + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_add_match", &error);
dbus_error_free(&error);
return nullptr;
}
dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state,
nullptr);
auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!state.received && std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
return state.message;
}
bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) {
if (!reply || !request_path) {
return false;
}
const char* path = nullptr;
DBusError error;
dbus_error_init(&error);
const dbus_bool_t ok = dbus_message_get_args(
reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
if (!ok || !path) {
LogDbusError("dbus_message_get_args(request_path)", &error);
dbus_error_free(&error);
return false;
}
*request_path = path;
return true;
}
bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code,
DBusMessageIter* results_array) {
if (!message || !response_code || !results_array) {
return false;
}
DBusMessageIter iter;
if (!dbus_message_iter_init(message, &iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
return false;
}
dbus_message_iter_get_basic(&iter, response_code);
if (!dbus_message_iter_next(&iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
return false;
}
*results_array = iter;
return true;
}
bool SendPortalRequestAndHandleResponse(
DBusConnection* connection, const char* interface_name,
const char* method_name, const char* action_name,
const std::function<bool(DBusMessage*)>& append_message_args,
const std::function<bool(uint32_t, DBusMessageIter*)>& handle_results,
std::string* request_path_out = nullptr) {
if (!connection || !interface_name || interface_name[0] == '\0' ||
!method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, interface_name, method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
if (append_message_args && !append_message_args(message)) {
dbus_message_unref(message);
LOG_ERROR("{} arguments are malformed", method_name);
return false;
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(connection, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(action_name ? action_name : method_name, &error);
dbus_error_free(&error);
return false;
}
std::string request_path;
const bool got_request_path = ExtractRequestPath(reply, &request_path);
dbus_message_unref(reply);
if (!got_request_path) {
return false;
}
if (request_path_out) {
*request_path_out = request_path;
}
DBusMessage* response = WaitForPortalResponse(connection, request_path);
if (!response) {
LOG_ERROR("Timed out waiting for {} response", method_name);
return false;
}
uint32_t response_code = 1;
DBusMessageIter results;
const bool parsed = ExtractPortalResponse(response, &response_code, &results);
if (!parsed) {
dbus_message_unref(response);
LOG_ERROR("{} response was malformed", method_name);
return false;
}
const bool ok = handle_results ? handle_results(response_code, &results)
: (response_code == 0);
dbus_message_unref(response);
return ok;
}
} // namespace
#endif
bool KeyboardCapturer::InitWaylandPortal() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
CleanupWaylandPortal();
DBusError error;
dbus_error_init(&error);
DBusConnection* check_connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
if (!check_connection) {
LogDbusError("dbus_bus_get", &error);
dbus_error_free(&error);
return false;
}
const dbus_bool_t has_owner =
dbus_bus_name_has_owner(check_connection, kPortalBusName, &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_name_has_owner", &error);
dbus_error_free(&error);
dbus_connection_unref(check_connection);
return false;
}
dbus_connection_unref(check_connection);
if (!has_owner) {
LOG_ERROR("xdg-desktop-portal is not available on session bus");
return false;
}
dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
if (!dbus_connection_) {
LogDbusError("dbus_bus_get_private", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE);
const std::string session_handle_token =
MakeToken("crossdesk_keyboard_session");
std::string request_path;
const bool create_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession",
"CreateSession",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "session_handle_token",
session_handle_token);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.CreateSession denied, response={}",
response_code);
return false;
}
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT &&
strcmp(key, "session_handle") == 0) {
DBusMessageIter variant;
std::string parsed_handle;
dbus_message_iter_recurse(&entry, &variant);
if (ReadPathLikeVariant(&variant, &parsed_handle) &&
!parsed_handle.empty()) {
wayland_session_handle_ = parsed_handle;
break;
}
}
}
dbus_message_iter_next(&dict);
}
return true;
},
&request_path);
if (!create_ok) {
CleanupWaylandPortal();
return false;
}
if (wayland_session_handle_.empty()) {
wayland_session_handle_ =
BuildSessionHandleFromRequestPath(request_path, session_handle_token);
}
if (wayland_session_handle_.empty()) {
LOG_ERROR("RemoteDesktop.CreateSession did not return session handle");
CleanupWaylandPortal();
return false;
}
const char* session_handle = wayland_session_handle_.c_str();
const bool select_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices",
"SelectDevices",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kRemoteDesktopDeviceKeyboard);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.SelectDevices denied, response={}",
response_code);
return false;
}
return true;
});
if (!select_ok) {
CleanupWaylandPortal();
return false;
}
const char* parent_window = "";
bool keyboard_granted = false;
const bool start_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.Start denied, response={}", response_code);
return false;
}
uint32_t granted_devices = 0;
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&entry, &variant);
if (strcmp(key, "devices") == 0) {
ReadUint32Like(&variant, &granted_devices);
}
}
}
dbus_message_iter_next(&dict);
}
keyboard_granted =
(granted_devices & kRemoteDesktopDeviceKeyboard) != 0;
if (!keyboard_granted) {
LOG_ERROR(
"RemoteDesktop.Start granted devices mask={}, keyboard not allowed",
granted_devices);
return false;
}
return true;
});
if (!start_ok) {
CleanupWaylandPortal();
return false;
}
if (!keyboard_granted) {
LOG_ERROR("RemoteDesktop session started without keyboard permission");
CleanupWaylandPortal();
return false;
}
return true;
#else
return false;
#endif
}
void KeyboardCapturer::CleanupWaylandPortal() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (dbus_connection_) {
CloseWaylandPortalSessionAndConnection(dbus_connection_,
wayland_session_handle_,
"RemoteDesktop.Session.Close");
dbus_connection_ = nullptr;
}
#endif
use_wayland_portal_ = false;
wayland_session_handle_.clear();
}
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!dbus_connection_ || wayland_session_handle_.empty()) {
return -1;
}
const auto key_it = vkCodeToX11KeySym.find(key_code);
if (key_it == vkCodeToX11KeySym.end()) {
return 0;
}
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
const int keysym = key_it->second;
// Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms.
if (display_) {
const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
if (x11_keycode > 8) {
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
return 0;
}
}
}
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
return 0;
}
LOG_ERROR("Failed to send Wayland keyboard event, vk_code={}, is_down={}",
key_code, is_down);
return -3;
#else
(void)key_code;
(void)is_down;
return -1;
#endif
}
bool KeyboardCapturer::NotifyWaylandKeyboardKeysym(int keysym, uint32_t state) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
return SendWaylandPortalVoidCall(
"NotifyKeyboardKeysym", [&](DBusMessageIter* iter) {
const char* session_handle = wayland_session_handle_.c_str();
int32_t key_sym = keysym;
uint32_t key_state = state;
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
AppendEmptyOptionsDict(iter);
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_sym);
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
});
#else
(void)keysym;
(void)state;
return false;
#endif
}
bool KeyboardCapturer::NotifyWaylandKeyboardKeycode(int keycode,
uint32_t state) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
return SendWaylandPortalVoidCall(
"NotifyKeyboardKeycode", [&](DBusMessageIter* iter) {
const char* session_handle = wayland_session_handle_.c_str();
int32_t key_code = keycode;
uint32_t key_state = state;
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
AppendEmptyOptionsDict(iter);
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_code);
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
});
#else
(void)keycode;
(void)state;
return false;
#endif
}
bool KeyboardCapturer::SendWaylandPortalVoidCall(
const char* method_name,
const std::function<void(DBusMessageIter*)>& append_args) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!dbus_connection_ || !method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, kPortalRemoteDesktopInterface,
method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
DBusMessageIter iter;
dbus_message_iter_init_append(message, &iter);
if (append_args) {
append_args(&iter);
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
dbus_connection_, message, 5000, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(method_name, &error);
dbus_error_free(&error);
return false;
}
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
const char* error_name = dbus_message_get_error_name(reply);
LOG_ERROR("{} returned DBus error: {}", method_name,
error_name ? error_name : "unknown");
dbus_message_unref(reply);
return false;
}
dbus_message_unref(reply);
return true;
#else
(void)method_name;
(void)append_args;
return false;
#endif
}
} // namespace crossdesk

View File

@@ -1,97 +1,186 @@
#include "keyboard_capturer.h"
#include <unordered_map>
#include "keyboard_converter.h"
#include "rd_log.h"
namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void *g_user_ptr = nullptr;
static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') {
return static_cast<int>(ch - 'a' + 'A');
}
if (ch >= 'A' && ch <= 'Z') {
return static_cast<int>(ch);
}
if (ch >= '0' && ch <= '9') {
return static_cast<int>(ch);
}
switch (ch) {
case ' ':
return 0x20; // VK_SPACE
case '-':
case '_':
return 0xBD; // VK_OEM_MINUS
case '=':
case '+':
return 0xBB; // VK_OEM_PLUS
case '[':
case '{':
return 0xDB; // VK_OEM_4
case ']':
case '}':
return 0xDD; // VK_OEM_6
case '\\':
case '|':
return 0xDC; // VK_OEM_5
case ';':
case ':':
return 0xBA; // VK_OEM_1
case '\'':
case '"':
return 0xDE; // VK_OEM_7
case ',':
case '<':
return 0xBC; // VK_OEM_COMMA
case '.':
case '>':
return 0xBE; // VK_OEM_PERIOD
case '/':
case '?':
return 0xBF; // VK_OEM_2
case '`':
case '~':
return 0xC0; // VK_OEM_3
default:
return -1;
}
}
static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
bool is_key_down) {
auto key_it = CGKeyCodeToVkCode.find(key_code);
if (key_it != CGKeyCodeToVkCode.end()) {
if (is_key_down) {
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
}
return key_it->second;
}
int vk_code = -1;
UniChar chars[4] = {0};
UniCharCount char_count = 0;
CGEventKeyboardGetUnicodeString(event, 4, &char_count, chars);
if (char_count > 0) {
vk_code = VkCodeFromUnicode(chars[0]);
}
if (vk_code < 0) {
auto fallback_it =
g_unmapped_keycode_to_vk.find(static_cast<int>(key_code));
if (fallback_it != g_unmapped_keycode_to_vk.end()) {
vk_code = fallback_it->second;
}
}
if (vk_code >= 0) {
if (is_key_down) {
g_unmapped_keycode_to_vk[static_cast<int>(key_code)] = vk_code;
} else {
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
}
}
return vk_code;
}
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void *userInfo) {
KeyboardCapturer *keyboard_capturer = (KeyboardCapturer *)userInfo;
CGEventRef event, void* userInfo) {
(void)proxy;
if (!g_on_key_action) {
return event;
}
KeyboardCapturer* keyboard_capturer = (KeyboardCapturer*)userInfo;
if (!keyboard_capturer) {
LOG_ERROR("keyboard_capturer is nullptr");
return event;
}
int vk_code = 0;
if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
const bool is_key_down = (type == kCGEventKeyDown);
CGKeyCode key_code = static_cast<CGKeyCode>(
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
if (CGKeyCodeToVkCode.find(key_code) != CGKeyCodeToVkCode.end()) {
g_on_key_action(CGKeyCodeToVkCode[key_code], type == kCGEventKeyDown,
g_user_ptr);
int vk_code = ResolveVkCodeFromMacEvent(event, key_code, is_key_down);
if (vk_code >= 0) {
g_on_key_action(vk_code, is_key_down, g_user_ptr);
}
} else if (type == kCGEventFlagsChanged) {
CGEventFlags current_flags = CGEventGetFlags(event);
CGKeyCode key_code = static_cast<CGKeyCode>(
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
auto key_it = CGKeyCodeToVkCode.find(key_code);
if (key_it == CGKeyCodeToVkCode.end()) {
return nullptr;
}
const int vk_code = key_it->second;
// caps lock
bool caps_lock_state = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
if (caps_lock_state != keyboard_capturer->caps_lock_flag_) {
keyboard_capturer->caps_lock_flag_ = caps_lock_state;
if (keyboard_capturer->caps_lock_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, g_user_ptr);
}
// shift
bool shift_state = (current_flags & kCGEventFlagMaskShift) != 0;
if (shift_state != keyboard_capturer->shift_flag_) {
keyboard_capturer->shift_flag_ = shift_state;
if (keyboard_capturer->shift_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, g_user_ptr);
}
// control
bool control_state = (current_flags & kCGEventFlagMaskControl) != 0;
if (control_state != keyboard_capturer->control_flag_) {
keyboard_capturer->control_flag_ = control_state;
if (keyboard_capturer->control_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->control_flag_, g_user_ptr);
}
// option
bool option_state = (current_flags & kCGEventFlagMaskAlternate) != 0;
if (option_state != keyboard_capturer->option_flag_) {
keyboard_capturer->option_flag_ = option_state;
if (keyboard_capturer->option_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->option_flag_, g_user_ptr);
}
// command
bool command_state = (current_flags & kCGEventFlagMaskCommand) != 0;
if (command_state != keyboard_capturer->command_flag_) {
keyboard_capturer->command_flag_ = command_state;
if (keyboard_capturer->command_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->command_flag_, g_user_ptr);
}
}
return nullptr;
}
KeyboardCapturer::KeyboardCapturer() {}
KeyboardCapturer::KeyboardCapturer()
: event_tap_(nullptr), run_loop_source_(nullptr) {}
KeyboardCapturer::~KeyboardCapturer() {}
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void *user_ptr) {
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (event_tap_) {
return 0;
}
g_unmapped_keycode_to_vk.clear();
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
@@ -109,17 +198,49 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void *user_ptr) {
run_loop_source_ =
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap_, 0);
if (!run_loop_source_) {
LOG_ERROR("CFMachPortCreateRunLoopSource failed");
CFRelease(event_tap_);
event_tap_ = nullptr;
return -1;
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source_,
kCFRunLoopCommonModes);
const CGEventFlags current_flags =
CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
caps_lock_flag_ = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
shift_flag_ = (current_flags & kCGEventFlagMaskShift) != 0;
control_flag_ = (current_flags & kCGEventFlagMaskControl) != 0;
option_flag_ = (current_flags & kCGEventFlagMaskAlternate) != 0;
command_flag_ = (current_flags & kCGEventFlagMaskCommand) != 0;
CGEventTapEnable(event_tap_, true);
return 0;
}
int KeyboardCapturer::Unhook() {
CFRelease(run_loop_source_);
CFRelease(event_tap_);
g_unmapped_keycode_to_vk.clear();
g_on_key_action = nullptr;
g_user_ptr = nullptr;
if (event_tap_) {
CGEventTapEnable(event_tap_, false);
}
if (run_loop_source_) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source_,
kCFRunLoopCommonModes);
CFRelease(run_loop_source_);
run_loop_source_ = nullptr;
}
if (event_tap_) {
CFRelease(event_tap_);
event_tap_ = nullptr;
}
return 0;
}
@@ -147,9 +268,12 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
if (vkCodeToCGKeyCode.find(key_code) != vkCodeToCGKeyCode.end()) {
CGKeyCode cg_key_code = vkCodeToCGKeyCode[key_code];
CGEventRef event = CGEventCreateKeyboardEvent(NULL, cg_key_code, is_down);
CGEventRef clearFlags =
CGEventCreateKeyboardEvent(NULL, (CGKeyCode)0, true);
CGEventSetFlags(clearFlags, 0);
if (!event) {
LOG_ERROR("CGEventCreateKeyboardEvent failed");
return -1;
}
CGEventSetFlags(event, 0);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
@@ -164,4 +288,5 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -11,19 +11,21 @@
#include "device_controller.h"
namespace crossdesk {
class KeyboardCapturer : public DeviceController {
public:
KeyboardCapturer();
virtual ~KeyboardCapturer();
public:
virtual int Hook(OnKeyAction on_key_action, void *user_ptr);
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
private:
CFMachPortRef event_tap_;
CFRunLoopSourceRef run_loop_source_;
CFMachPortRef event_tap_ = nullptr;
CFRunLoopSourceRef run_loop_source_ = nullptr;
public:
bool caps_lock_flag_ = false;
@@ -33,5 +35,5 @@ class KeyboardCapturer : public DeviceController {
bool command_flag_ = false;
int fn_key_code_ = 0x3F;
};
#endif
} // namespace crossdesk
#endif

View File

@@ -2,6 +2,8 @@
#include "rd_log.h"
namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
@@ -37,7 +39,12 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
UnhookWindowsHookEx(keyboard_hook_);
if (keyboard_hook_) {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
UnhookWindowsHookEx(keyboard_hook_);
keyboard_hook_ = nullptr;
}
return 0;
}
@@ -47,10 +54,28 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
input.type = INPUT_KEYBOARD;
input.ki.wVk = (WORD)key_code;
if (!is_down) {
input.ki.dwFlags = KEYEVENTF_KEYUP;
const UINT scan_code =
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
if (scan_code != 0) {
input.ki.wVk = 0;
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
if ((scan_code & 0xFF00) != 0) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
}
if (!is_down) {
input.ki.dwFlags |= KEYEVENTF_KEYUP;
}
UINT sent = SendInput(1, &input, sizeof(INPUT));
if (sent != 1) {
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
is_down, GetLastError());
return -1;
}
SendInput(1, &input, sizeof(INPUT));
return 0;
}
}
} // namespace crossdesk

View File

@@ -11,18 +11,21 @@
#include "device_controller.h"
namespace crossdesk {
class KeyboardCapturer : public DeviceController {
public:
KeyboardCapturer();
virtual ~KeyboardCapturer();
public:
virtual int Hook(OnKeyAction on_key_action, void *user_ptr);
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
private:
HHOOK keyboard_hook_ = nullptr;
};
} // namespace crossdesk
#endif

View File

@@ -9,6 +9,8 @@
#include <map>
namespace crossdesk {
// Windows vkCode to macOS CGKeyCode (104 keys)
std::map<int, int> vkCodeToCGKeyCode = {
// A-Z
@@ -71,13 +73,13 @@ std::map<int, int> vkCodeToCGKeyCode = {
{0x20, 0x31}, // Space
{0x08, 0x33}, // Backspace
{0x09, 0x30}, // Tab
{0x2C, 0x74}, // Print Screen
{0x2C, 0x69}, // Print Screen(F13)
{0x2D, 0x72}, // Insert
{0x2E, 0x75}, // Delete
{0x24, 0x73}, // Home
{0x23, 0x77}, // End
{0x21, 0x79}, // Page Up
{0x22, 0x7A}, // Page Down
{0x21, 0x74}, // Page Up
{0x22, 0x79}, // Page Down
// arrow keys
{0x25, 0x7B}, // Left Arrow
@@ -189,13 +191,13 @@ std::map<int, int> CGKeyCodeToVkCode = {
{0x31, 0x20}, // Space
{0x33, 0x08}, // Backspace
{0x30, 0x09}, // Tab
{0x74, 0x2C}, // Print Screen
{0x69, 0x2C}, // Print Screen(F13)
{0x72, 0x2D}, // Insert
{0x75, 0x2E}, // Delete
{0x73, 0x24}, // Home
{0x77, 0x23}, // End
{0x79, 0x21}, // Page Up
{0x7A, 0x22}, // Page Down
{0x74, 0x21}, // Page Up
{0x79, 0x22}, // Page Down
// arrow keys
{0x7B, 0x25}, // Left Arrow
@@ -324,21 +326,21 @@ std::map<int, int> vkCodeToX11KeySym = {
{0x28, 0xFF54}, // Down Arrow
// numpad
{0x60, 0x0030}, // Numpad 0
{0x61, 0x0031}, // Numpad 1
{0x62, 0x0032}, // Numpad 2
{0x63, 0x0033}, // Numpad 3
{0x64, 0x0034}, // Numpad 4
{0x65, 0x0035}, // Numpad 5
{0x66, 0x0036}, // Numpad 6
{0x67, 0x0037}, // Numpad 7
{0x68, 0x0038}, // Numpad 8
{0x69, 0x0039}, // Numpad 9
{0x6E, 0x003A}, // Numpad .
{0x6F, 0x002F}, // Numpad /
{0x6A, 0x002A}, // Numpad *
{0x6D, 0x002D}, // Numpad -
{0x6B, 0x002B}, // Numpad +
{0x60, 0xFFB0}, // Numpad 0
{0x61, 0xFFB1}, // Numpad 1
{0x62, 0xFFB2}, // Numpad 2
{0x63, 0xFFB3}, // Numpad 3
{0x64, 0xFFB4}, // Numpad 4
{0x65, 0xFFB5}, // Numpad 5
{0x66, 0xFFB6}, // Numpad 6
{0x67, 0xFFB7}, // Numpad 7
{0x68, 0xFFB8}, // Numpad 8
{0x69, 0xFFB9}, // Numpad 9
{0x6E, 0xFFAE}, // Numpad .
{0x6F, 0xFFAF}, // Numpad /
{0x6A, 0xFFAA}, // Numpad *
{0x6D, 0xFFAD}, // Numpad -
{0x6B, 0xFFAB}, // Numpad +
// symbol keys
{0xBA, 0x003B}, // ; (Semicolon)
@@ -452,21 +454,21 @@ std::map<int, int> x11KeySymToVkCode = {
{0xFF54, 0x28}, // Down Arrow
// numpad
{0x0030, 0x60}, // Numpad 0
{0x0031, 0x61}, // Numpad 1
{0x0032, 0x62}, // Numpad 2
{0x0033, 0x63}, // Numpad 3
{0x0034, 0x64}, // Numpad 4
{0x0035, 0x65}, // Numpad 5
{0x0036, 0x66}, // Numpad 6
{0x0037, 0x67}, // Numpad 7
{0x0038, 0x68}, // Numpad 8
{0x0039, 0x69}, // Numpad 9
{0x003A, 0x6E}, // Numpad .
{0x002F, 0x6F}, // Numpad /
{0x002A, 0x6A}, // Numpad *
{0x002D, 0x6D}, // Numpad -
{0x002B, 0x6B}, // Numpad +
{0xFFB0, 0x60}, // Numpad 0
{0xFFB1, 0x61}, // Numpad 1
{0xFFB2, 0x62}, // Numpad 2
{0xFFB3, 0x63}, // Numpad 3
{0xFFB4, 0x64}, // Numpad 4
{0xFFB5, 0x65}, // Numpad 5
{0xFFB6, 0x66}, // Numpad 6
{0xFFB7, 0x67}, // Numpad 7
{0xFFB8, 0x68}, // Numpad 8
{0xFFB9, 0x69}, // Numpad 9
{0xFFAE, 0x6E}, // Numpad .
{0xFFAF, 0x6F}, // Numpad /
{0xFFAA, 0x6A}, // Numpad *
{0xFFAD, 0x6D}, // Numpad -
{0xFFAB, 0x6B}, // Numpad +
// symbol keys
{0x003B, 0xBA}, // ; (Semicolon)
@@ -555,13 +557,13 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
{0x31, 0x0020}, // Space
{0x33, 0xFF08}, // Backspace
{0x30, 0xFF09}, // Tab
{0x74, 0xFF15}, // Print Screen
{0x69, 0xFF15}, // Print Screen(F13)
{0x72, 0xFF63}, // Insert
{0x75, 0xFFFF}, // Delete
{0x73, 0xFF50}, // Home
{0x77, 0xFF57}, // End
{0x79, 0xFF55}, // Page Up
{0x7A, 0xFF56}, // Page Down
{0x74, 0xFF55}, // Page Up
{0x79, 0xFF56}, // Page Down
// arrow keys
{0x7B, 0xFF51}, // Left Arrow
@@ -570,21 +572,21 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
{0x7D, 0xFF54}, // Down Arrow
// numpad
{0x52, 0x0030}, // Numpad 0
{0x53, 0x0031}, // Numpad 1
{0x54, 0x0032}, // Numpad 2
{0x55, 0x0033}, // Numpad 3
{0x56, 0x0034}, // Numpad 4
{0x57, 0x0035}, // Numpad 5
{0x58, 0x0036}, // Numpad 6
{0x59, 0x0037}, // Numpad 7
{0x5B, 0x0038}, // Numpad 8
{0x5C, 0x0039}, // Numpad 9
{0x41, 0x003A}, // Numpad .
{0x4B, 0x002F}, // Numpad /
{0x43, 0x002A}, // Numpad *
{0x4E, 0x002D}, // Numpad -
{0x45, 0x002B}, // Numpad +
{0x52, 0xFFB0}, // Numpad 0
{0x53, 0xFFB1}, // Numpad 1
{0x54, 0xFFB2}, // Numpad 2
{0x55, 0xFFB3}, // Numpad 3
{0x56, 0xFFB4}, // Numpad 4
{0x57, 0xFFB5}, // Numpad 5
{0x58, 0xFFB6}, // Numpad 6
{0x59, 0xFFB7}, // Numpad 7
{0x5B, 0xFFB8}, // Numpad 8
{0x5C, 0xFFB9}, // Numpad 9
{0x41, 0xFFAE}, // Numpad .
{0x4B, 0xFFAF}, // Numpad /
{0x43, 0xFFAA}, // Numpad *
{0x4E, 0xFFAD}, // Numpad -
{0x45, 0xFFAB}, // Numpad +
// symbol keys
{0x29, 0x003B}, // ; (Semicolon)
@@ -681,13 +683,13 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0x0020, 0x31}, // Space
{0xFF08, 0x33}, // Backspace
{0xFF09, 0x30}, // Tab
{0xFF15, 0x74}, // Print Screen
{0xFF15, 0x69}, // Print Screen(F13)
{0xFF63, 0x72}, // Insert
{0xFFFF, 0x75}, // Delete
{0xFF50, 0x73}, // Home
{0xFF57, 0x77}, // End
{0xFF55, 0x79}, // Page Up
{0xFF56, 0x7A}, // Page Down
{0xFF55, 0x74}, // Page Up
{0xFF56, 0x79}, // Page Down
// arrow keys
{0xFF51, 0x7B}, // Left Arrow
@@ -696,21 +698,21 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0xFF54, 0x7D}, // Down Arrow
// numpad
{0x0030, 0x52}, // Numpad 0
{0x0031, 0x53}, // Numpad 1
{0x0032, 0x54}, // Numpad 2
{0x0033, 0x55}, // Numpad 3
{0x0034, 0x56}, // Numpad 4
{0x0035, 0x57}, // Numpad 5
{0x0036, 0x58}, // Numpad 6
{0x0037, 0x59}, // Numpad 7
{0x0038, 0x5B}, // Numpad 8
{0x0039, 0x5C}, // Numpad 9
{0x003A, 0x41}, // Numpad .
{0x002F, 0x4B}, // Numpad /
{0x002A, 0x43}, // Numpad *
{0x002D, 0x4E}, // Numpad -
{0x002B, 0x45}, // Numpad +
{0xFFB0, 0x52}, // Numpad 0
{0xFFB1, 0x53}, // Numpad 1
{0xFFB2, 0x54}, // Numpad 2
{0xFFB3, 0x55}, // Numpad 3
{0xFFB4, 0x56}, // Numpad 4
{0xFFB5, 0x57}, // Numpad 5
{0xFFB6, 0x58}, // Numpad 6
{0xFFB7, 0x59}, // Numpad 7
{0xFFB8, 0x5B}, // Numpad 8
{0xFFB9, 0x5C}, // Numpad 9
{0xFFAE, 0x41}, // Numpad .
{0xFFAF, 0x4B}, // Numpad /
{0xFFAA, 0x43}, // Numpad *
{0xFFAD, 0x4E}, // Numpad -
{0xFFAB, 0x45}, // Numpad +
// symbol keys
{0x003B, 0x29}, // ; (Semicolon)
@@ -736,5 +738,5 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0xFFEB, 0x37}, // Left Command
{0xFFEC, 0x36}, // Right Command
};
#endif
} // namespace crossdesk
#endif

View File

@@ -2,14 +2,28 @@
#include <X11/extensions/XTest.h>
#include "platform.h"
#include "rd_log.h"
namespace crossdesk {
MouseController::MouseController() {}
MouseController::~MouseController() { Destroy(); }
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
display_info_list_ = display_info_list;
if (IsWaylandSession()) {
if (InitWaylandPortal()) {
use_wayland_portal_ = true;
LOG_INFO("Mouse controller initialized with Wayland portal backend");
return 0;
}
LOG_WARN(
"Wayland mouse control init failed, falling back to X11/XTest backend");
}
display_ = XOpenDisplay(NULL);
if (!display_) {
LOG_ERROR("Cannot connect to X server");
@@ -23,26 +37,68 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
&minor_version)) {
LOG_ERROR("XTest extension not available");
XCloseDisplay(display_);
display_ = nullptr;
return -2;
}
return 0;
}
void MouseController::UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) {
if (display_info_list.empty()) {
return;
}
display_info_list_ = display_info_list;
if (use_wayland_portal_) {
OnWaylandDisplayInfoListUpdated();
}
if (last_display_index_ < 0 ||
last_display_index_ >= static_cast<int>(display_info_list_.size())) {
last_display_index_ = -1;
last_norm_x_ = -1.0;
last_norm_y_ = -1.0;
}
}
int MouseController::Destroy() {
CleanupWaylandPortal();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
return 0;
}
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
if (remote_action.type != ControlType::mouse) {
return 0;
}
if (use_wayland_portal_) {
return SendWaylandMouseCommand(remote_action, display_index);
}
if (!display_) {
LOG_ERROR("X11 display not initialized");
return -1;
}
switch (remote_action.type) {
case mouse:
switch (remote_action.m.flag) {
case MouseFlag::move:
case MouseFlag::move: {
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid display index: {}", display_index);
return -2;
}
SetMousePosition(
static_cast<int>(remote_action.m.x *
display_info_list_[display_index].width +
@@ -51,6 +107,7 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
display_info_list_[display_index].height +
display_info_list_[display_index].top));
break;
}
case MouseFlag::left_down:
XTestFakeButtonEvent(display_, 1, True, CurrentTime);
XFlush(display_);
@@ -101,24 +158,39 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
}
void MouseController::SetMousePosition(int x, int y) {
if (!display_) {
return;
}
XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y);
XFlush(display_);
}
void MouseController::SimulateKeyDown(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, True, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateKeyUp(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, False, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateMouseWheel(int direction_button, int count) {
if (!display_) {
return;
}
for (int i = 0; i < count; ++i) {
XTestFakeButtonEvent(display_, direction_button, True, CurrentTime);
XTestFakeButtonEvent(display_, direction_button, False, CurrentTime);
}
XFlush(display_);
}
}
} // namespace crossdesk

View File

@@ -11,10 +11,18 @@
#include <X11/Xutil.h>
#include <unistd.h>
#include <functional>
#include <cstdint>
#include <string>
#include <vector>
#include "device_controller.h"
struct DBusConnection;
struct DBusMessageIter;
namespace crossdesk {
class MouseController : public DeviceController {
public:
MouseController();
@@ -24,18 +32,47 @@ class MouseController : public DeviceController {
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(const std::vector<DisplayInfo>& display_info_list);
private:
void SimulateKeyDown(int kval);
void SimulateKeyUp(int kval);
void SetMousePosition(int x, int y);
void SimulateMouseWheel(int direction_button, int count);
bool InitWaylandPortal();
void CleanupWaylandPortal();
int SendWaylandMouseCommand(RemoteAction remote_action, int display_index);
void OnWaylandDisplayInfoListUpdated();
bool NotifyWaylandPointerMotion(double dx, double dy);
bool NotifyWaylandPointerMotionAbsolute(uint32_t stream, double x, double y);
bool NotifyWaylandPointerButton(int button, uint32_t state);
bool NotifyWaylandPointerAxisDiscrete(uint32_t axis, int32_t steps);
bool SendWaylandPortalVoidCall(const char* method_name,
const std::function<void(DBusMessageIter*)>&
append_args);
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
Display* display_ = nullptr;
Window root_ = 0;
std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0;
int screen_height_ = 0;
};
bool use_wayland_portal_ = false;
#endif
DBusConnection* dbus_connection_ = nullptr;
std::string wayland_session_handle_;
int last_display_index_ = -1;
double last_norm_x_ = -1.0;
double last_norm_y_ = -1.0;
bool logged_wayland_display_info_ = false;
uintptr_t last_logged_wayland_stream_ = 0;
int last_logged_wayland_width_ = 0;
int last_logged_wayland_height_ = 0;
WaylandAbsoluteMode wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown;
bool wayland_absolute_disabled_logged_ = false;
uint32_t wayland_absolute_stream_id_ = 0;
bool using_shared_wayland_session_ = false;
};
} // namespace crossdesk
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@
#include "rd_log.h"
namespace crossdesk {
MouseController::MouseController() {}
MouseController::~MouseController() {}
@@ -98,4 +100,5 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -11,6 +11,8 @@
#include "device_controller.h"
namespace crossdesk {
class MouseController : public DeviceController {
public:
MouseController();
@@ -26,5 +28,5 @@ class MouseController : public DeviceController {
bool left_dragging_ = false;
bool right_dragging_ = false;
};
} // namespace crossdesk
#endif

View File

@@ -2,6 +2,8 @@
#include "rd_log.h"
namespace crossdesk {
MouseController::MouseController() {}
MouseController::~MouseController() {}
@@ -69,4 +71,5 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -11,6 +11,8 @@
#include "device_controller.h"
namespace crossdesk {
class MouseController : public DeviceController {
public:
MouseController();
@@ -24,5 +26,5 @@ class MouseController : public DeviceController {
private:
std::vector<DisplayInfo> display_info_list_;
};
} // namespace crossdesk
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -7,57 +7,81 @@
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#else
#define SETTINGS_WINDOW_HEIGHT_CN 315
#define SETTINGS_WINDOW_HEIGHT_EN 315
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 165
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 165
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#include "render.h"
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
#define LOCAL_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define REMOTE_WINDOW_WIDTH_CN 300 * dpi_scale_
#define REMOTE_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define MENU_WINDOW_WIDTH_EN 190 * dpi_scale_
#define MENU_WINDOW_HEIGHT_EN 245 * dpi_scale_
#define IPUT_WINDOW_WIDTH 160 * dpi_scale_
#define INPUT_WINDOW_PADDING_CN 66 * dpi_scale_
#define INPUT_WINDOW_PADDING_EN 96 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_CN 202 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_EN 248 * dpi_scale_
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 405 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 345 * dpi_scale_
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_EN 218 * dpi_scale_
#define ENABLE_DAEMON_PADDING_CN 171 * dpi_scale_
#define ENABLE_DAEMON_PADDING_EN 218 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SETTINGS_SELECT_WINDOW_WIDTH 73 * dpi_scale_
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_CN 65 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_EN 83 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 * dpi_scale_
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_
#endif

View File

@@ -0,0 +1,91 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#include "render.h"
#define TITLE_BAR_HEIGHT 0.0625f
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
#define STATUS_BAR_HEIGHT 0.05f
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210
#endif

View File

@@ -1,170 +1,156 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <string>
#include <unordered_map>
#include <vector>
#include "localization_data.h"
#if _WIN32
#include <Windows.h>
#endif
namespace localization {
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
static std::vector<std::string> local_desktop = {
reinterpret_cast<const char*>(u8"本桌面"), "Local Desktop"};
static std::vector<std::string> local_id = {
reinterpret_cast<const char*>(u8"本机ID"), "Local ID"};
static std::vector<std::string> local_id_copied_to_clipboard = {
reinterpret_cast<const char*>(u8"已复制到剪贴板"), "Copied to clipboard"};
static std::vector<std::string> password = {
reinterpret_cast<const char*>(u8"密码"), "Password"};
static std::vector<std::string> max_password_len = {
reinterpret_cast<const char*>(u8"最大6个字符"), "Max 6 chars"};
class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
namespace detail {
static std::vector<std::string> remote_desktop = {
reinterpret_cast<const char*>(u8"控制远程桌面"), "Control Remote Desktop"};
static std::vector<std::string> remote_id = {
reinterpret_cast<const char*>(u8"对端ID"), "Remote ID"};
static std::vector<std::string> connect = {
reinterpret_cast<const char*>(u8"连接"), "Connect"};
static std::vector<std::string> recent_connections = {
reinterpret_cast<const char*>(u8"近期连接"), "Recent Connections"};
static std::vector<std::string> disconnect = {
reinterpret_cast<const char*>(u8"断开连接"), "Disconnect"};
static std::vector<std::string> fullscreen = {
reinterpret_cast<const char*>(u8"全屏"), " Fullscreen"};
static std::vector<std::string> show_net_traffic_stats = {
reinterpret_cast<const char*>(u8"显示流量统计"), "Show Net Traffic Stats"};
static std::vector<std::string> hide_net_traffic_stats = {
reinterpret_cast<const char*>(u8"隐藏流量统计"), "Hide Net Traffic Stats"};
static std::vector<std::string> video = {
reinterpret_cast<const char*>(u8"视频"), "Video"};
static std::vector<std::string> audio = {
reinterpret_cast<const char*>(u8"音频"), "Audio"};
static std::vector<std::string> data = {reinterpret_cast<const char*>(u8"数据"),
"Data"};
static std::vector<std::string> total = {
reinterpret_cast<const char*>(u8"总计"), "Total"};
static std::vector<std::string> in = {reinterpret_cast<const char*>(u8"输入"),
"In"};
static std::vector<std::string> out = {reinterpret_cast<const char*>(u8"输出"),
"Out"};
static std::vector<std::string> loss_rate = {
reinterpret_cast<const char*>(u8"丢包率"), "Loss Rate"};
static std::vector<std::string> exit_fullscreen = {
reinterpret_cast<const char*>(u8"退出全屏"), "Exit fullscreen"};
static std::vector<std::string> control_mouse = {
reinterpret_cast<const char*>(u8"控制"), "Control"};
static std::vector<std::string> release_mouse = {
reinterpret_cast<const char*>(u8"释放"), "Release"};
static std::vector<std::string> audio_capture = {
reinterpret_cast<const char*>(u8"声音"), "Audio"};
static std::vector<std::string> mute = {
reinterpret_cast<const char*>(u8" 静音"), " Mute"};
static std::vector<std::string> settings = {
reinterpret_cast<const char*>(u8"设置"), "Settings"};
static std::vector<std::string> language = {
reinterpret_cast<const char*>(u8"语言:"), "Language:"};
static std::vector<std::string> language_zh = {
reinterpret_cast<const char*>(u8"中文"), "Chinese"};
static std::vector<std::string> language_en = {
reinterpret_cast<const char*>(u8"英文"), "English"};
static std::vector<std::string> video_quality = {
reinterpret_cast<const char*>(u8"视频质量:"), "Video Quality:"};
static std::vector<std::string> video_frame_rate = {
reinterpret_cast<const char*>(u8"画面采集帧率:"),
"Video Capture Frame Rate:"};
static std::vector<std::string> video_quality_high = {
reinterpret_cast<const char*>(u8""), "High"};
static std::vector<std::string> video_quality_medium = {
reinterpret_cast<const char*>(u8""), "Medium"};
static std::vector<std::string> video_quality_low = {
reinterpret_cast<const char*>(u8""), "Low"};
static std::vector<std::string> video_encode_format = {
reinterpret_cast<const char*>(u8"视频编码格式:"), "Video Encode Format:"};
static std::vector<std::string> av1 = {reinterpret_cast<const char*>(u8"AV1"),
"AV1"};
static std::vector<std::string> h264 = {
reinterpret_cast<const char*>(u8"H.264"), "H.264"};
static std::vector<std::string> enable_hardware_video_codec = {
reinterpret_cast<const char*>(u8"启用硬件编解码器:"),
"Enable Hardware Video Codec:"};
static std::vector<std::string> enable_turn = {
reinterpret_cast<const char*>(u8"启用中继服务:"), "Enable TURN Service:"};
static std::vector<std::string> enable_srtp = {
reinterpret_cast<const char*>(u8"启用SRTP:"), "Enable SRTP:"};
static std::vector<std::string> self_hosted_server_config = {
reinterpret_cast<const char*>(u8"自托管服务器配置"),
"Self-Hosted Server Config"};
static std::vector<std::string> self_hosted_server_settings = {
reinterpret_cast<const char*>(u8"自托管服务器设置"),
"Self-Hosted Server Settings"};
static std::vector<std::string> self_hosted_server_address = {
reinterpret_cast<const char*>(u8"服务器地址:"), "Server Address:"};
static std::vector<std::string> self_hosted_server_port = {
reinterpret_cast<const char*>(u8"服务器端口:"), "Server Port:"};
static std::vector<std::string> self_hosted_server_certificate_path = {
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
static std::vector<std::string> select_a_file = {
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
"OK"};
static std::vector<std::string> cancel = {
reinterpret_cast<const char*>(u8"取消"), "Cancel"};
inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
}
static std::vector<std::string> new_password = {
reinterpret_cast<const char*>(u8"请输入六位密码:"),
"Please input a six-char password:"};
using TranslationTable =
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>;
static std::vector<std::string> input_password = {
reinterpret_cast<const char*>(u8"请输入密码:"), "Please input password:"};
static std::vector<std::string> validate_password = {
reinterpret_cast<const char*>(u8"验证密码中..."), "Validate password ..."};
static std::vector<std::string> reinput_password = {
reinterpret_cast<const char*>(u8"请重新输入密码"),
"Please input password again"};
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
{"en-US", row.en},
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
}
static std::vector<std::string> remember_password = {
reinterpret_cast<const char*>(u8"记住密码"), "Remember password"};
inline TranslationTable BuildTranslationTable() {
TranslationTable table;
for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
static std::vector<std::string> signal_connected = {
reinterpret_cast<const char*>(u8"已连接服务器"), "Connected"};
static std::vector<std::string> signal_disconnected = {
reinterpret_cast<const char*>(u8"未连接服务器"), "Disconnected"};
static std::vector<std::string> p2p_connected = {
reinterpret_cast<const char*>(u8"对等连接已建立"), "P2P Connected"};
static std::vector<std::string> p2p_disconnected = {
reinterpret_cast<const char*>(u8"对等连接已断开"), "P2P Disconnected"};
static std::vector<std::string> p2p_connecting = {
reinterpret_cast<const char*>(u8"正在建立对等连接..."),
"P2P Connecting ..."};
static std::vector<std::string> p2p_failed = {
reinterpret_cast<const char*>(u8"对等连接失败"), "P2P Failed"};
static std::vector<std::string> p2p_closed = {
reinterpret_cast<const char*>(u8"对等连接已关闭"), "P2P closed"};
static std::vector<std::string> no_such_id = {
reinterpret_cast<const char*>(u8"无此ID"), "No such ID"};
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"};
static std::vector<std::string> version = {
reinterpret_cast<const char*>(u8"版本"), "Version"};
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"),
"Confirm to delete this connection"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
"Minimize to system tray when exit:"};
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
#endif
} // namespace localization
#endif
return table;
}
inline const TranslationTable& GetTranslationTable() {
static const TranslationTable table = BuildTranslationTable();
return table;
}
inline const std::string& GetTranslatedText(const std::string& key,
int language_index) {
static const std::string kEmptyText = "";
const auto& table = GetTranslationTable();
const auto key_it = table.find(key);
if (key_it == table.end()) {
return kEmptyText;
}
const auto& localized_values = key_it->second;
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
const auto exact_it = localized_values.find(language_code);
if (exact_it != localized_values.end()) {
return exact_it->second;
}
const auto english_it = localized_values.find("en-US");
if (english_it != localized_values.end()) {
return english_it->second;
}
const auto chinese_it = localized_values.find("zh-CN");
if (chinese_it != localized_values.end()) {
return chinese_it->second;
}
return kEmptyText;
}
} // namespace detail
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText("exit_program", normalized_index);
if (utf8_text.empty()) {
cached_text = L"Exit";
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = L"Exit";
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length - 1));
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
wide_length);
return cached_text.c_str();
}
#endif
} // namespace localization
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,166 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_DATA_H_
#define _LOCALIZATION_DATA_H_
namespace crossdesk {
namespace localization {
namespace detail {
struct TranslationRow {
const char* key;
const char* zh;
const char* en;
const char* ru;
};
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制", "Control", u8"Управление") \
X(release_mouse, u8"释放", "Release", u8"Освободить") \
X(audio_capture, u8"声音", "Audio", u8"Звук") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"高", "High", u8"Высокое") \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
X(video_quality_low, u8"低", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
};
} // namespace detail
} // namespace localization
} // namespace crossdesk
#endif

36
src/gui/device_presence.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_H_
#define _DEVICE_PRESENCE_H_
#include <mutex>
#include <string>
#include <unordered_map>
class DevicePresence {
public:
void SetOnline(const std::string& device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string& device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
return cache_.count(device_id) > 0 && cache_.at(device_id);
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
#endif

View File

@@ -1,26 +1,36 @@
#include <random>
#include "layout.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
int Render::LocalWindow() {
ImGui::SetNextWindowPos(ImVec2(-1.0f, title_bar_height_), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
namespace crossdesk {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
int Render::LocalWindow() {
ImGuiIO& io = ImGui::GetIO();
float local_window_width = io.DisplaySize.x * 0.5f;
float local_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
float local_window_button_width = io.DisplaySize.x * 0.046f;
float local_window_button_height = io.DisplaySize.y * 0.075f;
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("LocalDesktopWindow",
ImVec2(local_window_width_, local_window_height_),
ImVec2(local_window_width, local_window_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + main_window_text_y_padding_);
ImGui::Indent(main_child_window_x_padding_);
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::local_desktop[localization_language_index_].c_str());
@@ -28,18 +38,16 @@ int Render::LocalWindow() {
ImGui::Spacing();
{
ImGui::SetNextWindowPos(
ImVec2(main_child_window_x_padding_,
title_bar_height_ + main_child_window_y_padding_),
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.15f),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::BeginChild(
"LocalDesktopWindow_1",
ImVec2(local_child_window_width_, local_child_window_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoTitleBar |
"LocalDesktopPanel",
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
@@ -50,7 +58,7 @@ int Render::LocalWindow() {
ImGui::Spacing();
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::SetWindowFontScale(1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
@@ -74,7 +82,8 @@ int Render::LocalWindow() {
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(ICON_FA_COPY, ImVec2(22, 38))) {
if (ImGui::Button(ICON_FA_COPY, ImVec2(local_window_button_width,
local_window_button_height))) {
local_id_copied_ = true;
ImGui::SetClipboardText(client_id_);
copy_start_time_ = ImGui::GetTime();
@@ -84,22 +93,15 @@ int Render::LocalWindow() {
double time_duration = ImGui::GetTime() - copy_start_time_;
if (local_id_copied_ && time_duration < 1.0f) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
notification_window_width_) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
notification_window_height_) /
2));
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(notification_window_width_, notification_window_height_));
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleColor(
ImGuiCol_WindowBg,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
@@ -113,7 +115,7 @@ int Render::LocalWindow() {
[localization_language_index_];
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.5f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::PushStyleColor(ImGuiCol_Text,
ImVec4(0, 0, 0, 1.0f - (float)time_duration));
ImGui::Text("%s", text.c_str());
@@ -132,7 +134,7 @@ int Render::LocalWindow() {
localization::password[localization_language_index_].c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::Spacing();
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
@@ -154,51 +156,35 @@ int Render::LocalWindow() {
ImGui::SetWindowFontScale(0.5f);
auto l_x = ImGui::GetCursorScreenPos().x;
auto l_y = ImGui::GetCursorScreenPos().y;
if (ImGui::Button(ICON_FA_EYE, ImVec2(22, 38))) {
if (ImGui::Button(
show_password_ ? ICON_FA_EYE : ICON_FA_EYE_SLASH,
ImVec2(local_window_button_width, local_window_button_height))) {
show_password_ = !show_password_;
}
if (!show_password_) {
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddLine(ImVec2(l_x + 3.0f, l_y + 12.5f),
ImVec2(l_x + 20.3f, l_y + 26.5f),
IM_COL32(239, 240, 242, 255), 2.0f);
draw_list->AddLine(ImVec2(l_x + 3.0f, l_y + 11.0f),
ImVec2(l_x + 20.3f, l_y + 25.0f),
IM_COL32(0, 0, 0, 255), 1.5f);
}
ImGui::SameLine();
if (ImGui::Button(ICON_FA_PEN, ImVec2(22, 38))) {
if (ImGui::Button(ICON_FA_PEN, ImVec2(local_window_button_width,
local_window_button_height))) {
show_reset_password_window_ = true;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
if (show_reset_password_window_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
connection_status_window_width_) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
connection_status_window_height_) /
2));
ImGui::SetNextWindowSize(ImVec2(connection_status_window_width_,
connection_status_window_height_));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,
window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ResetPasswordWindow", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
@@ -213,9 +199,9 @@ int Render::LocalWindow() {
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetCursorPosX((window_width - IPUT_WINDOW_WIDTH / 2) * 0.5f);
ImGui::SetCursorPosX(window_width * 0.33f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH / 2);
ImGui::SetNextItemWidth(window_width * 0.33f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
@@ -249,15 +235,41 @@ int Render::LocalWindow() {
sizeof(password_saved_) - 1);
password_saved_[sizeof(password_saved_) - 1] = '\0';
std::string client_id_with_password =
std::string(client_id_) + "@" + password_saved_;
strncpy(client_id_with_password_, client_id_with_password.c_str(),
sizeof(client_id_with_password_) - 1);
client_id_with_password_[sizeof(client_id_with_password_) - 1] =
'\0';
// if self hosted
if (config_center_->IsSelfHosted()) {
std::string self_hosted_id_str;
if (strlen(self_hosted_id_) > 0) {
const char* at_pos = strchr(self_hosted_id_, '@');
if (at_pos != nullptr) {
self_hosted_id_str =
std::string(self_hosted_id_, at_pos - self_hosted_id_);
} else {
self_hosted_id_str = self_hosted_id_;
}
} else {
self_hosted_id_str = client_id_;
}
std::string new_self_hosted_id =
self_hosted_id_str + "@" + password_saved_;
memset(&self_hosted_id_, 0, sizeof(self_hosted_id_));
strncpy(self_hosted_id_, new_self_hosted_id.c_str(),
sizeof(self_hosted_id_) - 1);
self_hosted_id_[sizeof(self_hosted_id_) - 1] = '\0';
} else {
std::string client_id_with_password =
std::string(client_id_) + "@" + password_saved_;
strncpy(client_id_with_password_, client_id_with_password.c_str(),
sizeof(client_id_with_password_) - 1);
client_id_with_password_[sizeof(client_id_with_password_) - 1] =
'\0';
}
SaveSettingsIntoCacheFile();
memset(new_password_, 0, sizeof(new_password_));
LeaveConnection(peer_, client_id_);
DestroyPeer(&peer_);
focus_on_input_widget_ = true;
@@ -287,4 +299,5 @@ int Render::LocalWindow() {
ImGui::PopStyleVar();
return 0;
}
}
} // namespace crossdesk

View File

@@ -1,28 +1,32 @@
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
int Render::RecentConnectionsWindow() {
ImGui::SetNextWindowPos(
ImVec2(0, title_bar_height_ + local_window_height_ - 1.0f),
ImGuiCond_Always);
ImGuiIO& io = ImGui::GetIO();
float recent_connection_window_width = io.DisplaySize.x;
float recent_connection_window_height =
io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild(
"RecentConnectionsWindow",
ImVec2(main_window_width_default_,
main_window_height_default_ - title_bar_height_ -
local_window_height_ - status_bar_height_ + 1.0f),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImVec2(recent_connection_window_width, recent_connection_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + main_window_text_y_padding_);
ImGui::Indent(main_child_window_x_padding_);
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::recent_connections[localization_language_index_].c_str());
@@ -35,31 +39,40 @@ int Render::RecentConnectionsWindow() {
}
int Render::ShowRecentConnections() {
ImGui::SetCursorPosX(25.0f);
ImVec2 sub_window_pos = ImGui::GetCursorPos();
std::map<std::string, ImVec2> sub_containers_pos;
ImGuiIO& io = ImGui::GetIO();
float recent_connection_panel_width = io.DisplaySize.x * 0.912f;
float recent_connection_panel_height = io.DisplaySize.y * 0.29f;
float recent_connection_image_height = recent_connection_panel_height * 0.6f;
float recent_connection_image_width = recent_connection_image_height * 16 / 9;
float recent_connection_sub_container_width =
recent_connection_image_width_ + 16.0f;
recent_connection_image_width * 1.2f;
float recent_connection_sub_container_height =
recent_connection_image_height_ + 36.0f;
recent_connection_image_height * 1.4f;
float recent_connection_button_width = recent_connection_image_width * 0.15f;
float recent_connection_button_height =
recent_connection_image_height * 0.25f;
float recent_connection_dummy_button_width =
recent_connection_image_width - 2 * recent_connection_button_width;
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
std::map<std::string, ImVec2> sub_containers_pos;
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::BeginChild("RecentConnectionsContainer",
ImVec2(main_window_width_default_ - 50.0f, 145.0f),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::BeginChild(
"RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
size_t recent_connections_count = recent_connections_.size();
int count = 0;
float button_width = 22;
float button_height = 22;
for (auto& it : recent_connections_) {
sub_containers_pos[it.first] = ImGui::GetCursorPos();
std::string recent_connection_sub_window_name =
@@ -69,11 +82,8 @@ int Render::ShowRecentConnections() {
ImVec2(recent_connection_sub_container_width,
recent_connection_sub_container_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoScrollbar);
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
std::string connection_info = it.first;
// remote id length is 9
@@ -112,14 +122,41 @@ int Render::ShowRecentConnections() {
it.second.remote_host_name = "unknown";
}
ImVec2 image_screen_pos = ImVec2(ImGui::GetCursorScreenPos().x + 5.0f,
ImGui::GetCursorScreenPos().y + 5.0f);
bool online = device_presence_.IsOnline(it.second.remote_id);
ImVec2 image_screen_pos = ImVec2(
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
ImVec2 image_pos =
ImVec2(ImGui::GetCursorPosX() + 5.0f, ImGui::GetCursorPosY() + 5.0f);
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
ImGui::SetCursorPos(image_pos);
ImGui::Image((ImTextureID)(intptr_t)it.second.texture,
ImVec2((float)recent_connection_image_width_,
(float)recent_connection_image_height_));
ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height));
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string display_host_name_with_presence =
it.second.remote_host_name + " " +
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_]);
ImGui::Text("%s", display_host_name_with_presence.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 circle_pos =
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
image_screen_pos.y + recent_connection_image_height * 0.12f);
ImU32 fill_color =
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
ImU32 border_color = IM_COL32(255, 255, 255, 255);
float dot_radius = recent_connection_image_height * 0.06f;
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
100);
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
// remote id display button
{
@@ -128,28 +165,21 @@ int Render::ShowRecentConnections() {
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
ImVec2 dummy_button_pos =
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height_);
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
ImGui::SetCursorPos(dummy_button_pos);
ImGui::SetWindowFontScale(0.6f);
ImGui::Button(dummy_button_name.c_str(),
ImVec2(recent_connection_image_width_ - 2 * button_width,
button_height));
ImVec2(recent_connection_dummy_button_width,
recent_connection_button_height));
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPos(
ImVec2(dummy_button_pos.x + 2.0f, dummy_button_pos.y + 1.0f));
ImGui::SetCursorPos(ImVec2(
dummy_button_pos.x + recent_connection_dummy_button_width * 0.05f,
dummy_button_pos.y + recent_connection_button_height * 0.05f));
ImGui::SetWindowFontScale(0.65f);
ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", it.second.remote_host_name.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
@@ -160,16 +190,18 @@ int Render::ShowRecentConnections() {
ImGui::SetWindowFontScale(0.5f);
// trash button
{
ImVec2 trash_can_button_pos = ImVec2(
image_pos.x + recent_connection_image_width_ - 2 * button_width,
image_pos.y + recent_connection_image_height_);
ImVec2 trash_can_button_pos =
ImVec2(image_pos.x + recent_connection_image_width -
2 * recent_connection_button_width,
image_pos.y + recent_connection_image_height);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" +
std::to_string(trash_can_button_pos.x);
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(button_width, button_height))) {
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
@@ -185,14 +217,16 @@ int Render::ShowRecentConnections() {
// connect button
{
ImVec2 connect_button_pos =
ImVec2(image_pos.x + recent_connection_image_width_ - button_width,
image_pos.y + recent_connection_image_height_);
ImVec2(image_pos.x + recent_connection_image_width -
recent_connection_button_width,
image_pos.y + recent_connection_image_height);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(button_width, button_height))) {
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
}
@@ -204,17 +238,20 @@ int Render::ShowRecentConnections() {
if (count != recent_connections_count - 1) {
ImVec2 line_start =
ImVec2(image_screen_pos.x + recent_connection_image_width_ + 20.0f,
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y);
ImVec2 line_end = ImVec2(
image_screen_pos.x + recent_connection_image_width_ + 20.0f,
image_screen_pos.y + recent_connection_image_height_ + button_height);
ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y + recent_connection_image_height +
recent_connection_button_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
count++;
ImGui::SameLine(0, count != recent_connections_count ? 26.0f : 0.0f);
ImGui::SameLine(0, count != recent_connections_count
? (recent_connection_image_width * 0.165f)
: 0.0f);
}
ImGui::EndChild();
@@ -222,37 +259,38 @@ int Render::ShowRecentConnections() {
if (show_confirm_delete_connection_) {
ConfirmDeleteConnection();
}
if (show_offline_warning_window_) {
OfflineWarningWindow();
}
return 0;
}
int Render::ConfirmDeleteConnection() {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
connection_status_window_width_) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
connection_status_window_height_) /
2));
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(ImVec2(connection_status_window_width_,
connection_status_window_height_));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto connection_status_window_width = ImGui::GetWindowSize().x;
auto connection_status_window_height = ImGui::GetWindowSize().y;
std::string text =
localization::confirm_delete_connection[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 6 / 19);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.33f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
ImGui::SetWindowFontScale(0.5f);
@@ -271,12 +309,9 @@ int Render::ConfirmDeleteConnection() {
show_confirm_delete_connection_ = false;
}
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
@@ -284,3 +319,45 @@ int Render::ConfirmDeleteConnection() {
ImGui::PopStyleVar();
return 0;
}
int Render::OfflineWarningWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("OfflineWarningWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetCursorPosX(window_width * 0.43f);
ImGui::SetCursorPosY(window_height * 0.67f);
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_offline_warning_window_ = false;
}
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", offline_warning_text_.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

View File

@@ -1,27 +1,37 @@
#include "layout.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
static int InputTextCallback(ImGuiInputTextCallbackData *data);
namespace crossdesk {
static int InputTextCallback(ImGuiInputTextCallbackData* data);
int Render::RemoteWindow() {
ImGui::SetNextWindowPos(ImVec2(local_window_width_ + 1.0f, title_bar_height_),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGuiIO& io = ImGui::GetIO();
float remote_window_width = io.DisplaySize.x * 0.5f;
float remote_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
float remote_window_arrow_button_width = io.DisplaySize.x * 0.1f;
float remote_window_arrow_button_height = io.DisplaySize.y * 0.078f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("RemoteDesktopWindow",
ImVec2(remote_window_width_, remote_window_height_),
ImVec2(remote_window_width, remote_window_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + main_window_text_y_padding_);
ImGui::Indent(main_child_window_x_padding_ - 1.0f);
ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
ImGui::SetWindowFontScale(0.9f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::remote_desktop[localization_language_index_].c_str());
@@ -29,8 +39,7 @@ int Render::RemoteWindow() {
ImGui::Spacing();
{
ImGui::SetNextWindowPos(
ImVec2(local_window_width_ + main_child_window_x_padding_ - 1.0f,
title_bar_height_ + main_child_window_y_padding_),
ImVec2(io.DisplaySize.x * 0.557f, io.DisplaySize.y * 0.15f),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
@@ -38,10 +47,9 @@ int Render::RemoteWindow() {
ImGui::BeginChild(
"RemoteDesktopWindow_1",
ImVec2(remote_child_window_width_, remote_child_window_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
@@ -51,7 +59,7 @@ int Render::RemoteWindow() {
"%s", localization::remote_id[localization_language_index_].c_str());
ImGui::Spacing();
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(io.DisplaySize.x * 0.25f);
ImGui::SetWindowFontScale(1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (re_enter_remote_id_) {
@@ -73,38 +81,65 @@ int Render::RemoteWindow() {
remote_id.erase(remove_if(remote_id.begin(), remote_id.end(),
static_cast<int (*)(int)>(&isspace)),
remote_id.end());
if (ImGui::Button(ICON_FA_ARROW_RIGHT_LONG, ImVec2(55, 38)) ||
if (ImGui::Button(ICON_FA_ARROW_RIGHT_LONG,
ImVec2(remote_window_arrow_button_width,
remote_window_arrow_button_height)) ||
enter_pressed) {
connect_button_pressed_ = true;
bool found = false;
for (auto &[id, props] : recent_connections_) {
std::string target_remote_id;
std::string target_password;
bool should_connect = false;
bool already_connected = false;
for (auto& [id, props] : recent_connections_) {
if (id.find(remote_id) != std::string::npos) {
found = true;
if (client_properties_.find(remote_id) !=
client_properties_.end()) {
if (!client_properties_[remote_id]->connection_established_) {
ConnectTo(props.remote_id, props.password.c_str(), false);
target_remote_id = props.remote_id;
target_password = props.password;
{
// std::shared_lock lock(client_properties_mutex_);
if (client_properties_.find(remote_id) !=
client_properties_.end()) {
if (!client_properties_[remote_id]->connection_established_) {
should_connect = true;
} else {
already_connected = true;
}
} else {
// todo: show warning message
LOG_INFO("Already connected to [{}]", remote_id);
should_connect = true;
}
} else {
ConnectTo(props.remote_id, props.password.c_str(), false);
}
if (should_connect) {
ConnectTo(target_remote_id, target_password.c_str(), false);
} else if (already_connected) {
LOG_INFO("Already connected to [{}]", remote_id);
}
break;
}
}
if (!found) {
ConnectTo(remote_id, "", false);
}
}
// check every 1 second for rejoin
if (need_to_rejoin_) {
need_to_rejoin_ = false;
for (const auto &[_, props] : client_properties_) {
if (props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_);
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - last_rejoin_check_time_)
.count();
if (elapsed >= 1000) {
last_rejoin_check_time_ = now;
need_to_rejoin_ = false;
// std::shared_lock lock(client_properties_mutex_);
for (const auto& [_, props] : client_properties_) {
if (props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_);
}
}
}
}
@@ -117,7 +152,7 @@ int Render::RemoteWindow() {
return 0;
}
static int InputTextCallback(ImGuiInputTextCallbackData *data) {
static int InputTextCallback(ImGuiInputTextCallbackData* data) {
if (data->BufTextLen > 3 && data->Buf[3] != ' ') {
data->InsertChars(3, " ");
}
@@ -129,34 +164,89 @@ static int InputTextCallback(ImGuiInputTextCallbackData *data) {
return 0;
}
int Render::ConnectTo(const std::string &remote_id, const char *password,
bool remember_password) {
int Render::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
if (client_properties_.find(remote_id) == client_properties_.end()) {
client_properties_[remote_id] =
std::make_shared<SubStreamWindowProperties>();
auto props = client_properties_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str());
// std::shared_lock shared_lock(client_properties_mutex_);
bool exists =
(client_properties_.find(remote_id) != client_properties_.end());
// shared_lock.unlock();
if (props->peer_) {
LOG_INFO("[{}] Create peer instance successful", props->local_id_);
Init(props->peer_);
LOG_INFO("[{}] Peer init finish", props->local_id_);
} else {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
if (!exists) {
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(client_properties_mutex_);
if (client_properties_.find(remote_id) == client_properties_.end()) {
client_properties_[remote_id] =
std::make_shared<SubStreamWindowProperties>();
auto props = client_properties_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 9.0f;
props->control_window_height_ = title_bar_height_ * 1.3f;
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 9.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_ = ConnectionStatus::Connecting;
show_connection_status_window_ = true;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
return -1;
}
for (auto& display_info : display_info_list_) {
AddVideoStream(props->peer_, display_info.name.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_ = ConnectionStatus::Connecting;
peer_to_init = props->peer_;
local_id = props->local_id_;
}
}
props->connection_status_ = ConnectionStatus::Connecting;
if (peer_to_init) {
LOG_INFO("[{}] Create peer instance successful", local_id);
Init(peer_to_init);
LOG_INFO("[{}] Peer init finish", local_id);
}
}
int ret = -1;
// std::shared_lock read_lock(client_properties_mutex_);
auto props = client_properties_[remote_id];
if (!props->connection_established_) {
props->remember_password_ = remember_password;
@@ -168,14 +258,18 @@ int Render::ConnectTo(const std::string &remote_id, const char *password,
}
std::string remote_id_with_pwd = remote_id + "@" + password;
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
if (props->peer_) {
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
}
}
}
// read_lock.unlock();
return 0;
}
}
} // namespace crossdesk

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,22 @@
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <queue>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "IconsFontAwesome6.h"
#include "config_center.h"
#include "device_controller_factory.h"
#include "device_presence.h"
#include "imgui.h"
#include "imgui_impl_sdl3.h"
#include "imgui_impl_sdlrenderer3.h"
@@ -29,17 +36,60 @@
#include "screen_capturer_factory.h"
#include "speaker_capturer_factory.h"
#include "thumbnail.h"
#if _WIN32
#include "win_tray.h"
#endif
namespace crossdesk {
class Render {
public:
struct FileTransferState {
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
std::string remote_id;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
struct SubStreamWindowProperties {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "control_data";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string file_label_ = "file";
std::string control_data_label_ = "control_data";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
std::string local_id_ = "";
std::string remote_id_ = "";
bool exit_ = false;
@@ -48,10 +98,10 @@ class Render {
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool mouse_control_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = false;
bool control_mouse_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
@@ -67,18 +117,23 @@ class Render {
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 230;
float control_window_min_height_ = 40;
float control_window_max_height_ = 170;
float control_window_min_height_ = 38;
float control_window_max_height_ = 180;
float control_window_width_ = 230;
float control_window_height_ = 40;
float control_window_height_ = 38;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
unsigned char* dst_buffer_ = nullptr;
size_t dst_buffer_capacity_ = 0;
// Double-buffered NV12 frame storage. Written by decode callback thread,
// consumed by SDL main thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
@@ -108,6 +163,7 @@ class Render {
SDL_Texture* stream_texture_ = nullptr;
uint8_t* argb_buffer_ = nullptr;
int argb_buffer_size_ = 0;
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
SDL_Rect stream_render_rect_;
SDL_Rect stream_render_rect_last_;
ImVec2 control_window_pos_;
@@ -117,6 +173,11 @@ class Render {
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_;
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
};
public:
@@ -136,7 +197,10 @@ class Render {
void UpdateLabels();
void UpdateInteractions();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleStreamWindow();
void HandleServerWindow();
void Cleanup();
void CleanupFactories();
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
@@ -146,17 +210,29 @@ class Render {
void UpdateRenderRect();
void ProcessSdlEvent(const SDL_Event& event);
void ProcessFileDropEvent(const SDL_Event& event);
void ProcessSelectedFile(
const std::string& path,
const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id = "");
std::shared_ptr<SubStreamWindowProperties>
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
private:
int CreateStreamRenderWindow();
int TitleBar(bool main_window);
int MainWindow();
int UpdateNotificationWindow();
int StreamWindow();
int ServerWindow();
int RemoteClientInfoWindow();
int LocalWindow();
int RemoteWindow();
int RecentConnectionsWindow();
int SettingWindow();
int SelfHostedServerWindow();
int ShowSimpleFileBrowser();
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
int AboutWindow();
@@ -164,50 +240,76 @@ class Render {
bool ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props);
int ShowRecentConnections();
bool OpenUrl(const std::string& url);
void Hyperlink(const std::string& label, const std::string& url,
const float window_width);
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
std::string OpenFileDialog(std::string title);
private:
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password);
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
int CreateMainWindow();
int DestroyMainWindow();
int CreateStreamWindow();
int DestroyStreamWindow();
int SetupFontAndStyle();
int SetupMainWindow();
int CreateServerWindow();
int DestroyServerWindow();
int SetupFontAndStyle(ImFont** system_chinese_font_out);
int DestroyMainWindowContext();
int SetupStreamWindow();
int DestroyStreamWindowContext();
int DestroyServerWindowContext();
int DrawMainWindow();
int DrawStreamWindow();
int DrawServerWindow();
int ConfirmDeleteConnection();
int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props);
void DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props);
#ifdef __APPLE__
int RequestPermissionWindow();
bool CheckScreenRecordingPermission();
bool CheckAccessibilityPermission();
void OpenScreenRecordingPreferences();
void OpenAccessibilityPreferences();
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
#endif
public:
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size,
void* user_data);
static void OnReceiveAudioBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size,
void* user_data);
static void OnReceiveDataBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size,
void* user_data);
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void OnSignalMessageCb(const char* message, size_t size,
void* user_data);
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void NetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data);
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data);
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
const SDL_Point* area, void* data);
@@ -221,6 +323,9 @@ class Render {
private:
int SendKeyCommand(int key_code, bool is_down);
static bool IsModifierVkKey(int key_code);
void UpdatePressedModifierState(int key_code, bool is_down);
void ForceReleasePressedModifiers();
int ProcessMouseEvent(const SDL_Event& event);
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
@@ -245,6 +350,13 @@ class Render {
int CreateConnectionPeer();
// File transfer helper functions
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
const std::filesystem::path& file_path,
const std::string& file_label,
const std::string& remote_id = "");
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
int AudioDeviceInit();
int AudioDeviceDestroy();
@@ -263,14 +375,30 @@ class Render {
unsigned char iv[16];
};
struct CDCacheV2 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
char self_hosted_id[17];
};
private:
CDCache cd_cache_;
CDCacheV2 cd_cache_v2_;
std::mutex cd_cache_mutex_;
std::unique_ptr<ConfigCenter> config_center_;
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string cert_path_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
@@ -280,25 +408,33 @@ class Render {
/* ------ all windows property start ------ */
float title_bar_width_ = 640;
float title_bar_height_ = 30;
float title_bar_button_width_ = 30;
float title_bar_button_height_ = 30;
/* ------ all windows property end ------ */
/* ------ main window property start ------ */
// thumbnail
unsigned char aes128_key_[16];
unsigned char aes128_iv_[16];
std::unique_ptr<Thumbnail> thumbnail_;
std::shared_ptr<Thumbnail> thumbnail_;
// recent connections
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresence device_presence_;
bool need_to_send_recent_connections_ = true;
// main window render
SDL_Window* main_window_ = nullptr;
SDL_Renderer* main_renderer_ = nullptr;
ImGuiContext* main_ctx_ = nullptr;
ImFont* main_windows_system_chinese_font_ = nullptr;
ImFont* stream_windows_system_chinese_font_ = nullptr;
ImFont* server_windows_system_chinese_font_ = nullptr;
bool exit_ = false;
const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32
@@ -306,19 +442,30 @@ class Render {
#endif
// main window properties
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_ = "";
std::string release_notes_ = "";
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool foucs_on_stream_window_ = false;
bool focus_on_stream_window_ = false;
bool main_window_minimized_ = false;
uint32_t last_main_minimize_request_tick_ = 0;
uint32_t last_stream_minimize_request_tick_ = 0;
bool audio_capture_ = false;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
float main_window_dpi_scaling_h_ = 1.0f;
float dpi_scale_ = 1.0f;
float main_window_width_default_ = 640;
float main_window_height_default_ = 480;
float main_window_width_ = 640;
@@ -343,6 +490,8 @@ class Render {
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
@@ -356,10 +505,13 @@ class Render {
int audio_len_ = 0;
bool audio_buffer_fresh_ = false;
bool need_to_rejoin_ = false;
std::chrono::steady_clock::time_point last_rejoin_check_time_;
bool just_created_ = false;
std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = "";
bool need_to_send_host_info_ = false;
std::string remote_client_id_ = "";
std::unordered_set<int> pressed_modifier_keys_;
std::mutex pressed_modifier_keys_mutex_;
SDL_Event last_mouse_event;
SDL_AudioStream* output_stream_;
uint32_t STREAM_REFRESH_EVENT = 0;
@@ -386,6 +538,44 @@ class Render {
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
// server window render
SDL_Window* server_window_ = nullptr;
SDL_Renderer* server_renderer_ = nullptr;
ImGuiContext* server_ctx_ = nullptr;
// server window properties
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
int server_window_height_default_ = 150;
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
// server window collapsed mode
bool server_window_collapsed_ = false;
bool server_window_collapsed_dragging_ = false;
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
int server_window_collapsed_drag_start_win_x_ = 0;
int server_window_collapsed_drag_start_win_y_ = 0;
// server window drag normal mode
bool server_window_dragging_ = false;
float server_window_drag_start_mouse_x_ = 0.0f;
float server_window_drag_start_mouse_y_ = 0.0f;
int server_window_drag_start_win_x_ = 0;
int server_window_drag_start_win_y_ = 0;
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
@@ -398,14 +588,18 @@ class Render {
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool show_update_notification_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = "";
std::string offline_warning_text_ = "";
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
@@ -417,7 +611,22 @@ class Render {
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string info_label_ = "info";
std::string control_data_label_ = "control_data";
std::string file_label_ = "file";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
Params params_;
// Map file_id to props for tracking file transfer progress via ACK
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
file_id_to_props_;
std::shared_mutex file_id_to_props_mutex_;
// Map file_id to FileTransferState for global file transfer (props == null)
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
std::shared_mutex file_id_to_transfer_state_mutex_;
SDL_AudioDeviceID input_dev_;
SDL_AudioDeviceID output_dev_;
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
@@ -429,42 +638,80 @@ class Render {
KeyboardCapturer* keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
uint64_t last_frame_time_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0;
#ifdef __APPLE__
bool show_request_permission_window_ = true;
#endif
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 0;
int video_frame_rate_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false;
bool enable_turn_ = false;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char cert_file_path_[256] = "";
bool enable_self_hosted_server_ = false;
char coturn_server_port_[6] = "3478";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char signal_server_ip_tmp_[256] = "api.crossdesk.cn";
char signal_server_port_tmp_[6] = "9099";
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_ = "";
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = "";
std::string selected_file_ = "";
bool show_file_browser_ = true;
/* ------ main window property end ------ */
/* ------ sub stream window property start ------ */
std::unordered_map<std::string, std::shared_ptr<SubStreamWindowProperties>>
client_properties_;
std::shared_mutex client_properties_mutex_;
void CloseTab(decltype(client_properties_)::iterator& it);
/* ------ stream window property end ------ */
};
#endif
/* ------ async thumbnail save tasks ------ */
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
void WaitForThumbnailSaveTasks();
/* ------ server mode ------ */
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = "";
std::string selected_server_remote_hostname_ = "";
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::string pending_presence_remote_id_ = "";
std::string pending_presence_password_ = "";
bool pending_presence_remember_password_ = false;
FileTransferState file_transfer_;
};
} // namespace crossdesk
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,19 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <thread>
#include <vector>
#include "file_transfer.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h"
namespace crossdesk {
int CountDigits(int number) {
if (number == 0) return 1;
@@ -29,27 +41,122 @@ int LossRateDisplay(float loss_rate) {
return 0;
}
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
std::string Render::OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(),
"", // default path
0, // number of filters
nullptr, // filters
nullptr, // filter description
0 // no multiple selection
);
return path ? path : "";
}
void Render::ProcessSelectedFile(
const std::string& path,
const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id) {
if (path.empty()) {
return;
}
FileTransferState* file_transfer_state =
props ? &props->file_transfer_ : &file_transfer_;
LOG_INFO("Selected file: {}", path.c_str());
std::filesystem::path file_path = std::filesystem::u8path(path);
// Get file size
std::error_code ec;
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
file_size = 0;
}
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
info.file_name = file_path.filename().u8string();
info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
file_transfer_state->file_transfer_list_.push_back(info);
}
file_transfer_state->file_transfer_window_visible_ = true;
if (file_transfer_state->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
} else {
StartFileTransfer(props, file_path, file_label, remote_id);
if (file_transfer_state->file_sending_.load()) {
} else {
// Failed to start (race condition: another file started between
// check and call) Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO(
"File added to queue after race condition: {} ({} files in "
"queue)",
file_path.filename().string().c_str(), queue_size);
}
}
}
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float button_width = title_bar_height_ * 0.8f;
float button_height = title_bar_height_ * 0.8f;
float line_padding = title_bar_height_ * 0.12f;
float line_thickness = title_bar_height_ * 0.07f;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
if (props->control_bar_expand_) {
ImGui::SetCursorPosX(props->is_control_bar_in_left_
? (props->control_window_width_ + 5.0f)
: 38.0f);
// mouse control button
ImDrawList* draw_list = ImGui::GetWindowDrawList();
? props->control_window_width_ * 0.03f
: props->control_window_width_ * 0.17f);
if (props->is_control_bar_in_left_) {
draw_list->AddLine(ImVec2(ImGui::GetCursorScreenPos().x - 5.0f,
ImGui::GetCursorScreenPos().y - 7.0f),
ImVec2(ImGui::GetCursorScreenPos().x - 5.0f,
ImGui::GetCursorScreenPos().y - 7.0f +
props->control_window_height_),
IM_COL32(178, 178, 178, 255), 1.0f);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
if (!props->is_control_bar_in_left_) {
draw_list->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
IM_COL32(178, 178, 178, 255), 2.0f);
}
std::string display = ICON_FA_DISPLAY;
if (ImGui::Button(display.c_str(), ImVec2(25, 25))) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(display.c_str(), ImVec2(button_width, button_height))) {
ImGui::OpenPopup("display");
}
@@ -66,69 +173,73 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
remote_action.type = ControlType::display_id;
remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) {
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
}
}
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndPopup();
}
ImGui::SetWindowFontScale(0.6f);
ImGui::SetWindowFontScale(0.5f);
ImVec2 text_size = ImGui::CalcTextSize(
std::to_string(props->selected_display_ + 1).c_str());
ImVec2 text_pos =
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f,
btn_min.y + (btn_size_actual.y - text_size.y) * 0.5f - 2.0f);
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f);
ImGui::GetWindowDrawList()->AddText(
text_pos, IM_COL32(0, 0, 0, 255),
std::to_string(props->selected_display_ + 1).c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::SameLine();
float disable_mouse_x = ImGui::GetCursorScreenPos().x + 4.0f;
float disable_mouse_y = ImGui::GetCursorScreenPos().y + 4.0f;
std::string mouse = props->mouse_control_button_pressed_
? ICON_FA_COMPUTER_MOUSE
: ICON_FA_COMPUTER_MOUSE;
if (ImGui::Button(mouse.c_str(), ImVec2(25, 25))) {
float mouse_x = ImGui::GetCursorScreenPos().x;
float mouse_y = ImGui::GetCursorScreenPos().y;
float disable_mouse_x = mouse_x + line_padding;
float disable_mouse_y = mouse_y + line_padding;
std::string mouse = ICON_FA_COMPUTER_MOUSE;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) {
start_keyboard_capturer_ = !start_keyboard_capturer_;
props->control_mouse_ = !props->control_mouse_;
props->mouse_control_button_pressed_ =
!props->mouse_control_button_pressed_;
props->enable_mouse_control_ = !props->enable_mouse_control_;
props->mouse_control_button_label_ =
props->mouse_control_button_pressed_
props->enable_mouse_control_
? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_];
}
}
if (!props->mouse_control_button_pressed_) {
if (!props->enable_mouse_control_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
ImVec2(mouse_x + button_width - line_padding,
mouse_y + button_height - line_padding),
IM_COL32(0, 0, 0, 255), line_thickness);
draw_list->AddLine(
ImVec2(disable_mouse_x, disable_mouse_y),
ImVec2(disable_mouse_x + 16.0f, disable_mouse_y + 14.2f),
IM_COL32(0, 0, 0, 255), 2.0f);
draw_list->AddLine(
ImVec2(disable_mouse_x - 1.2f, disable_mouse_y + 1.2f),
ImVec2(disable_mouse_x + 15.3f, disable_mouse_y + 15.4f),
ImVec2(disable_mouse_x - line_thickness * 0.7f,
disable_mouse_y + line_thickness * 0.7f),
ImVec2(
mouse_x + button_width - line_padding - line_thickness * 0.7f,
mouse_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
2.0f);
line_thickness);
}
ImGui::SameLine();
// audio capture button
float disable_audio_x = ImGui::GetCursorScreenPos().x + 4;
float disable_audio_y = ImGui::GetCursorScreenPos().y + 4.0f;
// std::string audio = audio_capture_button_pressed_ ? ICON_FA_VOLUME_HIGH
// :
// ICON_FA_VOLUME_XMARK;
float audio_x = ImGui::GetCursorScreenPos().x;
float audio_y = ImGui::GetCursorScreenPos().y;
float disable_audio_x = audio_x + line_padding;
float disable_audio_y = audio_y + line_padding;
std::string audio = props->audio_capture_button_pressed_
? ICON_FA_VOLUME_HIGH
: ICON_FA_VOLUME_HIGH;
if (ImGui::Button(audio.c_str(), ImVec2(25, 25))) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(audio.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) {
props->audio_capture_button_pressed_ =
!props->audio_capture_button_pressed_;
@@ -140,21 +251,36 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
RemoteAction remote_action;
remote_action.type = ControlType::audio_capture;
remote_action.a = props->audio_capture_button_pressed_;
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str());
}
}
if (!props->audio_capture_button_pressed_) {
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
ImVec2(audio_x + button_width - line_padding,
audio_y + button_height - line_padding),
IM_COL32(0, 0, 0, 255), line_thickness);
draw_list->AddLine(
ImVec2(disable_audio_x, disable_audio_y),
ImVec2(disable_audio_x + 16.0f, disable_audio_y + 14.2f),
IM_COL32(0, 0, 0, 255), 2.0f);
draw_list->AddLine(
ImVec2(disable_audio_x - 1.2f, disable_audio_y + 1.2f),
ImVec2(disable_audio_x + 15.3f, disable_audio_y + 15.4f),
ImVec2(disable_audio_x - line_thickness * 0.7f,
disable_audio_y + line_thickness * 0.7f),
ImVec2(
audio_x + button_width - line_padding - line_thickness * 0.7f,
audio_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255),
2.0f);
line_thickness);
}
ImGui::SameLine();
std::string open_folder = ICON_FA_FOLDER_OPEN;
if (ImGui::Button(open_folder.c_str(),
ImVec2(button_width, button_height))) {
std::string title =
localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
ProcessSelectedFile(path, props, file_label_);
}
ImGui::SameLine();
@@ -166,7 +292,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
button_color_style_pushed = true;
}
std::string net_traffic_stats = ICON_FA_SIGNAL;
if (ImGui::Button(net_traffic_stats.c_str(), ImVec2(25, 25))) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(net_traffic_stats.c_str(),
ImVec2(button_width, button_height))) {
props->net_traffic_stats_button_pressed_ =
!props->net_traffic_stats_button_pressed_;
props->control_window_height_is_changing_ = true;
@@ -178,6 +306,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: localization::show_net_traffic_stats
[localization_language_index_];
}
if (button_color_style_pushed) {
ImGui::PopStyleColor();
button_color_style_pushed = false;
@@ -187,7 +316,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
// fullscreen button
std::string fullscreen =
fullscreen_button_pressed_ ? ICON_FA_COMPRESS : ICON_FA_EXPAND;
if (ImGui::Button(fullscreen.c_str(), ImVec2(25, 25))) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(fullscreen.c_str(),
ImVec2(button_width, button_height))) {
fullscreen_button_pressed_ = !fullscreen_button_pressed_;
props->fullscreen_button_label_ =
fullscreen_button_pressed_
@@ -205,25 +336,33 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SameLine();
// close button
std::string close_button = ICON_FA_XMARK;
if (ImGui::Button(close_button.c_str(), ImVec2(25, 25))) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(close_button.c_str(),
ImVec2(button_width, button_height))) {
CleanupPeer(props);
}
ImGui::SameLine();
if (!props->is_control_bar_in_left_) {
draw_list->AddLine(ImVec2(ImGui::GetCursorScreenPos().x - 3.0f,
ImGui::GetCursorScreenPos().y - 7.0f),
ImVec2(ImGui::GetCursorScreenPos().x - 3.0f,
ImGui::GetCursorScreenPos().y - 7.0f +
props->control_window_height_),
IM_COL32(178, 178, 178, 255), 1.0f);
if (props->is_control_bar_in_left_) {
draw_list->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
IM_COL32(178, 178, 178, 255), 2.0f);
}
ImGui::SameLine();
}
ImGui::SetCursorPosX(props->is_control_bar_in_left_
? (props->control_window_width_ * 2 - 20.0f)
: 5.0f);
float expand_button_pos_x = props->control_bar_expand_
? (props->is_control_bar_in_left_
? props->control_window_width_ * 0.917f
: props->control_window_width_ * 0.03f)
: props->control_window_width_ * 0.11f;
ImGui::SetCursorPosX(expand_button_pos_x);
std::string control_bar =
props->control_bar_expand_
@@ -231,13 +370,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: ICON_FA_ANGLE_RIGHT)
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
: ICON_FA_ANGLE_LEFT);
if (ImGui::Button(control_bar.c_str(), ImVec2(15, 25))) {
if (ImGui::Button(control_bar.c_str(),
ImVec2(button_height * 0.6f, button_height))) {
props->control_bar_expand_ = !props->control_bar_expand_;
props->control_bar_button_pressed_time_ = ImGui::GetTime();
props->control_window_width_is_changing_ = true;
if (!props->control_bar_expand_) {
props->control_window_height_ = 40;
props->control_window_height_ = props->control_window_min_height_;
props->net_traffic_stats_button_pressed_ = false;
}
}
@@ -252,14 +392,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
}
int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SetCursorPos(ImVec2(props->is_control_bar_in_left_
? (props->control_window_width_ + 5.0f)
: 5.0f,
40.0f));
ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH,
ImVec2(props->control_window_max_width_ - 10.0f,
props->control_window_max_height_ - 60.0f))) {
ImVec2(props->control_window_max_width_ * 0.9f,
props->control_window_max_height_ - 0.9f))) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
@@ -315,12 +453,37 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("FPS");
ImGui::Text("FPS:");
ImGui::TableNextColumn();
ImGui::Text("%d", props->fps_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text("%s:",
localization::resolution[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%dx%d", props->video_width_, props->video_height_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text(
"%s:",
localization::connection_mode[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text(
"%s",
props->traversal_mode_ == 0
? localization::connection_mode_direct[localization_language_index_]
.c_str()
: localization::connection_mode_relay[localization_language_index_]
.c_str());
ImGui::EndTable();
}
ImGui::SetWindowFontScale(1.0f);
return 0;
}
} // namespace crossdesk

View File

@@ -1,30 +1,40 @@
#include "layout_relative.h"
#include "localization.h"
#include "render.h"
namespace crossdesk {
int Render::StatusBar() {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGuiIO& io = ImGui::GetIO();
float status_bar_width = io.DisplaySize.x;
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
static bool a, b, c, d, e;
ImGui::SetNextWindowPos(
ImVec2(0, main_window_height_default_ - status_bar_height_ - 1),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::BeginChild(
"StatusBar", ImVec2(main_window_width_, status_bar_height_ + 1),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor(2);
ImVec2 dot_pos =
ImVec2(13, main_window_height_default_ - status_bar_height_ + 11.0f);
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddCircleFilled(dot_pos, 5.0f,
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
ImColor(signal_connected_ ? 0.0f : 1.0f,
signal_connected_ ? 1.0f : 0.0f, 0.0f),
100);
draw_list->AddCircle(dot_pos, 6.0f, ImColor(1.0f, 1.0f, 1.0f), 100);
ImGui::SetWindowFontScale(0.6f);
draw_list->AddText(
ImVec2(25, main_window_height_default_ - status_bar_height_ + 3.0f),
ImVec2(status_bar_width * 0.045f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
ImColor(0.0f, 0.0f, 0.0f),
signal_connected_
? localization::signal_connected[localization_language_index_].c_str()
@@ -32,7 +42,7 @@ int Render::StatusBar() {
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::EndChild();
return 0;
}
}
} // namespace crossdesk

View File

@@ -1,174 +1,332 @@
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
#define BUTTON_PADDING 36.0f
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
namespace crossdesk {
int Render::TitleBar(bool main_window) {
ImGui::PushStyleColor(ImGuiCol_MenuBarBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGuiIO& io = ImGui::GetIO();
float title_bar_width = title_bar_width_;
float title_bar_height = title_bar_height_;
float title_bar_height_padding = title_bar_height_;
float title_bar_button_width = title_bar_button_width_;
float title_bar_button_height = title_bar_button_height_;
if (main_window) {
// When the main window is minimized, Dear ImGui may report DisplaySize as
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
// stream/server windows (which reuse these metrics) will lose their title
// bars and appear collapsed.
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
title_bar_height_ = title_bar_height;
title_bar_button_width_ = title_bar_button_width;
title_bar_button_height_ = title_bar_button_height;
} else {
// Keep using last known good values.
title_bar_width = title_bar_width_;
title_bar_height = title_bar_height_;
title_bar_height_padding = title_bar_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
} else {
title_bar_width = io.DisplaySize.x;
title_bar_height = title_bar_button_height_;
title_bar_button_width = title_bar_button_width_;
title_bar_button_height = title_bar_button_height_;
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetWindowFontScale(0.8f);
ImGui::BeginChild(
main_window ? "MainTitleBar" : "StreamTitleBar",
ImVec2(main_window ? main_window_width_ : stream_window_width_,
title_bar_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::SetWindowFontScale(1.0f);
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
ImVec2(title_bar_width, title_bar_height_padding),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
// get draw list
ImDrawList* draw_list = ImGui::GetWindowDrawList();
if (ImGui::BeginMenuBar()) {
ImGui::SetCursorPosX(
(main_window ? main_window_width_ : stream_window_width_) -
(BUTTON_PADDING * 3 - 3));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (main_window) {
float bar_pos_x = ImGui::GetCursorPosX() + 6;
float bar_pos_y = ImGui::GetCursorPosY() + 15;
std::string menu_button = " "; // ICON_FA_BARS;
if (ImGui::BeginMenu(menu_button.c_str())) {
ImGui::SetWindowFontScale(0.5f);
if (ImGui::MenuItem(
localization::settings[localization_language_index_].c_str())) {
show_settings_window_ = true;
}
if (ImGui::MenuItem(
localization::about[localization_language_index_].c_str())) {
show_about_window_ = true;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndMenu();
}
float menu_bar_line_size = 15.0f;
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y - 6),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y - 6),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y + 6),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y + 6),
IM_COL32(0, 0, 0, 255));
{
SettingWindow();
SelfHostedServerWindow();
AboutWindow();
ImGui::SetCursorPos(
ImVec2(title_bar_width - title_bar_button_width * 3, 0.0f));
if (main_window) {
float bar_pos_x = title_bar_width - title_bar_button_width * 3 +
title_bar_button_width * 0.33f;
float bar_pos_y = title_bar_button_height * 0.5f;
// draw menu icon
float menu_bar_line_size = title_bar_button_width * 0.33f;
draw_list->AddLine(
ImVec2(bar_pos_x, bar_pos_y - title_bar_button_height * 0.15f),
ImVec2(bar_pos_x + menu_bar_line_size,
bar_pos_y - title_bar_button_height * 0.15f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(
ImVec2(bar_pos_x, bar_pos_y + title_bar_button_height * 0.15f),
ImVec2(bar_pos_x + menu_bar_line_size,
bar_pos_y + title_bar_button_height * 0.15f),
IM_COL32(0, 0, 0, 255));
std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS;
std::string title_bar_menu = "##title_bar_menu";
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (ImGui::Button(
title_bar_menu_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
ImGui::OpenPopup(title_bar_menu.c_str());
}
ImGui::PopStyleColor(3);
if (ImGui::BeginPopup(title_bar_menu.c_str())) {
ImGui::SetWindowFontScale(0.6f);
if (ImGui::MenuItem(
localization::settings[localization_language_index_].c_str())) {
show_settings_window_ = true;
}
show_new_version_icon_in_menu_ = false;
std::string about_str = localization::about[localization_language_index_];
if (update_available_) {
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
} else {
about_str = about_str + " ";
}
}
if (ImGui::MenuItem(about_str.c_str())) {
show_about_window_ = true;
}
if (update_available_ && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string new_version_available_str =
localization::new_version_available[localization_language_index_] +
": " + latest_version_;
ImGui::Text("%s", new_version_available_str.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::EndPopup();
} else {
show_new_version_icon_in_menu_ = true;
}
if (update_available_ && show_new_version_icon_in_menu_) {
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
bar_pos_y - title_bar_button_width * 0.24f));
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
ImGui::SetWindowFontScale(1.0f);
if (now_time - new_version_icon_render_start_time_ >=
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
}
}
ImGui::PopStyleColor(2);
{
SettingWindow();
SelfHostedServerWindow();
AboutWindow();
}
} else {
ImGui::SetWindowFontScale(1.2f);
}
ImGui::SetCursorPos(ImVec2(
title_bar_width - title_bar_button_width * (main_window ? 2 : 3), 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
float minimize_pos_x = title_bar_width -
title_bar_button_width * (main_window ? 2 : 3) +
title_bar_button_width * 0.33f;
float minimize_pos_y = title_bar_button_height * 0.5f;
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
if (main_window) {
last_main_minimize_request_tick_ = SDL_GetTicks();
} else {
last_stream_minimize_request_tick_ = SDL_GetTicks();
}
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
}
draw_list->AddLine(
ImVec2(minimize_pos_x, minimize_pos_y),
ImVec2(minimize_pos_x + title_bar_button_width * 0.33f, minimize_pos_y),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
if (!main_window) {
ImGui::SetCursorPos(
ImVec2(title_bar_width - title_bar_button_width * 2, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::SetCursorPosX(main_window
? (main_window_width_ - BUTTON_PADDING * 2)
: (stream_window_width_ - BUTTON_PADDING * 3));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
float minimize_pos_x = ImGui::GetCursorPosX() + 12;
float minimize_pos_y = ImGui::GetCursorPosY() + 15;
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(BUTTON_PADDING, 30))) {
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
}
draw_list->AddLine(ImVec2(minimize_pos_x, minimize_pos_y),
ImVec2(minimize_pos_x + 12, minimize_pos_y),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(2);
if (!main_window) {
ImGui::SetCursorPosX(stream_window_width_ - BUTTON_PADDING * 2);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
if (window_maximized_) {
float pos_x_top = ImGui::GetCursorPosX() + 11;
float pos_y_top = ImGui::GetCursorPosY() + 11;
float pos_x_bottom = ImGui::GetCursorPosX() + 13;
float pos_y_bottom = ImGui::GetCursorPosY() + 9;
std::string window_restore_button =
"##restore"; // ICON_FA_WINDOW_RESTORE;
if (ImGui::Button(window_restore_button.c_str(),
ImVec2(BUTTON_PADDING, 30))) {
SDL_RestoreWindow(stream_window_);
window_maximized_ = false;
}
draw_list->AddRect(ImVec2(pos_x_top, pos_y_top),
ImVec2(pos_x_top + 12, pos_y_top + 12),
IM_COL32(0, 0, 0, 255));
draw_list->AddRect(ImVec2(pos_x_bottom, pos_y_bottom),
ImVec2(pos_x_bottom + 12, pos_y_bottom + 12),
IM_COL32(0, 0, 0, 255));
draw_list->AddRectFilled(ImVec2(pos_x_top + 1, pos_y_top + 1),
ImVec2(pos_x_top + 11, pos_y_top + 11),
IM_COL32(255, 255, 255, 255));
} else {
float maximize_pos_x = ImGui::GetCursorPosX() + 12;
float maximize_pos_y = ImGui::GetCursorPosY() + 10;
std::string window_maximize_button =
"##maximize"; // ICON_FA_SQUARE_FULL;
if (ImGui::Button(window_maximize_button.c_str(),
ImVec2(BUTTON_PADDING, 30))) {
SDL_MaximizeWindow(stream_window_);
window_maximized_ = !window_maximized_;
}
draw_list->AddRect(ImVec2(maximize_pos_x, maximize_pos_y),
ImVec2(maximize_pos_x + 12, maximize_pos_y + 12),
IM_COL32(0, 0, 0, 255));
if (window_maximized_) {
float pos_x_top = title_bar_width - title_bar_button_width * 1.65f;
float pos_y_top = title_bar_button_height * 0.36f;
float pos_x_bottom = title_bar_width - title_bar_button_width * 1.6f;
float pos_y_bottom = title_bar_button_height * 0.28f;
std::string window_restore_button =
"##restore"; // ICON_FA_WINDOW_RESTORE;
if (ImGui::Button(
window_restore_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
SDL_RestoreWindow(stream_window_);
window_maximized_ = false;
}
ImGui::PopStyleColor(2);
}
ImGui::SetCursorPosX(
(main_window ? main_window_width_ : stream_window_width_) -
BUTTON_PADDING);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
float xmark_pos_x = ImGui::GetCursorPosX() + 18;
float xmark_pos_y = ImGui::GetCursorPosY() + 16;
float xmark_size = 12.0f;
std::string close_button = "##xmark"; // ICON_FA_XMARK;
if (ImGui::Button(close_button.c_str(), ImVec2(BUTTON_PADDING, 30))) {
#if _WIN32
if (enable_minimize_to_tray_) {
tray_->MinimizeToTray();
draw_list->AddRect(ImVec2(pos_x_top, pos_y_top),
ImVec2(pos_x_top + title_bar_button_height * 0.33f,
pos_y_top + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
draw_list->AddRect(ImVec2(pos_x_bottom, pos_y_bottom),
ImVec2(pos_x_bottom + title_bar_button_height * 0.33f,
pos_y_bottom + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
if (ImGui::IsItemHovered()) {
draw_list->AddRectFilled(
ImVec2(pos_x_top + title_bar_button_height * 0.02f,
pos_y_top + title_bar_button_height * 0.01f),
ImVec2(pos_x_top + title_bar_button_height * 0.32f,
pos_y_top + title_bar_button_height * 0.31f),
IM_COL32(229, 229, 229, 255));
} else {
#endif
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#if _WIN32
draw_list->AddRectFilled(
ImVec2(pos_x_top + title_bar_button_height * 0.02f,
pos_y_top + title_bar_button_height * 0.01f),
ImVec2(pos_x_top + title_bar_button_height * 0.32f,
pos_y_top + title_bar_button_height * 0.31f),
IM_COL32(255, 255, 255, 255));
}
#endif
} else {
float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f -
title_bar_button_height * 0.165f;
float maximize_pos_y = title_bar_button_height * 0.33f;
std::string window_maximize_button =
"##maximize"; // ICON_FA_SQUARE_FULL;
if (ImGui::Button(
window_maximize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
SDL_MaximizeWindow(stream_window_);
window_maximized_ = !window_maximized_;
}
draw_list->AddRect(
ImVec2(maximize_pos_x, maximize_pos_y),
ImVec2(maximize_pos_x + title_bar_button_height * 0.33f,
maximize_pos_y + title_bar_button_height * 0.33f),
IM_COL32(0, 0, 0, 255));
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
xmark_pos_y + xmark_size / 2 - 0.5f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x - xmark_size / 2,
xmark_pos_y + xmark_size / 2 - 1.0f),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(2);
ImGui::PopStyleColor();
ImGui::PopStyleColor(3);
}
ImGui::EndMenuBar();
float xmark_button_pos_x = title_bar_width - title_bar_button_width;
ImGui::SetCursorPos(ImVec2(xmark_button_pos_x, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0, 0, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0, 0, 0.5f));
float xmark_pos_x = xmark_button_pos_x + title_bar_button_width * 0.5f;
float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK;
bool close_button_clicked = false;
if (main_window) {
close_button_clicked = RoundedCornerButton(
close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height), 8.5f,
ImDrawFlags_RoundCornersTopRight, true, IM_COL32(0, 0, 0, 0),
IM_COL32(250, 0, 0, 255), IM_COL32(255, 0, 0, 128));
} else {
close_button_clicked =
ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height));
}
if (close_button_clicked) {
#if _WIN32
if (enable_minimize_to_tray_) {
tray_->MinimizeToTray();
} else {
#endif
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
#if _WIN32
}
#endif
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
xmark_pos_y + xmark_size / 2 - 0.5f),
IM_COL32(0, 0, 0, 255));
draw_list->AddLine(
ImVec2(xmark_pos_x + xmark_size / 2 - 1.75f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x - xmark_size / 2, xmark_pos_y + xmark_size / 2 - 1.0f),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
ImGui::EndChild();
ImGui::PopStyleColor();
return 0;
}
}
} // namespace crossdesk

View File

@@ -4,6 +4,8 @@
#include "localization.h"
namespace crossdesk {
// callback for the message-only window that handles tray icon messages
static LRESULT CALLBACK MsgWndProc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam) {
@@ -87,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
GetCursorPos(&pt);
HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 1001,
localization::exit_program[language_index_]);
localization::GetExitProgramLabel(language_index_));
SetForegroundWindow(hwnd_message_only_);
int cmd =
@@ -109,4 +111,5 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
}
}
return true;
}
}
} // namespace crossdesk

View File

@@ -14,6 +14,8 @@
#define WM_TRAY_CALLBACK (WM_USER + 1)
namespace crossdesk {
class WinTray {
public:
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
@@ -32,5 +34,5 @@ class WinTray {
int language_index_;
NOTIFYICONDATA nid_;
};
} // namespace crossdesk
#endif

View File

@@ -1,23 +1,89 @@
#include <cstdlib>
#include <string>
#if defined(_WIN32)
#include <windows.h>
#endif
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
bool Render::OpenUrl(const std::string& url) {
#if defined(_WIN32)
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len <= 0) {
return false;
}
std::wstring wide_url(static_cast<size_t>(wide_len), L'\0');
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wide_url[0], wide_len);
if (!wide_url.empty() && wide_url.back() == L'\0') {
wide_url.pop_back();
}
std::wstring cmd = L"cmd.exe /c start \"\" \"" + wide_url + L"\"";
STARTUPINFOW startup_info = {sizeof(startup_info)};
PROCESS_INFORMATION process_info = {};
if (!CreateProcessW(nullptr, &cmd[0], nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup_info,
&process_info)) {
return false;
}
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
return system(cmd.c_str()) == 0;
#else
std::string cmd = "xdg-open " + url;
return system(cmd.c_str()) == 0;
#endif
}
void Render::Hyperlink(const std::string& label, const std::string& url,
const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::TextUnformatted(label.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.6f);
ImGui::TextUnformatted(url.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
OpenUrl(url);
}
}
}
int Render::AboutWindow() {
if (show_about_window_) {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
float about_window_width = title_bar_button_width_ * 7.5f;
float about_window_height = latest_version_.empty()
? title_bar_button_width_ * 4.0f
: title_bar_button_width_ * 4.9f;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(
(viewport->WorkSize.x - viewport->WorkPos.x - about_window_width_) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height_) /
(viewport->WorkSize.x - viewport->WorkPos.x - about_window_width) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) /
2));
ImGui::SetNextWindowSize(ImVec2(about_window_width_, about_window_height_));
ImGui::SetNextWindowSize(ImVec2(about_window_width, about_window_height));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::SetWindowFontScale(0.5f);
ImGui::Begin(
localization::about[localization_language_index_].c_str(), nullptr,
@@ -34,17 +100,37 @@ int Render::AboutWindow() {
#endif
std::string text = localization::version[localization_language_index_] +
": CrossDesk v" + version;
": CrossDesk " + version;
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", text.c_str());
ImGui::Text("");
if (0) {
std::string new_version_available =
localization::new_version_available[localization_language_index_] +
": ";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", new_version_available.c_str());
std::string access_website =
localization::access_website[localization_language_index_];
ImGui::SetCursorPosX((about_window_width -
ImGui::CalcTextSize(latest_version_.c_str()).x) /
2.0f);
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
ImGui::Spacing();
} else {
ImGui::Text("%s", "");
}
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
std::string license_text = "Licensed under GNU LGPL v3.";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", copyright_text.c_str());
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", license_text.c_str());
ImGui::SetCursorPosX(about_window_width_ * 0.42f);
ImGui::SetCursorPosY(about_window_height_ * 0.75f);
ImGui::SetCursorPosX(about_window_width * 0.445f);
ImGui::SetCursorPosY(about_window_height * 0.8f);
// OK
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
show_about_window_ = false;
@@ -58,4 +144,5 @@ int Render::AboutWindow() {
ImGui::PopStyleColor();
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -3,44 +3,55 @@
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
bool Render::ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties> &props) {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
std::shared_ptr<SubStreamWindowProperties>& props) {
ImGuiIO& io = ImGui::GetIO();
bool ret_flag = false;
ImGui::SetNextWindowPos(ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
connection_status_window_width_) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
connection_status_window_height_) /
2));
ImGui::SetNextWindowSize(ImVec2(connection_status_window_width_,
connection_status_window_height_));
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto connection_status_window_width = ImGui::GetWindowSize().x;
auto connection_status_window_height = ImGui::GetWindowSize().y;
ImGui::SetWindowFontScale(0.5f);
std::string text;
if (ConnectionStatus::Connecting == props->connection_status_) {
text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == props->connection_status_) {
text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
@@ -49,8 +60,8 @@ bool Render::ConnectionStatusWindow(
}
} else if (ConnectionStatus::Disconnected == props->connection_status_) {
text = localization::p2p_disconnected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
@@ -59,8 +70,8 @@ bool Render::ConnectionStatusWindow(
}
} else if (ConnectionStatus::Failed == props->connection_status_) {
text = localization::p2p_failed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
@@ -69,8 +80,8 @@ bool Render::ConnectionStatusWindow(
}
} else if (ConnectionStatus::Closed == props->connection_status_) {
text = localization::p2p_closed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
@@ -85,11 +96,9 @@ bool Render::ConnectionStatusWindow(
text = localization::reinput_password[localization_language_index_];
}
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetCursorPosX((window_width - IPUT_WINDOW_WIDTH / 2) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH / 2);
ImGui::SetCursorPosX(connection_status_window_width * 0.336f);
ImGui::SetCursorPosY(connection_status_window_height * 0.4f);
ImGui::SetNextItemWidth(connection_status_window_width * 0.33f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
@@ -107,15 +116,16 @@ bool Render::ConnectionStatusWindow(
ImVec2 text_size = ImGui::CalcTextSize(
localization::remember_password[localization_language_index_]
.c_str());
ImGui::SetCursorPosX((window_width - text_size.x) * 0.5f - 13.0f);
ImGui::SetCursorPosX((connection_status_window_width - text_size.x) *
0.45f);
ImGui::Checkbox(
localization::remember_password[localization_language_index_].c_str(),
&(props->remember_password_));
ImGui::SetWindowFontScale(0.5f);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(window_width * 0.315f);
ImGui::SetCursorPosY(window_height * 0.75f);
ImGui::SetCursorPosX(connection_status_window_width * 0.325f);
ImGui::SetCursorPosY(connection_status_window_height * 0.75f);
// ok
if (ImGui::Button(
localization::ok[localization_language_index_].c_str()) ||
@@ -138,14 +148,14 @@ bool Render::ConnectionStatusWindow(
}
} else if (password_validating_) {
text = localization::validate_password[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
}
} else if (ConnectionStatus::NoSuchTransmissionId ==
props->connection_status_) {
text = localization::no_such_id[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 3 / 7);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// ok
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
@@ -156,11 +166,9 @@ bool Render::ConnectionStatusWindow(
}
}
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::SetCursorPosX((connection_status_window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(connection_status_window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
@@ -168,4 +176,5 @@ bool Render::ConnectionStatusWindow(
ImGui::PopStyleVar();
return ret_flag;
}
}
} // namespace crossdesk

View File

@@ -1,7 +1,9 @@
#include "rd_log.h"
#include "render.h"
int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
namespace crossdesk {
int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
double time_duration =
ImGui::GetTime() - props->control_bar_button_pressed_time_;
if (props->control_window_width_is_changing_) {
@@ -40,50 +42,74 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
}
}
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1, 1, 1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 10.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowSize(
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiCond_Always);
float y_boundary = fullscreen_button_pressed_ ? 0.0f : title_bar_height_;
float container_x = 0.0f;
float container_y = y_boundary;
float container_w = stream_window_width_;
float container_h = stream_window_height_ - y_boundary;
ImGui::SetNextWindowPos(ImVec2(0, title_bar_height_ + 1), ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(container_w, container_h), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(container_x, container_y), ImGuiCond_Always);
float pos_x = 0;
float pos_y = 0;
float y_boundary = fullscreen_button_pressed_ ? 0 : (title_bar_height_ + 1);
if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0;
float new_cursor_pos_y = 0;
std::string container_window_title =
props->remote_id_ + "ControlContainerWindow";
ImGui::Begin(container_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBackground);
ImGui::PopStyleVar();
ImVec2 container_pos = ImGui::GetWindowPos();
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
props->control_bar_hovered_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
ImVec2 delta = ImGui::GetIO().MouseDelta;
pos_x = current_x_rel + delta.x;
pos_y = current_y_rel + delta.y;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
} else if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0.0f;
float new_cursor_pos_y = 0.0f;
// set control window pos
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
} else if (props->control_window_pos_.y < y_boundary) {
pos_y = y_boundary;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
} else {
pos_y = props->control_window_pos_.y;
pos_y = current_y_rel;
}
if (props->is_control_bar_in_left_) {
pos_x = 0;
pos_x = 0.0f;
} else {
pos_x = stream_window_width_ - props->control_window_width_;
pos_x = container_w - props->control_window_width_;
}
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
if (0 != props->mouse_diff_control_bar_pos_x_ &&
0 != props->mouse_diff_control_bar_pos_y_) {
// set cursor pos
new_cursor_pos_x = pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y = pos_y + props->mouse_diff_control_bar_pos_y_;
new_cursor_pos_x =
container_pos.x + pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y =
container_pos.y + pos_y + props->mouse_diff_control_bar_pos_y_;
SDL_WarpMouseInWindow(stream_window_, (int)new_cursor_pos_x,
(int)new_cursor_pos_y);
@@ -92,12 +118,14 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
} else if (!props->reset_control_bar_pos_ &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
props->control_window_width_is_changing_) {
if (props->control_window_pos_.x <= stream_window_width_ / 2) {
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_x_rel <= container_w * 0.5f) {
pos_x = 0.0f;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else {
pos_y = props->control_window_pos_.y;
pos_y = current_y_rel;
}
if (props->control_bar_expand_) {
@@ -116,49 +144,53 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
}
}
props->is_control_bar_in_left_ = true;
} else if (props->control_window_pos_.x > stream_window_width_ / 2) {
pos_x = 0;
pos_y =
(props->control_window_pos_.y >= y_boundary &&
props->control_window_pos_.y <=
stream_window_height_ - props->control_window_height_)
? props->control_window_pos_.y
: (props->control_window_pos_.y < (fullscreen_button_pressed_
? 0
: (title_bar_height_ + 1))
? (fullscreen_button_pressed_ ? 0
: (title_bar_height_ + 1))
: (stream_window_height_ - props->control_window_height_));
} else if (current_x_rel > container_w * 0.5f) {
pos_x = container_w - props->control_window_width_;
pos_y = (current_y_rel >= 0.0f &&
current_y_rel <= container_h - props->control_window_height_)
? current_y_rel
: (current_y_rel < 0.0f
? 0.0f
: (container_h - props->control_window_height_));
if (props->control_bar_expand_) {
if (props->control_window_width_ >= props->control_window_max_width_) {
props->control_window_width_ = props->control_window_max_width_;
props->control_window_width_is_changing_ = false;
pos_x = stream_window_width_ - props->control_window_max_width_;
pos_x = container_w - props->control_window_max_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = stream_window_width_ - props->control_window_width_;
pos_x = container_w - props->control_window_width_;
}
} else {
if (props->control_window_width_ <= props->control_window_min_width_) {
props->control_window_width_ = props->control_window_min_width_;
props->control_window_width_is_changing_ = false;
pos_x = stream_window_width_ - props->control_window_min_width_;
pos_x = container_w - props->control_window_min_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = stream_window_width_ - props->control_window_width_;
pos_x = container_w - props->control_window_width_;
}
}
props->is_control_bar_in_left_ = false;
}
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
} else if (props->control_window_pos_.y < y_boundary) {
pos_y = y_boundary;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
}
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
} else {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
pos_x = current_x_rel;
pos_y = current_y_rel;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
}
if (props->control_bar_expand_ && props->control_window_height_is_changing_) {
@@ -180,10 +212,20 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
}
std::string control_window_title = props->remote_id_ + "ControlWindow";
ImGui::Begin(control_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar();
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
float child_cursor_x = pos_x;
float child_cursor_y = pos_y;
ImGui::SetCursorPos(ImVec2(child_cursor_x, child_cursor_y));
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(
control_child_window_title.c_str(),
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
props->control_window_pos_ = ImGui::GetWindowPos();
SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_);
@@ -192,32 +234,27 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties> &props) {
props->mouse_diff_control_bar_pos_y_ =
props->mouse_pos_y_ - props->control_window_pos_.y;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
ImGui::SetNextWindowPos(
ImVec2(props->is_control_bar_in_left_
? props->control_window_pos_.x - props->control_window_width_
: props->control_window_pos_.x,
props->control_window_pos_.y),
ImGuiCond_Always);
ImGui::SetWindowFontScale(0.5f);
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(
control_child_window_title.c_str(),
ImVec2(props->control_window_width_ * 2, props->control_window_height_),
ImGuiChildFlags_Border, ImGuiWindowFlags_NoDecoration);
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
if (props->control_window_pos_.y < container_pos.y ||
props->control_window_pos_.y + props->control_window_height_ >
(container_pos.y + container_h) ||
props->control_window_pos_.x < container_pos.x ||
props->control_window_pos_.x + props->control_window_width_ >
(container_pos.x + container_w)) {
ImGui::ClearActiveID();
props->reset_control_bar_pos_ = true;
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
ControlBar(props);
props->control_bar_hovered_ = ImGui::IsWindowHovered();
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleVar(4);
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
return 0;
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,245 @@
#include <algorithm>
#include <cmath>
#include "IconsFontAwesome6.h"
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
namespace {
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
int BitrateDisplay(int bitrate) {
int num_of_digits = CountDigits(bitrate);
if (num_of_digits <= 3) {
ImGui::Text("%d bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%d kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
return 0;
}
} // namespace
int Render::FileTransferWindow(
std::shared_ptr<SubStreamWindowProperties>& props) {
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
// Only show window if there are files in transfer list or currently
// transferring
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
file_list = state->file_transfer_list_;
}
// Sort file list: Sending first, then Completed, then Queued, then Failed
std::sort(
file_list.begin(), file_list.end(),
[](const SubStreamWindowProperties::FileTransferInfo& a,
const SubStreamWindowProperties::FileTransferInfo& b) {
// Priority: Sending > Completed > Queued > Failed
auto get_priority =
[](SubStreamWindowProperties::FileTransferStatus status) {
switch (status) {
case SubStreamWindowProperties::FileTransferStatus::Sending:
return 0;
case SubStreamWindowProperties::FileTransferStatus::Completed:
return 1;
case SubStreamWindowProperties::FileTransferStatus::Queued:
return 2;
case SubStreamWindowProperties::FileTransferStatus::Failed:
return 3;
}
return 3;
};
return get_priority(a.status) < get_priority(b.status);
});
// Only show window if file_transfer_window_visible_ is true
// Window can be closed by user even during transfer
// It will be reopened automatically when:
// 1. A file transfer completes (in render_callback.cpp)
// 2. A new file starts sending from queue (in render.cpp)
if (!state->file_transfer_window_visible_) {
return 0;
}
ImGuiIO& io = ImGui::GetIO();
// Position window at bottom-left of stream window
// Adjust window size based on number of files
float file_transfer_window_width = main_window_width_ * 0.6f;
float file_transfer_window_height =
main_window_height_ * 0.3f; // Dynamic height
float pos_x = file_transfer_window_width * 0.05f;
float pos_y = stream_window_height_ - file_transfer_window_height -
file_transfer_window_width * 0.05;
float same_line_width = file_transfer_window_width * 0.1f;
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
ImGui::SetNextWindowSize(
ImVec2(file_transfer_window_width, file_transfer_window_height),
ImGuiCond_Always);
// Set Chinese font for proper display
if (stream_windows_system_chinese_font_) {
ImGui::PushFont(stream_windows_system_chinese_font_);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f);
bool window_opened = true;
if (ImGui::Begin(
localization::file_transfer_progress[localization_language_index_]
.c_str(),
&window_opened,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoScrollbar)) {
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);
// Close button handling
if (!window_opened) {
state->file_transfer_window_visible_ = false;
ImGui::End();
return 0;
}
// Display file list
if (file_list.empty()) {
ImGui::Text("No files in transfer queue");
} else {
// Use a scrollable child window for the file list
ImGui::SetWindowFontScale(0.5f);
ImGui::BeginChild(
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
for (size_t i = 0; i < file_list.size(); ++i) {
const auto& info = file_list[i];
ImGui::PushID(static_cast<int>(i));
// Status icon and file name
const char* status_icon = "";
ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f);
const char* status_text = "";
switch (info.status) {
case SubStreamWindowProperties::FileTransferStatus::Queued:
status_icon = ICON_FA_CLOCK;
status_color =
ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued
status_text =
localization::queued[localization_language_index_].c_str();
break;
case SubStreamWindowProperties::FileTransferStatus::Sending:
status_icon = ICON_FA_ARROW_UP;
status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f);
status_text =
localization::sending[localization_language_index_].c_str();
break;
case SubStreamWindowProperties::FileTransferStatus::Completed:
status_icon = ICON_FA_CHECK;
status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f);
status_text =
localization::completed[localization_language_index_].c_str();
break;
case SubStreamWindowProperties::FileTransferStatus::Failed:
status_icon = ICON_FA_XMARK;
status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
status_text =
localization::failed[localization_language_index_].c_str();
break;
}
ImGui::TextColored(status_color, "%s", status_icon);
ImGui::SameLine();
ImGui::Text("%s", info.file_name.c_str());
ImGui::SameLine();
ImGui::TextColored(status_color, "%s", status_text);
// Progress bar for sending files
if (info.status ==
SubStreamWindowProperties::FileTransferStatus::Sending &&
info.file_size > 0) {
float progress = static_cast<float>(info.sent_bytes) /
static_cast<float>(info.file_size);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
float text_height = ImGui::GetTextLineHeight();
ImGui::ProgressBar(
progress, ImVec2(file_transfer_window_width * 0.5f, text_height),
"");
ImGui::SameLine();
ImGui::Text("%.1f%%", progress * 100.0f);
ImGui::SameLine();
float speed_x_pos = file_transfer_window_width * 0.65f;
ImGui::SetCursorPosX(speed_x_pos);
BitrateDisplay(static_cast<int>(info.rate_bps));
} else if (info.status ==
SubStreamWindowProperties::FileTransferStatus::Completed) {
// Show completed size
char size_str[64];
if (info.file_size < 1024) {
snprintf(size_str, sizeof(size_str), "%llu B",
(unsigned long long)info.file_size);
} else if (info.file_size < 1024 * 1024) {
snprintf(size_str, sizeof(size_str), "%.2f KB",
info.file_size / 1024.0f);
} else {
snprintf(size_str, sizeof(size_str), "%.2f MB",
info.file_size / (1024.0f * 1024.0f));
}
ImGui::Text("Size: %s", size_str);
}
ImGui::PopID();
ImGui::Spacing();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::EndChild();
ImGui::SetWindowFontScale(1.0f);
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
// Pop Chinese font if it was pushed
if (stream_windows_system_chinese_font_) {
ImGui::PopFont();
}
} else {
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);
}
ImGui::SetWindowFontScale(1.0f);
return 0;
}
} // namespace crossdesk

View File

@@ -2,33 +2,43 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h"
namespace crossdesk {
int Render::SettingWindow() {
ImGuiIO& io = ImGui::GetIO();
if (show_settings_window_) {
if (settings_window_pos_reset_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
SETTINGS_WINDOW_WIDTH_CN) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
SETTINGS_WINDOW_HEIGHT_CN) /
2));
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(SETTINGS_WINDOW_WIDTH_CN, SETTINGS_WINDOW_HEIGHT_CN));
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
#endif
} else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
SETTINGS_WINDOW_WIDTH_EN) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
SETTINGS_WINDOW_HEIGHT_EN) /
2));
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(SETTINGS_WINDOW_WIDTH_EN, SETTINGS_WINDOW_HEIGHT_EN));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
#endif
}
settings_window_pos_reset_ = false;
@@ -36,40 +46,58 @@ int Render::SettingWindow() {
// Settings
{
static int settings_items_padding = 30;
static int settings_items_padding = title_bar_button_width_ * 0.75f;
int settings_items_offset = 0;
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::settings[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{
const char* language_items[] = {
localization::language_zh[localization_language_index_].c_str(),
localization::language_en[localization_language_index_].c_str()};
const auto& supported_languages = localization::GetSupportedLanguages();
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::language[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(LANGUAGE_SELECT_WINDOW_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(LANGUAGE_SELECT_WINDOW_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SETTINGS_SELECT_WINDOW_WIDTH);
ImGui::Combo("##language", &language_button_value_, language_items,
IM_ARRAYSIZE(language_items));
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##language",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
language_button_value_)]
.display_name
.c_str())) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) {
bool selected = (i == language_button_value_);
if (ImGui::Selectable(
supported_languages[i].display_name.c_str(), selected))
language_button_value_ = i;
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
}
ImGui::Separator();
@@ -80,29 +108,39 @@ int Render::SettingWindow() {
{
const char* video_quality_items[] = {
localization::video_quality_high[localization_language_index_]
localization::video_quality_low[localization_language_index_]
.c_str(),
localization::video_quality_medium[localization_language_index_]
.c_str(),
localization::video_quality_low[localization_language_index_]
localization::video_quality_high[localization_language_index_]
.c_str()};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::video_quality[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SETTINGS_SELECT_WINDOW_WIDTH);
ImGui::Combo("##video_quality", &video_quality_button_value_,
video_quality_items, IM_ARRAYSIZE(video_quality_items));
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_quality",
video_quality_items[video_quality_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_quality_items); i++) {
bool selected = (i == video_quality_button_value_);
if (ImGui::Selectable(video_quality_items[i], selected))
video_quality_button_value_ = i;
}
ImGui::EndCombo();
}
}
ImGui::Separator();
@@ -111,22 +149,31 @@ int Render::SettingWindow() {
const char* video_frame_rate_items[] = {"30 fps", "60 fps"};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::video_frame_rate[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SETTINGS_SELECT_WINDOW_WIDTH);
ImGui::Combo("##video_frame_rate", &video_frame_rate_button_value_,
video_frame_rate_items,
IM_ARRAYSIZE(video_frame_rate_items));
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_frame_rate",
video_frame_rate_items[video_frame_rate_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_frame_rate_items); i++) {
bool selected = (i == video_frame_rate_button_value_);
if (ImGui::Selectable(video_frame_rate_items[i], selected))
video_frame_rate_button_value_ = i;
}
ImGui::EndCombo();
}
}
ImGui::Separator();
@@ -137,59 +184,74 @@ int Render::SettingWindow() {
localization::av1[localization_language_index_].c_str()};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::video_encode_format[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else {
ImGui::SetCursorPosX(VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SETTINGS_SELECT_WINDOW_WIDTH);
ImGui::Combo(
"##video_encode_format", &video_encode_format_button_value_,
video_encode_format_items, IM_ARRAYSIZE(video_encode_format_items));
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##video_encode_format",
video_encode_format_items[video_encode_format_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < IM_ARRAYSIZE(video_encode_format_items); i++) {
bool selected = (i == video_encode_format_button_value_);
if (ImGui::Selectable(video_encode_format_items[i], selected))
video_encode_format_button_value_ = i;
}
ImGui::EndCombo();
}
}
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::enable_hardware_video_codec
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_hardware_video_codec",
&enable_hardware_video_codec_);
}
#endif
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_turn[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_TURN_CHECKBOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(ENABLE_TURN_CHECKBOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_turn", &enable_turn_);
}
@@ -197,17 +259,18 @@ int Render::SettingWindow() {
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_srtp[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_SRTP_CHECKBOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(ENABLE_SRTP_CHECKBOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
}
@@ -216,55 +279,166 @@ int Render::SettingWindow() {
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 1);
ImGui::AlignTextToFramePadding();
if (ImGui::Button(localization::self_hosted_server_config
[localization_language_index_]
.c_str())) {
show_self_hosted_server_config_window_ = true;
}
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_self_hosted_server",
&enable_self_hosted_server_);
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
}
#if _WIN32
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::enable_autostart[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::enable_daemon[localization_language_index_].c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", localization::takes_effect_after_restart
[localization_language_index_]
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
ImGui::Separator();
{
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::minimize_to_tray[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_MINIZE_TO_TRAY_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else {
ImGui::SetCursorPosX(ENABLE_MINIZE_TO_TRAY_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_);
}
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#endif
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::file_transfer_save_path[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
}
std::string display_path =
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: localization::default_desktop[localization_language_index_];
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.95f, 0.95f, 0.95f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.9f, 0.9f, 0.9f, 1.0f));
ImGui::PushFont(main_windows_system_chinese_font_);
if (ImGui::Button(display_path.c_str(),
ImVec2(title_bar_button_width_ * 2.0f, 0))) {
const char* folder =
tinyfd_selectFolderDialog(localization::file_transfer_save_path
[localization_language_index_]
.c_str(),
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: nullptr);
if (folder) {
strncpy(file_transfer_save_path_buf_, folder,
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) -
1] = '\0';
}
}
if (ImGui::IsItemHovered() &&
strlen(file_transfer_save_path_buf_) > 0) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", file_transfer_save_path_buf_);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::PopFont();
ImGui::PopStyleColor(3);
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(SETTINGS_OK_BUTTON_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
} else {
ImGui::SetCursorPosX(SETTINGS_OK_BUTTON_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.22f);
}
settings_items_offset += settings_items_padding + 10;
settings_items_offset +=
settings_items_padding + title_bar_button_width_ * 0.3f;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar();
// OK
@@ -274,27 +448,44 @@ int Render::SettingWindow() {
show_self_hosted_server_config_window_ = false;
// Language
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
if (language_button_value_ == 0) {
config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE);
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (language_button_value_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
} else {
config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH);
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
}
config_center_->SetLanguage(localization_language_);
language_button_value_last_ = language_button_value_;
localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_;
localization_language_index_ = language_button_value_;
LOG_INFO("Set localization language: {}",
localization_language_index_ == 0 ? "zh" : "en");
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
localization_language_index_)]
.code
.c_str());
// Video quality
if (video_quality_button_value_ == 0) {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::HIGH);
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::LOW);
} else if (video_quality_button_value_ == 1) {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::MEDIUM);
} else {
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::LOW);
config_center_->SetVideoQuality(ConfigCenter::VIDEO_QUALITY::HIGH);
}
video_quality_button_value_last_ = video_quality_button_value_;
if (video_frame_rate_button_value_ == 0) {
config_center_->SetVideoFrameRate(
ConfigCenter::VIDEO_FRAME_RATE::FPS_30);
} else if (video_frame_rate_button_value_ == 1) {
config_center_->SetVideoFrameRate(
ConfigCenter::VIDEO_FRAME_RATE::FPS_60);
}
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
// Video encode format
if (video_encode_format_button_value_ == 0) {
config_center_->SetVideoEncodeFormat(
@@ -330,11 +521,39 @@ int Render::SettingWindow() {
}
enable_srtp_last_ = enable_srtp_;
if (enable_self_hosted_server_) {
if (enable_self_hosted_) {
config_center_->SetSelfHosted(true);
} else {
config_center_->SetSelfHosted(false);
}
enable_self_hosted_last_ = enable_self_hosted_;
if (enable_autostart_) {
config_center_->SetAutostart(true);
} else {
config_center_->SetAutostart(false);
}
enable_autostart_last_ = enable_autostart_;
if (enable_daemon_) {
config_center_->SetDaemon(true);
} else {
config_center_->SetDaemon(false);
}
enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true);
} else {
config_center_->SetMinimizeToTray(false);
}
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
settings_window_pos_reset_ = true;
@@ -364,6 +583,11 @@ int Render::SettingWindow() {
video_quality_button_value_ = video_quality_button_value_last_;
}
if (video_frame_rate_button_value_ !=
video_frame_rate_button_value_last_) {
video_frame_rate_button_value_ = video_frame_rate_button_value_last_;
}
if (video_encode_format_button_value_ !=
video_encode_format_button_value_last_) {
video_encode_format_button_value_ =
@@ -378,16 +602,22 @@ int Render::SettingWindow() {
enable_turn_ = enable_turn_last_;
}
// Restore file transfer save path
strncpy(file_transfer_save_path_buf_,
file_transfer_save_path_last_.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
settings_window_pos_reset_ = true;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
ImGui::SetWindowFontScale(1.0f);
}
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -1,27 +1,36 @@
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
int Render::MainWindow() {
ImGui::SetNextWindowPos(ImVec2(0, title_bar_height_), ImGuiCond_Always);
ImGuiIO& io = ImGui::GetIO();
float local_remote_window_width = io.DisplaySize.x;
float local_remote_window_height =
io.DisplaySize.y * (0.56f - TITLE_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild("DeskWindow",
ImVec2(main_window_width_default_, local_window_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild(
"DeskWindow",
ImVec2(local_remote_window_width, local_remote_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
ImGui::PopStyleColor(2);
LocalWindow();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddLine(
ImVec2(main_window_width_default_ / 2, title_bar_height_ + 15.0f),
ImVec2(main_window_width_default_ / 2, title_bar_height_ + 225.0f),
IM_COL32(0, 0, 0, 122), 1.0f);
draw_list->AddLine(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.1f),
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.53f),
IM_COL32(0, 0, 0, 122), 1.0f);
RemoteWindow();
ImGui::EndChild();
@@ -30,6 +39,7 @@ int Render::MainWindow() {
StatusBar();
if (show_connection_status_window_) {
// std::unique_lock lock(client_properties_mutex_);
for (auto it = client_properties_.begin();
it != client_properties_.end();) {
auto& props = it->second;
@@ -47,3 +57,4 @@ int Render::MainWindow() {
return 0;
}
} // namespace crossdesk

View File

@@ -0,0 +1,205 @@
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#include <unistd.h>
#include <cstdlib>
namespace crossdesk {
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f;
const float KNOB_PADDING = 2.0f;
const float KNOB_HEIGHT = TRACK_HEIGHT - 4.0f;
const float KNOB_WIDTH = KNOB_HEIGHT * 1.2f;
const float KNOB_RADIUS = KNOB_HEIGHT * 0.5f;
const float DISABLED_ALPHA = 0.6f;
const float KNOB_ALPHA_DISABLED = 0.9f;
const ImVec4 COLOR_ACTIVE = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
const ImVec4 COLOR_ACTIVE_HOVER = ImVec4(0.26f, 0.59f, 0.98f, 1.0f);
const ImVec4 COLOR_INACTIVE = ImVec4(0.60f, 0.60f, 0.60f, 1.0f);
const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 1.0f);
const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 track_pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT));
bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked() && enabled;
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
if (!enabled) {
track_color.w *= DISABLED_ALPHA;
}
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f);
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS);
float knob_position = active ? 1.0f : 0.0f;
float knob_min_x = track_pos.x + KNOB_PADDING;
float knob_max_x = track_pos.x + TRACK_WIDTH - KNOB_WIDTH - KNOB_PADDING;
float knob_x = knob_min_x + knob_position * (knob_max_x - knob_min_x);
float knob_y = track_pos.y + (TRACK_HEIGHT - KNOB_HEIGHT) * 0.5f;
ImVec4 knob_color = COLOR_KNOB;
if (!enabled) {
knob_color.w = KNOB_ALPHA_DISABLED;
}
ImVec2 knob_min = ImVec2(knob_x, knob_y);
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color), KNOB_RADIUS);
return clicked;
}
bool Render::CheckScreenRecordingPermission() {
// CGPreflightScreenCaptureAccess is available on macOS 10.15+
if (@available(macOS 10.15, *)) {
bool granted = CGPreflightScreenCaptureAccess();
return granted;
}
// for older macOS versions, assume permission is granted
return true;
}
bool Render::CheckAccessibilityPermission() {
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO};
bool trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
return trusted;
}
void Render::OpenAccessibilityPreferences() {
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
system("open "
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
"Accessibility\"");
}
void Render::OpenScreenRecordingPreferences() {
if (@available(macOS 10.15, *)) {
CGRequestScreenCaptureAccess();
}
system("open "
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
"ScreenCapture\"");
}
int Render::RequestPermissionWindow() {
bool screen_recording_granted = CheckScreenRecordingPermission();
bool accessibility_granted = CheckAccessibilityPermission();
show_request_permission_window_ = !screen_recording_granted || !accessibility_granted;
if (!show_request_permission_window_) {
return 0;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
float window_width = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_WIDTH_CN
: REQUEST_PERMISSION_WINDOW_WIDTH_EN;
float window_height = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN
: REQUEST_PERMISSION_WINDOW_HEIGHT_EN;
float checkbox_padding = localization_language_index_ == 0
? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN
: REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN;
ImVec2 center_pos = ImVec2((viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x,
(viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y);
ImGui::SetNextWindowPos(center_pos, ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin(
localization::request_permissions[localization_language_index_].c_str(), nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(0.3f);
// use system font
if (main_windows_system_chinese_font_ != nullptr) {
ImGui::PushFont(main_windows_system_chinese_font_);
}
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() + 5.0f);
ImGui::SetCursorPosX(10.0f);
ImGui::TextWrapped(
"%s", localization::permission_required_message[localization_language_index_].c_str());
ImGui::Spacing();
ImGui::Spacing();
ImGui::Spacing();
// accessibility permission
ImGui::SetCursorPosX(10.0f);
ImGui::AlignTextToFramePadding();
ImGui::Text("1. %s:",
localization::accessibility_permission[localization_language_index_].c_str());
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::SetCursorPosX(checkbox_padding);
if (accessibility_granted) {
DrawToggleSwitch("accessibility_toggle_on", true, false);
} else {
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) {
OpenAccessibilityPreferences();
}
}
ImGui::Spacing();
// screen recording permission
ImGui::SetCursorPosX(10.0f);
ImGui::AlignTextToFramePadding();
ImGui::Text("2. %s:",
localization::screen_recording_permission[localization_language_index_].c_str());
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::SetCursorPosX(checkbox_padding);
if (screen_recording_granted) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
DrawToggleSwitch("screen_recording_toggle_on", true, false);
} else {
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted,
!screen_recording_granted)) {
OpenScreenRecordingPreferences();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.45f);
// pop system font
if (main_windows_system_chinese_font_ != nullptr) {
ImGui::PopFont();
}
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleVar(4);
ImGui::PopStyleColor();
return 0;
}
} // namespace crossdesk

View File

@@ -10,6 +10,8 @@
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
std::vector<std::string> GetRootEntries() {
std::vector<std::string> roots;
#ifdef _WIN32
@@ -26,99 +28,20 @@ std::vector<std::string> GetRootEntries() {
return roots;
}
int Render::ShowSimpleFileBrowser() {
std::string display_text;
if (!selected_file_.empty()) {
display_text = std::filesystem::path(selected_file_).filename().string();
} else if (selected_current_file_path_ != "Root") {
display_text =
std::filesystem::path(selected_current_file_path_).filename().string();
if (display_text.empty()) {
display_text = selected_current_file_path_;
}
}
if (display_text.empty()) {
display_text =
localization::select_a_file[localization_language_index_].c_str();
}
if (ImGui::BeginCombo("##select_a_file", display_text.c_str())) {
if (selected_current_file_path_ == "Root" ||
!std::filesystem::exists(selected_current_file_path_) ||
!std::filesystem::is_directory(selected_current_file_path_)) {
auto roots = GetRootEntries();
for (const auto& root : roots) {
if (ImGui::Selectable(root.c_str())) {
selected_current_file_path_ = root;
selected_file_.clear();
}
}
} else {
std::filesystem::path p(selected_current_file_path_);
if (ImGui::Selectable("..")) {
if (p.has_parent_path() && p != p.root_path())
selected_current_file_path_ = p.parent_path().string();
else
selected_current_file_path_ = "Root";
selected_file_.clear();
}
try {
for (const auto& entry :
std::filesystem::directory_iterator(selected_current_file_path_)) {
std::string name = entry.path().filename().string();
if (entry.is_directory()) {
if (ImGui::Selectable(name.c_str())) {
selected_current_file_path_ = entry.path().string();
selected_file_.clear();
}
} else {
if (ImGui::Selectable(name.c_str())) {
selected_file_ = entry.path().string();
}
}
}
} catch (const std::exception& e) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: %s", e.what());
}
}
ImGui::EndCombo();
}
return 0;
}
int Render::SelfHostedServerWindow() {
ImGuiIO& io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) {
if (self_hosted_server_config_window_pos_reset_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN) /
2));
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
ImGui::SetNextWindowSize(
ImVec2(SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN,
SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
} else {
ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN) /
2));
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
ImGui::SetNextWindowSize(
ImVec2(SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN,
SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN));
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
}
self_hosted_server_config_window_pos_reset_ = false;
@@ -126,13 +49,10 @@ int Render::SelfHostedServerWindow() {
// Settings
{
static int settings_items_padding = 30;
int settings_items_offset = 0;
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(localization::self_hosted_server_settings
[localization_language_index_]
@@ -144,78 +64,74 @@ int Render::SelfHostedServerWindow() {
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 2);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::self_hosted_server_address
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ImGui::InputText("##signal_server_ip_tmp_", signal_server_ip_tmp_,
IM_ARRAYSIZE(signal_server_ip_tmp_),
ImGui::InputText("##signal_server_ip_self_", signal_server_ip_self_,
IM_ARRAYSIZE(signal_server_ip_self_),
ImGuiInputTextFlags_AlwaysOverwrite);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 2);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::self_hosted_server_port[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ImGui::InputText("##signal_server_port_tmp_", signal_server_port_tmp_,
IM_ARRAYSIZE(signal_server_port_tmp_));
ImGui::InputText("##signal_server_port_self_", signal_server_port_self_,
IM_ARRAYSIZE(signal_server_port_self_));
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 2);
ImGui::Text("%s", localization::self_hosted_server_certificate_path
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::self_hosted_server_coturn_server_port
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
} else {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::SetNextItemWidth(SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH);
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
ShowSimpleFileBrowser();
ImGui::InputText("##coturn_server_port_self_", coturn_server_port_self_,
IM_ARRAYSIZE(coturn_server_port_self_));
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
ImGui::Dummy(ImVec2(0.0f, title_bar_button_width_ * 0.25f));
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.32f);
} else {
ImGui::SetCursorPosX(SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN);
ImGui::SetCursorPosX(title_bar_button_width_ * 2.7f);
}
settings_items_offset += settings_items_padding + 10;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar();
// OK
@@ -223,18 +139,18 @@ int Render::SelfHostedServerWindow() {
localization::ok[localization_language_index_].c_str())) {
show_self_hosted_server_config_window_ = false;
config_center_->SetServerHost(signal_server_ip_tmp_);
config_center_->SetServerPort(atoi(signal_server_port_tmp_));
config_center_->SetCertFilePath(selected_file_);
strncpy(signal_server_ip_, signal_server_ip_tmp_,
config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_server_port_self_));
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
strncpy(signal_server_ip_, signal_server_ip_self_,
sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
strncpy(signal_server_port_, signal_server_port_tmp_,
strncpy(signal_server_port_, signal_server_port_self_,
sizeof(signal_server_port_) - 1);
signal_server_port_[sizeof(signal_server_port_) - 1] = '\0';
strncpy(cert_file_path_, selected_file_.c_str(),
sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1);
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
self_hosted_server_config_window_pos_reset_ = true;
}
@@ -245,16 +161,26 @@ int Render::SelfHostedServerWindow() {
localization::cancel[localization_language_index_].c_str())) {
show_self_hosted_server_config_window_ = false;
self_hosted_server_config_window_pos_reset_ = true;
strncpy(signal_server_ip_tmp_, signal_server_ip_,
sizeof(signal_server_ip_tmp_) - 1);
signal_server_ip_tmp_[sizeof(signal_server_ip_tmp_) - 1] = '\0';
strncpy(signal_server_port_tmp_, signal_server_port_,
sizeof(signal_server_port_tmp_) - 1);
signal_server_port_tmp_[sizeof(signal_server_port_tmp_) - 1] = '\0';
config_center_->SetServerHost(signal_server_ip_tmp_);
config_center_->SetServerPort(atoi(signal_server_port_tmp_));
selected_file_.clear();
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
int signal_port = config_center_->GetSignalServerPort();
if (signal_port > 0) {
strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(),
sizeof(signal_server_port_self_) - 1);
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
} else {
signal_server_port_self_[0] = '\0';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_server_port_self_[0] = '\0';
}
}
ImGui::SetWindowFontScale(1.0f);
@@ -267,4 +193,5 @@ int Render::SelfHostedServerWindow() {
}
return 0;
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,379 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
namespace crossdesk {
namespace {
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
void BitrateDisplay(uint32_t bitrate) {
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
if (num_of_digits <= 3) {
ImGui::Text("%u bps", bitrate);
} else if (num_of_digits > 3 && num_of_digits <= 6) {
ImGui::Text("%u kbps", bitrate / 1000);
} else {
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
}
}
std::string FormatBytes(uint64_t bytes) {
char buf[64];
if (bytes < 1024ULL) {
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
} else if (bytes < 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
} else {
std::snprintf(buf, sizeof(buf), "%.2f GB",
bytes / (1024.0 * 1024.0 * 1024.0));
}
return std::string(buf);
}
} // namespace
int Render::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
server_window_title_bar_height_ = title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(
"ServerTitleBar",
ImVec2(server_window_width_, server_window_title_bar_height_),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
float server_title_bar_button_width = server_window_title_bar_height_;
float server_title_bar_button_height = server_window_title_bar_height_;
// Collapse/expand toggle button (FontAwesome icon).
{
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f);
const char* icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle";
bool toggle_clicked = RoundedCornerButton(
toggle_label.c_str(),
ImVec2(server_title_bar_button_width, server_title_bar_button_height),
8.5f, ImDrawFlags_RoundCornersTopLeft, true, IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 25), IM_COL32(255, 255, 255, 255));
if (toggle_clicked) {
if (server_window_) {
int w = 0;
int h = 0;
int x = 0;
int y = 0;
SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowPosition(server_window_, &x, &y);
if (server_window_collapsed_) {
const int normal_h = server_window_normal_height_;
SDL_SetWindowSize(server_window_, w, normal_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = false;
} else {
const int collapsed_h = (int)server_window_title_bar_height_;
// Collapse upward: keep top edge stable.
SDL_SetWindowSize(server_window_, w, collapsed_h);
SDL_SetWindowPosition(server_window_, x, y);
server_window_collapsed_ = true;
}
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
RemoteClientInfoWindow();
ImGui::End();
return 0;
}
int Render::RemoteClientInfoWindow() {
float remote_client_info_window_width = server_window_width_ * 0.8f;
float remote_client_info_window_height =
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
ImGui::BeginChild(
"RemoteClientInfoWindow",
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries;
remote_entries.reserve(connection_status_.size());
for (const auto& kv : connection_status_) {
const auto host_it = connection_host_names_.find(kv.first);
const std::string display_name =
(host_it != connection_host_names_.end() && !host_it->second.empty())
? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
}
auto find_display_name_by_remote_id =
[&remote_entries](const std::string& remote_id) -> std::string {
for (const auto& entry : remote_entries) {
if (entry.first == remote_id) {
return entry.second;
}
}
return {};
};
if (!selected_server_remote_id_.empty() &&
find_display_name_by_remote_id(selected_server_remote_id_).empty()) {
selected_server_remote_id_.clear();
selected_server_remote_hostname_.clear();
}
if (selected_server_remote_id_.empty() && !remote_entries.empty()) {
selected_server_remote_id_ = remote_entries.front().first;
}
if (!selected_server_remote_id_.empty()) {
selected_server_remote_hostname_ =
find_display_name_by_remote_id(selected_server_remote_id_);
}
ImGui::SetWindowFontScale(font_scale);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s",
localization::controller[localization_language_index_].c_str());
ImGui::SameLine();
const char* selected_preview = "-";
if (!selected_server_remote_hostname_.empty()) {
selected_preview = selected_server_remote_hostname_.c_str();
} else if (!remote_client_id_.empty()) {
selected_preview = remote_client_id_.c_str();
}
ImGui::SetNextItemWidth(remote_client_info_window_width *
(localization_language_index_ == 0 ? 0.68f : 0.62f));
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
ImGui::AlignTextToFramePadding();
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
for (int i = 0; i < static_cast<int>(remote_entries.size()); i++) {
const bool selected =
(remote_entries[i].first == selected_server_remote_id_);
if (ImGui::Selectable(remote_entries[i].second.c_str(), selected)) {
selected_server_remote_id_ = remote_entries[i].first;
selected_server_remote_hostname_ = remote_entries[i].second;
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
ImGui::Separator();
ImGui::SetWindowFontScale(font_scale);
if (!selected_server_remote_id_.empty()) {
auto it = connection_status_.find(selected_server_remote_id_);
const ConnectionStatus status = (it == connection_status_.end())
? ConnectionStatus::Closed
: it->second;
ImGui::Text(
"%s",
localization::connection_status[localization_language_index_].c_str());
ImGui::SameLine();
switch (status) {
case ConnectionStatus::Connected:
ImGui::Text(
"%s",
localization::p2p_connected[localization_language_index_].c_str());
break;
case ConnectionStatus::Connecting:
ImGui::Text(
"%s",
localization::p2p_connecting[localization_language_index_].c_str());
break;
case ConnectionStatus::Disconnected:
ImGui::Text("%s",
localization::p2p_disconnected[localization_language_index_]
.c_str());
break;
case ConnectionStatus::Failed:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
case ConnectionStatus::Closed:
ImGui::Text(
"%s",
localization::p2p_closed[localization_language_index_].c_str());
break;
default:
ImGui::Text(
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
}
}
ImGui::Separator();
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s", localization::file_transfer[localization_language_index_].c_str());
ImGui::SameLine();
if (ImGui::Button(
localization::select_file[localization_language_index_].c_str())) {
std::string title = localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
LOG_INFO("Selected file path: {}", path.c_str());
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
}
if (file_transfer_.file_transfer_window_visible_) {
ImGui::SameLine();
const bool is_sending = file_transfer_.file_sending_.load();
if (is_sending) {
// Simple animation: cycle icon every 0.5s while sending.
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
ICON_FA_ANGLES_UP};
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
(static_cast<int>(sizeof(kFileTransferIcons) /
sizeof(kFileTransferIcons[0])));
ImGui::Text("%s", kFileTransferIcons[icon_index]);
} else {
// Completed.
ImGui::Text("%s", ICON_FA_CHECK);
}
if (ImGui::IsItemHovered()) {
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
float progress = 0.0f;
if (total_bytes > 0) {
progress =
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
}
std::string current_file_name;
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
if (current_file_id != 0) {
std::lock_guard<std::mutex> lock(
file_transfer_.file_transfer_list_mutex_);
for (const auto& info : file_transfer_.file_transfer_list_) {
if (info.file_id == current_file_id) {
current_file_name = info.file_name;
break;
}
}
}
ImGui::BeginTooltip();
if (server_windows_system_chinese_font_) {
ImGui::PushFont(server_windows_system_chinese_font_);
}
ImGui::SetWindowFontScale(0.5f);
if (!current_file_name.empty()) {
ImGui::Text("%s", current_file_name.c_str());
}
if (total_bytes > 0) {
const std::string sent_str = FormatBytes(sent_bytes);
const std::string total_str = FormatBytes(total_bytes);
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
}
const float text_height = ImGui::GetTextLineHeight();
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
BitrateDisplay(rate_bps);
ImGui::SetWindowFontScale(1.0f);
if (server_windows_system_chinese_font_) {
ImGui::PopFont();
}
ImGui::EndTooltip();
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild();
ImGui::SameLine();
float close_connection_button_width = server_window_width_ * 0.1f;
float close_connection_button_height = remote_client_info_window_height;
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_);
ImGui::SetWindowFontScale(font_scale);
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
close_connection_button_height))) {
if (peer_ && !selected_server_remote_id_.empty()) {
LeaveConnection(peer_, selected_server_remote_id_.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk

View File

@@ -2,6 +2,8 @@
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
void Render::DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props) {
std::string text;
@@ -29,13 +31,44 @@ void Render::DrawConnectionStatusText(
}
}
void Render::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) {
return;
}
bool has_valid_frame = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
has_valid_frame = props->stream_texture_ != nullptr &&
props->video_width_ > 0 && props->video_height_ > 0 &&
props->front_frame_ && !props->front_frame_->empty();
}
if (has_valid_frame) {
return;
}
const std::string& text =
localization::receiving_screen[localization_language_index_];
ImVec2 size = ImGui::GetWindowSize();
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPos(
ImVec2((size.x - text_size.x) * 0.5f, (size.y - text_size.y) * 0.5f));
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
}
void Render::CloseTab(decltype(client_properties_)::iterator& it) {
CleanupPeer(it->second);
it = client_properties_.erase(it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
// std::unique_lock lock(client_properties_mutex_);
if (it != client_properties_.end()) {
CleanupPeer(it->second);
it = client_properties_.erase(it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
}
}
@@ -48,21 +81,28 @@ int Render::StreamWindow() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
ImGui::Begin("VideoBg", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
bool video_bg_opened = ImGui::Begin(
"VideoBg", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
if (!video_bg_opened) {
return 0;
}
ImGuiWindowFlags stream_window_flag =
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoMove;
if (!fullscreen_button_pressed_) {
ImGui::SetNextWindowPos(ImVec2(20, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(0, 20), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 8.0f));
ImGui::SetNextWindowPos(
ImVec2(title_bar_button_width_ * 0.8f, title_bar_button_width_ * 0.1f),
ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(0, title_bar_button_width_ * 0.8f),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0.0f));
ImGui::Begin("TabBar", nullptr,
@@ -77,25 +117,37 @@ int Render::StreamWindow() {
ImGuiTabBarFlags_AutoSelectNewTabs)) {
is_tab_bar_hovered_ = ImGui::IsWindowHovered();
// std::shared_lock lock(client_properties_mutex_);
for (auto it = client_properties_.begin();
it != client_properties_.end();) {
auto& props = it->second;
if (!props->tab_opened_) {
CloseTab(it);
std::string remote_id_to_close = props->remote_id_;
// lock.unlock();
{
// std::unique_lock unique_lock(client_properties_mutex_);
auto close_it = client_properties_.find(remote_id_to_close);
if (close_it != client_properties_.end()) {
CloseTab(close_it);
}
}
// lock.lock();
it = client_properties_.begin();
continue;
}
ImGui::SetWindowFontScale(0.6f);
std::string tab_label =
enable_srtp_
? std::string(ICON_FA_SHIELD_HALVED) + " " + props->remote_id_
: props->remote_id_;
if (ImGui::BeginTabItem(tab_label.c_str(), &props->tab_opened_)) {
props->tab_selected_ = true;
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.6f);
ImGui::SetNextWindowSize(
ImVec2(stream_window_width_, stream_window_height_),
ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImGuiCond_Always);
ImGui::SetNextWindowPos(
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
@@ -117,15 +169,33 @@ int Render::StreamWindow() {
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
DrawReceivingScreenText(props);
focused_remote_id_ = props->remote_id_;
if (!props->peer_) {
it = client_properties_.erase(it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
std::string remote_id_to_erase = props->remote_id_;
// lock.unlock();
{
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
// Ensure we flush pending STREAM_REFRESH_EVENT events and
// clean up peer resources before erasing the entry, otherwise
// SDL events may still hold raw pointers to freed
// SubStreamWindowProperties (including video_frame_mutex_),
// leading to std::system_error when locking.
CloseTab(erase_it);
}
}
// lock.lock();
ImGui::End();
ImGui::EndTabItem();
it = client_properties_.begin();
continue;
} else {
DrawConnectionStatusText(props);
++it;
@@ -135,7 +205,20 @@ int Render::StreamWindow() {
ImGui::EndTabItem();
} else {
props->tab_selected_ = false;
ImGui::SetWindowFontScale(1.0f);
if (!props->tab_opened_) {
std::string remote_id_to_close = props->remote_id_;
// lock.unlock();
{
// std::unique_lock unique_lock(client_properties_mutex_);
auto close_it = client_properties_.find(remote_id_to_close);
if (close_it != client_properties_.end()) {
CloseTab(close_it);
}
}
// lock.lock();
it = client_properties_.begin();
continue;
}
++it;
}
}
@@ -145,17 +228,30 @@ int Render::StreamWindow() {
ImGui::End(); // End TabBar
} else {
// std::shared_lock lock(client_properties_mutex_);
for (auto it = client_properties_.begin();
it != client_properties_.end();) {
auto& props = it->second;
if (!props->tab_opened_) {
CloseTab(it);
std::string remote_id_to_close = props->remote_id_;
// lock.unlock();
{
// std::unique_lock unique_lock(client_properties_mutex_);
auto close_it = client_properties_.find(remote_id_to_close);
if (close_it != client_properties_.end()) {
CloseTab(close_it);
}
}
// lock.lock();
it = client_properties_.begin();
continue;
}
if (props->tab_selected_) {
ImGui::SetNextWindowSize(
ImVec2(stream_window_width_, stream_window_height_),
ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -174,17 +270,29 @@ int Render::StreamWindow() {
UpdateRenderRect();
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
DrawReceivingScreenText(props);
ImGui::End();
if (!props->peer_) {
fullscreen_button_pressed_ = false;
SDL_SetWindowFullscreen(stream_window_, false);
it = client_properties_.erase(it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
std::string remote_id_to_erase = props->remote_id_;
// lock.unlock();
{
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
CloseTab(erase_it);
}
}
// lock.lock();
it = client_properties_.begin();
continue;
} else {
DrawConnectionStatusText(props);
++it;
@@ -199,4 +307,5 @@ int Render::StreamWindow() {
ImGui::End(); // End VideoBg
return 0;
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,207 @@
#include <algorithm>
#include <string>
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
std::string CleanMarkdown(const std::string& markdown) {
std::string result = markdown;
// remove # title mark
size_t pos = 0;
while (pos < result.length()) {
if (result[pos] == '\n' || pos == 0) {
size_t line_start = (result[pos] == '\n') ? pos + 1 : pos;
if (line_start < result.length() && result[line_start] == '#') {
size_t hash_end = line_start;
while (hash_end < result.length() &&
(result[hash_end] == '#' || result[hash_end] == ' ')) {
hash_end++;
}
result.erase(line_start, hash_end - line_start);
pos = line_start;
continue;
}
}
pos++;
}
// remove ** bold mark
pos = 0;
while ((pos = result.find("**", pos)) != std::string::npos) {
result.erase(pos, 2);
}
// remove all spaces
result.erase(std::remove(result.begin(), result.end(), ' '), result.end());
// replace . with 、
pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement
}
return result;
}
int Render::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
float update_notification_window_width = title_bar_button_width_ * 10.0f;
float update_notification_window_height = title_bar_button_width_ * 8.0f;
// #ifdef __APPLE__
// float font_scale = 0.3f;
// #else
// float font_scale = 0.5f;
// #endif
ImGui::SetNextWindowPos(ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
update_notification_window_width) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
update_notification_window_height) /
2),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(update_notification_window_width,
update_notification_window_height));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoTitleBar);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
update_notification_window_height * 0.05f);
// title: new version available
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
ImGui::SetWindowFontScale(0.55f);
std::string title =
localization::new_version_available[localization_language_index_] +
": v" + latest_version_;
ImGui::Text("%s", title.c_str());
ImGui::SetWindowFontScale(0.1f);
ImGui::Spacing();
// website link
std::string download_text =
localization::access_website[localization_language_index_] +
"https://crossdesk.cn";
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
Hyperlink(download_text, "https://crossdesk.cn",
update_notification_window_width);
ImGui::SetWindowFontScale(1.0f);
ImGui::Spacing();
float scrollable_height =
update_notification_window_height - UPDATE_NOTIFICATION_RESERVED_HEIGHT;
if (main_windows_system_chinese_font_ != nullptr) {
ImGui::PushFont(main_windows_system_chinese_font_);
}
// scrollable content area
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
ImGui::BeginChild(
"ScrollableContent",
ImVec2(update_notification_window_width * 0.9f, scrollable_height),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f);
// set text wrap position to current available width (accounts for
// scrollbar)
float wrap_pos = ImGui::GetContentRegionAvail().x;
ImGui::PushTextWrapPos(wrap_pos);
// release name
if (latest_version_info_.contains("releaseName") &&
latest_version_info_["releaseName"].is_string() &&
!latest_version_info_["releaseName"].empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string release_name =
latest_version_info_["releaseName"].get<std::string>();
ImGui::TextWrapped("%s", release_name.c_str());
ImGui::Spacing();
}
// release notes
if (!release_notes_.empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string cleaned_notes = CleanMarkdown(release_notes_);
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::Spacing();
}
// release date
if (latest_version_info_.contains("releaseDate") &&
latest_version_info_["releaseDate"].is_string() &&
!latest_version_info_["releaseDate"].empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string date_label =
localization::release_date[localization_language_index_];
std::string release_date = latest_version_info_["releaseDate"];
std::string date_text = date_label + release_date;
ImGui::Text("%s", date_text.c_str());
ImGui::Spacing();
}
// pop text wrap position
ImGui::PopTextWrapPos();
ImGui::EndChild();
// pop system font
if (main_windows_system_chinese_font_ != nullptr) {
ImGui::PopFont();
}
ImGui::Spacing();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(update_notification_window_width * 0.407f);
} else {
ImGui::SetCursorPosX(update_notification_window_width * 0.367f);
}
ImGui::SetWindowFontScale(0.5f);
// update button
if (ImGui::Button(
localization::update[localization_language_index_].c_str())) {
// open download page
std::string url = "https://crossdesk.cn";
OpenUrl(url);
show_update_notification_window_ = false;
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str())) {
show_update_notification_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
return 0;
}
} // namespace crossdesk

View File

@@ -3,6 +3,8 @@
#include <atomic>
#include <filesystem>
namespace crossdesk {
namespace {
std::string g_log_dir = "logs";
@@ -60,3 +62,4 @@ std::shared_ptr<spdlog::logger> get_logger() {
return g_logger;
}
} // namespace crossdesk

View File

@@ -25,6 +25,8 @@
#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO
namespace crossdesk {
constexpr auto LOGGER_NAME = "crossdesk";
void InitLogger(const std::string& log_dir);
@@ -35,5 +37,5 @@ std::shared_ptr<spdlog::logger> get_logger();
#define LOG_WARN(...) SPDLOG_LOGGER_WARN(get_logger(), __VA_ARGS__)
#define LOG_ERROR(...) SPDLOG_LOGGER_ERROR(get_logger(), __VA_ARGS__)
#define LOG_FATAL(...) SPDLOG_LOGGER_CRITICAL(get_logger(), __VA_ARGS__)
} // namespace crossdesk
#endif

View File

@@ -2,6 +2,8 @@
#include <cstdlib>
namespace crossdesk {
PathManager::PathManager(const std::string& app_name) : app_name_(app_name) {}
std::filesystem::path PathManager::GetConfigPath() {
@@ -16,7 +18,11 @@ std::filesystem::path PathManager::GetConfigPath() {
std::filesystem::path PathManager::GetCachePath() {
#ifdef _WIN32
#ifdef CROSSDESK_DEBUG
return "cache";
#else
return GetKnownFolder(FOLDERID_LocalAppData) / app_name_ / "cache";
#endif
#elif __APPLE__
return GetEnvOrDefault("XDG_CACHE_HOME", GetHome() + "/.cache") / app_name_;
#else
@@ -30,21 +36,7 @@ std::filesystem::path PathManager::GetLogPath() {
#elif __APPLE__
return GetHome() + "/Library/Logs/" + app_name_;
#else
return GetCachePath() / app_name_ / "logs";
#endif
}
std::filesystem::path PathManager::GetCertPath() {
#ifdef _WIN32
// %APPDATA%\AppName\Certs
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name_ / "certs";
#elif __APPLE__
// $HOME/Library/Application Support/AppName/certs
return GetHome() + "/Library/Application Support/" + app_name_ + "/certs";
#else
// $XDG_CONFIG_HOME/AppName/certs
return GetEnvOrDefault("XDG_CONFIG_HOME", GetHome() + "/.config") /
app_name_ / "certs";
return GetCachePath() / "logs";
#endif
}
@@ -70,22 +62,35 @@ std::filesystem::path PathManager::GetKnownFolder(REFKNOWNFOLDERID id) {
#endif
std::string PathManager::GetHome() {
if (const char* home = getenv("HOME")) {
return std::string(home);
}
#ifdef _WIN32
char path[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_PROFILE, NULL, 0, path)))
return std::string(path);
#else
if (const char* home = getenv("HOME")) {
return std::string(home);
}
#endif
return {};
}
std::filesystem::path PathManager::GetEnvOrDefault(const char* env_var,
const std::string& def) {
#ifdef _WIN32
char* buffer = nullptr;
size_t size = 0;
if (_dupenv_s(&buffer, &size, env_var) == 0 && buffer != nullptr) {
std::filesystem::path result(buffer);
free(buffer);
return result;
}
#else
if (const char* val = getenv(env_var)) {
return std::filesystem::path(val);
}
#endif
return std::filesystem::path(def);
}
} // namespace crossdesk

View File

@@ -14,6 +14,8 @@
#include <windows.h>
#endif
namespace crossdesk {
class PathManager {
public:
explicit PathManager(const std::string& app_name);
@@ -24,8 +26,6 @@ class PathManager {
std::filesystem::path GetLogPath();
std::filesystem::path GetCertPath();
bool CreateDirectories(const std::filesystem::path& p);
private:
@@ -40,5 +40,5 @@ class PathManager {
private:
std::string app_name_;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,573 @@
#include "screen_capturer_drm.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
defined(__has_include) && __has_include(<xf86drm.h>) && \
__has_include(<xf86drmMode.h>)
#define CROSSDESK_DRM_BUILD_ENABLED 1
#include <xf86drm.h>
#include <xf86drmMode.h>
#elif defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
defined(__has_include) && __has_include(<libdrm/xf86drm.h>) && \
__has_include(<libdrm/xf86drmMode.h>)
#define CROSSDESK_DRM_BUILD_ENABLED 1
#include <libdrm/xf86drm.h>
#include <libdrm/xf86drmMode.h>
#else
#define CROSSDESK_DRM_BUILD_ENABLED 0
#endif
#if CROSSDESK_DRM_BUILD_ENABLED
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <thread>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr int kMaxDrmCards = 16;
const char* ConnectorTypeName(uint32_t type) {
switch (type) {
case DRM_MODE_CONNECTOR_VGA:
return "VGA";
case DRM_MODE_CONNECTOR_DVII:
return "DVI-I";
case DRM_MODE_CONNECTOR_DVID:
return "DVI-D";
case DRM_MODE_CONNECTOR_DVIA:
return "DVI-A";
case DRM_MODE_CONNECTOR_HDMIA:
return "HDMI-A";
case DRM_MODE_CONNECTOR_HDMIB:
return "HDMI-B";
case DRM_MODE_CONNECTOR_DisplayPort:
return "DP";
case DRM_MODE_CONNECTOR_eDP:
return "eDP";
case DRM_MODE_CONNECTOR_LVDS:
return "LVDS";
#ifdef DRM_MODE_CONNECTOR_VIRTUAL
case DRM_MODE_CONNECTOR_VIRTUAL:
return "Virtual";
#endif
default:
return "Display";
}
}
} // namespace
ScreenCapturerDrm::ScreenCapturerDrm() {}
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
int ScreenCapturerDrm::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!cb) {
LOG_ERROR("DRM screen capturer callback is null");
return -1;
}
fps_ = std::max(1, fps);
callback_ = cb;
monitor_index_ = 0;
initial_monitor_index_ = 0;
consecutive_failures_ = 0;
display_info_list_.clear();
outputs_.clear();
y_plane_.clear();
uv_plane_.clear();
if (!DiscoverOutputs()) {
LOG_ERROR("DRM screen capturer could not find active outputs");
callback_ = nullptr;
CloseDevices();
return -1;
}
return 0;
}
int ScreenCapturerDrm::Destroy() {
Stop();
callback_ = nullptr;
display_info_list_.clear();
outputs_.clear();
y_plane_.clear();
uv_plane_.clear();
CloseDevices();
return 0;
}
int ScreenCapturerDrm::Start(bool show_cursor) {
if (running_) {
return 0;
}
if (outputs_.empty()) {
LOG_ERROR("DRM screen capturer has no output to capture");
return -1;
}
show_cursor_ = show_cursor;
paused_ = false;
int probe_index = monitor_index_.load();
if (probe_index < 0 || probe_index >= static_cast<int>(outputs_.size())) {
probe_index = 0;
}
if (!CaptureOutputFrame(outputs_[probe_index], false)) {
LOG_ERROR("DRM start probe failed on output {}", outputs_[probe_index].name);
return -1;
}
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDrm::Stop() {
if (!running_) {
return 0;
}
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
return 0;
}
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDrm::SwitchTo(int monitor_index) {
if (monitor_index < 0 ||
monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid DRM monitor index: {}", monitor_index);
return -1;
}
monitor_index_ = monitor_index;
return 0;
}
int ScreenCapturerDrm::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
return display_info_list_;
}
bool ScreenCapturerDrm::DiscoverOutputs() {
for (int card_index = 0; card_index < kMaxDrmCards; ++card_index) {
const std::string card_path = "/dev/dri/card" + std::to_string(card_index);
const int fd = open(card_path.c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0) {
continue;
}
drmModeRes* resources = drmModeGetResources(fd);
if (!resources) {
close(fd);
continue;
}
DrmDevice device;
device.fd = fd;
device.path = card_path;
devices_.push_back(device);
const int device_slot = static_cast<int>(devices_.size()) - 1;
const size_t output_count_before = outputs_.size();
for (int i = 0; i < resources->count_connectors; ++i) {
drmModeConnector* connector =
drmModeGetConnector(fd, resources->connectors[i]);
if (!connector) {
continue;
}
if (connector->connection != DRM_MODE_CONNECTED ||
connector->count_modes <= 0) {
drmModeFreeConnector(connector);
continue;
}
uint32_t crtc_id = 0;
if (connector->encoder_id != 0) {
drmModeEncoder* encoder = drmModeGetEncoder(fd, connector->encoder_id);
if (encoder) {
crtc_id = encoder->crtc_id;
drmModeFreeEncoder(encoder);
}
}
if (crtc_id == 0) {
for (int enc_idx = 0; enc_idx < connector->count_encoders; ++enc_idx) {
drmModeEncoder* encoder =
drmModeGetEncoder(fd, connector->encoders[enc_idx]);
if (!encoder) {
continue;
}
if (encoder->crtc_id != 0) {
crtc_id = encoder->crtc_id;
drmModeFreeEncoder(encoder);
break;
}
drmModeFreeEncoder(encoder);
}
}
if (crtc_id == 0) {
drmModeFreeConnector(connector);
continue;
}
drmModeCrtc* crtc = drmModeGetCrtc(fd, crtc_id);
if (!crtc || !crtc->mode_valid || crtc->width <= 0 || crtc->height <= 0) {
if (crtc) {
drmModeFreeCrtc(crtc);
}
drmModeFreeConnector(connector);
continue;
}
DrmOutput output;
output.device_index = device_slot;
output.connector_id = connector->connector_id;
output.crtc_id = crtc_id;
output.left = crtc->x;
output.top = crtc->y;
output.width = static_cast<int>(crtc->width);
output.height = static_cast<int>(crtc->height);
output.name = std::string(ConnectorTypeName(connector->connector_type)) +
std::to_string(connector->connector_type_id);
outputs_.push_back(output);
display_info_list_.push_back(
DisplayInfo(output.name, output.left, output.top,
output.left + output.width, output.top + output.height));
LOG_INFO("DRM output found: {} on {}, {}x{} @ ({}, {})", output.name,
card_path, output.width, output.height, output.left, output.top);
drmModeFreeCrtc(crtc);
drmModeFreeConnector(connector);
}
drmModeFreeResources(resources);
if (outputs_.size() == output_count_before) {
close(fd);
devices_.pop_back();
}
}
if (outputs_.empty()) {
return false;
}
LOG_INFO("DRM screen capturer discovered {} output(s)", outputs_.size());
return true;
}
void ScreenCapturerDrm::CloseDevices() {
for (auto& device : devices_) {
if (device.fd >= 0) {
close(device.fd);
device.fd = -1;
}
}
devices_.clear();
}
void ScreenCapturerDrm::CaptureLoop() {
using clock = std::chrono::steady_clock;
const auto frame_interval =
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
while (running_) {
const auto frame_start = clock::now();
if (!paused_) {
int index = monitor_index_.load();
if (index >= 0 && index < static_cast<int>(outputs_.size())) {
const bool ok = CaptureOutputFrame(outputs_[index], true);
if (!ok) {
++consecutive_failures_;
if (consecutive_failures_ == 1 || consecutive_failures_ % 60 == 0) {
LOG_WARN("DRM capture failed (consecutive={})",
consecutive_failures_);
}
} else {
consecutive_failures_ = 0;
}
}
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
clock::now() - frame_start);
if (elapsed < frame_interval) {
std::this_thread::sleep_for(frame_interval - elapsed);
}
}
}
bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
bool emit_callback) {
if (output.device_index < 0 ||
output.device_index >= static_cast<int>(devices_.size())) {
return false;
}
const int fd = devices_[output.device_index].fd;
if (fd < 0) {
return false;
}
drmModeCrtc* crtc = drmModeGetCrtc(fd, output.crtc_id);
if (!crtc) {
return false;
}
const uint32_t fb_id = crtc->buffer_id;
drmModeFreeCrtc(crtc);
if (fb_id == 0) {
return false;
}
drmModeFB* fb = drmModeGetFB(fd, fb_id);
if (!fb) {
return false;
}
const uint32_t handle = fb->handle;
const uint32_t pitch = fb->pitch;
const int src_width = static_cast<int>(fb->width);
const int src_height = static_cast<int>(fb->height);
const int bpp = static_cast<int>(fb->bpp);
drmModeFreeFB(fb);
if (handle == 0 || pitch == 0 || src_width <= 1 || src_height <= 1) {
return false;
}
if (bpp != 32) {
LOG_WARN("DRM capture unsupported bpp: {}", bpp);
return false;
}
const size_t map_size =
static_cast<size_t>(pitch) * static_cast<size_t>(src_height);
uint8_t* mapped_ptr = nullptr;
size_t mapped_size = 0;
int prime_fd = -1;
if (!MapFramebuffer(fd, handle, map_size, &mapped_ptr, &mapped_size,
&prime_fd)) {
return false;
}
int capture_width = std::min(src_width, output.width);
int capture_height = std::min(src_height, output.height);
if (capture_width <= 0 || capture_height <= 0) {
capture_width = src_width;
capture_height = src_height;
}
capture_width &= ~1;
capture_height &= ~1;
if (capture_width <= 1 || capture_height <= 1) {
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return false;
}
const size_t y_size =
static_cast<size_t>(capture_width) * static_cast<size_t>(capture_height);
const size_t uv_size = y_size / 2;
if (y_plane_.size() != y_size) {
y_plane_.resize(y_size);
}
if (uv_plane_.size() != uv_size) {
uv_plane_.resize(uv_size);
}
const int convert_ret =
libyuv::ARGBToNV12(mapped_ptr, static_cast<int>(pitch), y_plane_.data(),
capture_width, uv_plane_.data(), capture_width,
capture_width, capture_height);
if (convert_ret != 0) {
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return false;
}
std::vector<uint8_t> nv12;
nv12.reserve(y_plane_.size() + uv_plane_.size());
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
capture_height, output.name.c_str());
}
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return true;
}
bool ScreenCapturerDrm::MapFramebuffer(int fd, uint32_t handle, size_t map_size,
uint8_t** mapped_ptr,
size_t* mapped_size,
int* prime_fd) const {
if (!mapped_ptr || !mapped_size || !prime_fd || map_size == 0) {
return false;
}
*mapped_ptr = nullptr;
*mapped_size = 0;
*prime_fd = -1;
drm_mode_map_dumb map_arg{};
map_arg.handle = handle;
if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg) == 0) {
void* mapped = mmap(nullptr, map_size, PROT_READ, MAP_SHARED, fd,
static_cast<off_t>(map_arg.offset));
if (mapped != MAP_FAILED) {
*mapped_ptr = static_cast<uint8_t*>(mapped);
*mapped_size = map_size;
return true;
}
}
int dma_fd = -1;
if (drmPrimeHandleToFD(fd, handle, DRM_CLOEXEC, &dma_fd) == 0) {
size_t dma_map_size = map_size;
const off_t fd_size = lseek(dma_fd, 0, SEEK_END);
if (fd_size > 0) {
dma_map_size = std::min(map_size, static_cast<size_t>(fd_size));
}
void* mapped =
mmap(nullptr, dma_map_size, PROT_READ, MAP_SHARED, dma_fd, 0);
if (mapped != MAP_FAILED) {
*mapped_ptr = static_cast<uint8_t*>(mapped);
*mapped_size = dma_map_size;
*prime_fd = dma_fd;
return true;
}
close(dma_fd);
}
return false;
}
void ScreenCapturerDrm::UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
int prime_fd) const {
if (mapped_ptr && mapped_size > 0) {
munmap(mapped_ptr, mapped_size);
}
if (prime_fd >= 0) {
close(prime_fd);
}
}
} // namespace crossdesk
#else
#include "rd_log.h"
namespace crossdesk {
ScreenCapturerDrm::ScreenCapturerDrm() {}
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
int ScreenCapturerDrm::Init([[maybe_unused]] const int fps, cb_desktop_data cb) {
Destroy();
callback_ = cb;
LOG_WARN("DRM screen capturer disabled: libdrm headers not available");
return -1;
}
int ScreenCapturerDrm::Destroy() {
Stop();
callback_ = nullptr;
display_info_list_.clear();
outputs_.clear();
return 0;
}
int ScreenCapturerDrm::Start([[maybe_unused]] bool show_cursor) { return -1; }
int ScreenCapturerDrm::Stop() {
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
return 0;
}
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) { return 0; }
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) { return 0; }
int ScreenCapturerDrm::SwitchTo([[maybe_unused]] int monitor_index) {
return -1;
}
int ScreenCapturerDrm::ResetToInitialMonitor() { return 0; }
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
return display_info_list_;
}
bool ScreenCapturerDrm::DiscoverOutputs() { return false; }
void ScreenCapturerDrm::CloseDevices() {}
void ScreenCapturerDrm::CaptureLoop() {}
bool ScreenCapturerDrm::CaptureOutputFrame(
[[maybe_unused]] const DrmOutput& output,
[[maybe_unused]] bool emit_callback) {
return false;
}
bool ScreenCapturerDrm::MapFramebuffer([[maybe_unused]] int fd,
[[maybe_unused]] uint32_t handle,
[[maybe_unused]] size_t map_size,
[[maybe_unused]] uint8_t** mapped_ptr,
[[maybe_unused]] size_t* mapped_size,
[[maybe_unused]] int* prime_fd) const {
return false;
}
void ScreenCapturerDrm::UnmapFramebuffer([[maybe_unused]] uint8_t* mapped_ptr,
[[maybe_unused]] size_t mapped_size,
[[maybe_unused]] int prime_fd) const {}
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,87 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DRM_H_
#define _SCREEN_CAPTURER_DRM_H_
#include <atomic>
#include <cstdint>
#include <string>
#include <thread>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDrm : public ScreenCapturer {
public:
ScreenCapturerDrm();
~ScreenCapturerDrm();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
struct DrmDevice {
int fd = -1;
std::string path;
};
struct DrmOutput {
int device_index = -1;
uint32_t connector_id = 0;
uint32_t crtc_id = 0;
std::string name;
int left = 0;
int top = 0;
int width = 0;
int height = 0;
};
private:
bool DiscoverOutputs();
void CloseDevices();
void CaptureLoop();
bool CaptureOutputFrame(const DrmOutput& output, bool emit_callback = true);
bool MapFramebuffer(int fd, uint32_t handle, size_t map_size,
uint8_t** mapped_ptr, size_t* mapped_size,
int* prime_fd) const;
void UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
int prime_fd) const;
private:
std::vector<DrmDevice> devices_;
std::vector<DrmOutput> outputs_;
std::vector<DisplayInfo> display_info_list_;
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_;
int consecutive_failures_ = 0;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,506 @@
#include "screen_capturer_linux.h"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include "platform.h"
#include "rd_log.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
#include "screen_capturer_drm.h"
#endif
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include "screen_capturer_wayland.h"
#endif
#include "screen_capturer_x11.h"
namespace crossdesk {
namespace {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
constexpr bool kDrmBuildEnabled = true;
#else
constexpr bool kDrmBuildEnabled = false;
#endif
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
constexpr bool kWaylandBuildEnabled = true;
#else
constexpr bool kWaylandBuildEnabled = false;
#endif
} // namespace
ScreenCapturerLinux::ScreenCapturerLinux() {}
ScreenCapturerLinux::~ScreenCapturerLinux() { Destroy(); }
int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!cb) {
LOG_ERROR("Linux screen capturer callback is null");
return -1;
}
fps_ = fps;
callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height,
const char* display_name) {
const std::string mapped_name = MapDisplayName(display_name);
if (callback_orig_) {
callback_orig_(data, size, width, height, mapped_name.c_str());
}
};
const char* force_backend = getenv("CROSSDESK_SCREEN_BACKEND");
if (force_backend && force_backend[0] != '\0') {
if (strcmp(force_backend, "drm") == 0) {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
LOG_INFO("Linux screen capturer forced backend: DRM");
return InitDrm();
#else
LOG_ERROR(
"Linux screen capturer forced backend DRM is disabled at build time");
return -1;
#endif
}
if (strcmp(force_backend, "x11") == 0) {
LOG_INFO("Linux screen capturer forced backend: X11");
return InitX11();
}
if (strcmp(force_backend, "wayland") == 0) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
LOG_INFO("Linux screen capturer forced backend: Wayland");
return InitWayland();
#else
LOG_ERROR(
"Linux screen capturer forced backend Wayland is disabled at build "
"time");
return -1;
#endif
}
LOG_WARN("Unknown CROSSDESK_SCREEN_BACKEND={}, using auto strategy",
force_backend);
}
const bool wayland_session = IsWaylandSession();
if (wayland_session) {
if (kDrmBuildEnabled) {
LOG_INFO("Wayland session detected, prefer DRM -> X11 -> Wayland");
if (InitDrm() == 0) {
return 0;
}
} else {
LOG_INFO("Wayland session detected, DRM disabled, prefer X11 -> Wayland");
}
if (InitX11() == 0) {
return 0;
}
if (kDrmBuildEnabled) {
LOG_WARN(
"DRM and X11 init failed in Wayland session, trying Wayland portal");
} else {
LOG_WARN("X11 init failed in Wayland session, trying Wayland portal");
}
if (kWaylandBuildEnabled) {
return InitWayland();
}
LOG_ERROR("Wayland session detected but Wayland backend is disabled");
return -1;
}
if (InitX11() == 0) {
return 0;
}
if (kDrmBuildEnabled) {
LOG_WARN("X11 init failed, trying DRM fallback");
return InitDrm();
}
LOG_ERROR("X11 init failed and DRM backend is disabled");
return -1;
}
int ScreenCapturerLinux::Destroy() {
if (impl_) {
impl_->Destroy();
impl_.reset();
}
backend_ = BackendType::kNone;
callback_ = nullptr;
callback_orig_ = nullptr;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
canonical_displays_.clear();
label_alias_.clear();
}
return 0;
}
int ScreenCapturerLinux::Start(bool show_cursor) {
if (!impl_) {
LOG_ERROR("Linux screen capturer backend is not initialized");
return -1;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (backend_ == BackendType::kWayland) {
const int refresh_ret = RefreshWaylandBackend();
if (refresh_ret != 0) {
LOG_WARN("Linux screen capturer Wayland backend refresh failed: {}",
refresh_ret);
}
}
#endif
const int ret = impl_->Start(show_cursor);
if (ret == 0) {
return 0;
}
const char* backend_name = "None";
if (backend_ == BackendType::kX11) {
backend_name = "X11";
} else if (backend_ == BackendType::kDrm) {
backend_name = "DRM";
} else if (backend_ == BackendType::kWayland) {
backend_name = "Wayland";
}
LOG_WARN("Linux screen capturer backend {} start failed: {}",
backend_name, ret);
if (backend_ == BackendType::kX11 && kDrmBuildEnabled &&
TryFallbackToDrm(show_cursor)) {
return 0;
}
if (backend_ == BackendType::kX11 && kWaylandBuildEnabled &&
TryFallbackToWayland(show_cursor)) {
return 0;
}
if (backend_ == BackendType::kDrm && kDrmBuildEnabled) {
if (TryFallbackToX11(show_cursor)) {
return 0;
}
if (kWaylandBuildEnabled && TryFallbackToWayland(show_cursor)) {
return 0;
}
}
if (backend_ == BackendType::kWayland && kWaylandBuildEnabled) {
if (kDrmBuildEnabled && TryFallbackToDrm(show_cursor)) {
return 0;
}
if (TryFallbackToX11(show_cursor)) {
return 0;
}
}
return ret;
}
int ScreenCapturerLinux::Stop() {
if (!impl_) {
return 0;
}
const int ret = impl_->Stop();
UpdateAliasesFromBackend(impl_.get());
return ret;
}
int ScreenCapturerLinux::Pause(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->Pause(monitor_index);
}
int ScreenCapturerLinux::Resume(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->Resume(monitor_index);
}
int ScreenCapturerLinux::SwitchTo(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->SwitchTo(monitor_index);
}
int ScreenCapturerLinux::ResetToInitialMonitor() {
if (!impl_) {
return -1;
}
return impl_->ResetToInitialMonitor();
}
std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
if (!impl_) {
return std::vector<DisplayInfo>();
}
// Wayland backend may update display geometry/stream handle asynchronously
// after Start(). Refresh aliases every time to keep canonical displays fresh.
UpdateAliasesFromBackend(impl_.get());
std::lock_guard<std::mutex> lock(alias_mutex_);
if (!canonical_displays_.empty()) {
return canonical_displays_;
}
return impl_->GetDisplayInfoList();
}
int ScreenCapturerLinux::InitX11() {
auto backend = std::make_unique<ScreenCapturerX11>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer X11 init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kX11;
LOG_INFO("Linux screen capturer backend selected: X11");
return 0;
}
int ScreenCapturerLinux::InitDrm() {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
auto backend = std::make_unique<ScreenCapturerDrm>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer DRM init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kDrm;
LOG_INFO("Linux screen capturer backend selected: DRM");
return 0;
#else
LOG_WARN("Linux screen capturer DRM backend is disabled at build time");
return -1;
#endif
}
int ScreenCapturerLinux::InitWayland() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto backend = std::make_unique<ScreenCapturerWayland>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer Wayland init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer backend selected: Wayland");
return 0;
#else
LOG_WARN("Linux screen capturer Wayland backend is disabled at build time");
return -1;
#endif
}
int ScreenCapturerLinux::RefreshWaylandBackend() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto backend = std::make_unique<ScreenCapturerWayland>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
return ret;
}
if (impl_) {
impl_->Destroy();
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer Wayland backend refreshed before start");
return 0;
#else
return -1;
#endif
}
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
auto drm_backend = std::make_unique<ScreenCapturerDrm>();
int ret = drm_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback DRM init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(drm_backend.get());
ret = drm_backend->Start(show_cursor);
if (ret != 0) {
drm_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback DRM start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(drm_backend);
backend_ = BackendType::kDrm;
LOG_INFO("Linux screen capturer fallback switched to DRM");
return true;
#else
(void)show_cursor;
LOG_WARN("Linux screen capturer DRM fallback is disabled at build time");
return false;
#endif
}
bool ScreenCapturerLinux::TryFallbackToX11(bool show_cursor) {
auto x11_backend = std::make_unique<ScreenCapturerX11>();
int ret = x11_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback X11 init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(x11_backend.get());
ret = x11_backend->Start(show_cursor);
if (ret != 0) {
x11_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback X11 start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(x11_backend);
backend_ = BackendType::kX11;
LOG_INFO("Linux screen capturer fallback switched to X11");
return true;
}
bool ScreenCapturerLinux::TryFallbackToWayland(bool show_cursor) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto wayland_backend = std::make_unique<ScreenCapturerWayland>();
int ret = wayland_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback Wayland init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(wayland_backend.get());
ret = wayland_backend->Start(show_cursor);
if (ret != 0) {
wayland_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback Wayland start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(wayland_backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer fallback switched to Wayland");
return true;
#else
(void)show_cursor;
LOG_WARN("Linux screen capturer Wayland fallback is disabled at build time");
return false;
#endif
}
void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
if (!backend) {
return;
}
const auto backend_displays = backend->GetDisplayInfoList();
if (backend_displays.empty()) {
return;
}
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
if (canonical_displays_.empty()) {
canonical_displays_ = backend_displays;
for (const auto& display : backend_displays) {
label_alias_[display.name] = display.name;
}
return;
}
if (canonical_displays_.size() < backend_displays.size()) {
for (size_t i = canonical_displays_.size(); i < backend_displays.size();
++i) {
canonical_displays_.push_back(backend_displays[i]);
}
}
for (size_t i = 0; i < backend_displays.size(); ++i) {
const std::string mapped_name = i < canonical_displays_.size()
? canonical_displays_[i].name
: backend_displays[i].name;
label_alias_[backend_displays[i].name] = mapped_name;
if (i < canonical_displays_.size()) {
// Keep original stable names, but refresh geometry from active backend.
canonical_displays_[i].handle = backend_displays[i].handle;
canonical_displays_[i].is_primary = backend_displays[i].is_primary;
canonical_displays_[i].left = backend_displays[i].left;
canonical_displays_[i].top = backend_displays[i].top;
canonical_displays_[i].right = backend_displays[i].right;
canonical_displays_[i].bottom = backend_displays[i].bottom;
canonical_displays_[i].width = backend_displays[i].width;
canonical_displays_[i].height = backend_displays[i].height;
}
}
}
std::string ScreenCapturerLinux::MapDisplayName(const char* display_name) const {
std::string input_name = display_name ? display_name : "";
if (input_name.empty()) {
return input_name;
}
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(input_name);
if (it != label_alias_.end()) {
return it->second;
}
if (canonical_displays_.size() == 1) {
return canonical_displays_[0].name;
}
return input_name;
}
} // namespace crossdesk

View File

@@ -0,0 +1,66 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_LINUX_H_
#define _SCREEN_CAPTURER_LINUX_H_
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerLinux : public ScreenCapturer {
public:
ScreenCapturerLinux();
~ScreenCapturerLinux();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
enum class BackendType { kNone, kX11, kDrm, kWayland };
private:
int InitX11();
int InitDrm();
int InitWayland();
int RefreshWaylandBackend();
bool TryFallbackToDrm(bool show_cursor);
bool TryFallbackToX11(bool show_cursor);
bool TryFallbackToWayland(bool show_cursor);
void UpdateAliasesFromBackend(ScreenCapturer* backend);
std::string MapDisplayName(const char* display_name) const;
private:
std::unique_ptr<ScreenCapturer> impl_;
BackendType backend_ = BackendType::kNone;
int fps_ = 60;
cb_desktop_data callback_;
cb_desktop_data callback_orig_;
std::vector<DisplayInfo> canonical_displays_;
mutable std::mutex alias_mutex_;
std::unordered_map<std::string, std::string> label_alias_;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,242 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#if !CROSSDESK_WAYLAND_BUILD_ENABLED
#error "Wayland capturer requires USE_WAYLAND=true and Wayland development headers"
#endif
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <thread>
#include "platform.h"
#include "rd_log.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
namespace {
int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
struct PipeWireRecoveryConfig {
ScreenCapturerWayland::PipeWireConnectMode mode;
bool relaxed_connect = false;
};
constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200);
} // namespace
ScreenCapturerWayland::ScreenCapturerWayland() {}
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!IsWaylandSession()) {
LOG_ERROR("Wayland screen capturer requires a Wayland session");
return -1;
}
if (!cb) {
LOG_ERROR("Wayland screen capturer callback is null");
return -1;
}
if (!CheckPortalAvailability()) {
LOG_ERROR("xdg-desktop-portal screencast service is unavailable");
return -1;
}
fps_ = fps;
callback_ = cb;
pointer_granted_ = false;
shared_session_registered_ = false;
display_info_list_.clear();
display_info_list_.push_back(
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
monitor_index_ = 0;
initial_monitor_index_ = 0;
frame_width_ = kFallbackWidth;
frame_height_ = kFallbackHeight;
frame_stride_ = kFallbackWidth * 4;
logical_width_ = kFallbackWidth;
logical_height_ = kFallbackHeight;
y_plane_.resize(kFallbackWidth * kFallbackHeight);
uv_plane_.resize((kFallbackWidth / 2) * (kFallbackHeight / 2) * 2);
return 0;
}
int ScreenCapturerWayland::Destroy() {
Stop();
y_plane_.clear();
uv_plane_.clear();
display_info_list_.clear();
callback_ = nullptr;
return 0;
}
int ScreenCapturerWayland::Start(bool show_cursor) {
if (running_) {
return 0;
}
show_cursor_ = show_cursor;
paused_ = false;
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(0);
pipewire_last_frame_ms_.store(0);
running_ = true;
thread_ = std::thread([this]() { Run(); });
return 0;
}
int ScreenCapturerWayland::Stop() {
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
return 0;
}
int ScreenCapturerWayland::Pause([[maybe_unused]] int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerWayland::Resume([[maybe_unused]] int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerWayland::SwitchTo(int monitor_index) {
if (monitor_index != 0) {
LOG_WARN("Wayland screencast currently supports one logical display");
return -1;
}
monitor_index_ = 0;
return 0;
}
int ScreenCapturerWayland::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerWayland::GetDisplayInfoList() {
return display_info_list_;
}
void ScreenCapturerWayland::Run() {
static constexpr PipeWireRecoveryConfig kRecoveryConfigs[] = {
{PipeWireConnectMode::kTargetObject, false},
{PipeWireConnectMode::kAny, true},
{PipeWireConnectMode::kNodeId, false},
{PipeWireConnectMode::kNodeId, true},
};
int recovery_index = 0;
auto setup_pipewire = [this, &recovery_index]() -> bool {
const auto& config = kRecoveryConfigs[recovery_index];
return OpenPipeWireRemote() &&
SetupPipeWireStream(config.relaxed_connect, config.mode);
};
auto setup_pipeline = [this, &setup_pipewire]() -> bool {
return ConnectSessionBus() && CreatePortalSession() &&
SelectPortalDevices() && SelectPortalSource() &&
StartPortalSession() && setup_pipewire();
};
if (!setup_pipeline()) {
running_ = false;
CleanupPipeWire();
ClosePortalSession();
CleanupDbus();
return;
}
while (running_) {
if (!paused_) {
const int64_t now = NowMs();
const int64_t stream_start = pipewire_stream_start_ms_.load();
const int64_t last_frame = pipewire_last_frame_ms_.load();
const bool format_ready = pipewire_format_ready_.load();
const bool format_timeout =
stream_start > 0 && !format_ready && (now - stream_start) > 1200;
const bool first_frame_timeout =
stream_start > 0 && format_ready && last_frame == 0 &&
(now - stream_start) > 4000;
const bool frame_stall = last_frame > 0 && (now - last_frame) > 5000;
if (format_timeout || first_frame_timeout || frame_stall) {
if (recovery_index + 1 >=
static_cast<int>(sizeof(kRecoveryConfigs) /
sizeof(kRecoveryConfigs[0]))) {
LOG_ERROR(
"Wayland capture stalled and recovery limit reached, "
"format_ready={}, stream_start={}, last_frame={}, attempts={}",
format_ready, stream_start, last_frame, recovery_index);
running_ = false;
break;
}
++recovery_index;
const char* reason = format_timeout
? "format-timeout"
: (first_frame_timeout ? "first-frame-timeout"
: "frame-stall");
const auto& config = kRecoveryConfigs[recovery_index];
LOG_WARN(
"Wayland capture stalled ({}) - retrying PipeWire only, "
"attempt {}/{}, mode={}, relaxed_connect={}",
reason, recovery_index,
static_cast<int>(sizeof(kRecoveryConfigs) /
sizeof(kRecoveryConfigs[0])) -
1,
config.mode == PipeWireConnectMode::kTargetObject
? "target-object"
: (config.mode == PipeWireConnectMode::kNodeId ? "node-id"
: "any"),
config.relaxed_connect);
CleanupPipeWire();
if (!setup_pipewire()) {
LOG_ERROR("Wayland PipeWire-only recovery failed at attempt {}",
recovery_index);
running_ = false;
break;
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
CleanupPipeWire();
if (!session_handle_.empty()) {
std::this_thread::sleep_for(kPipeWireCloseSettleDelay);
}
ClosePortalSession();
CleanupDbus();
}
} // namespace crossdesk

View File

@@ -0,0 +1,110 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WAYLAND_H_
#define _SCREEN_CAPTURER_WAYLAND_H_
struct DBusConnection;
struct pw_context;
struct pw_core;
struct pw_stream;
struct pw_thread_loop;
#include <atomic>
#include <cstdint>
#include <string>
#include <thread>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerWayland : public ScreenCapturer {
public:
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
public:
ScreenCapturerWayland();
~ScreenCapturerWayland();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
bool CheckPortalAvailability() const;
bool ConnectSessionBus();
bool CreatePortalSession();
bool SelectPortalDevices();
bool SelectPortalSource();
bool StartPortalSession();
bool OpenPipeWireRemote();
bool SetupPipeWireStream(bool relaxed_connect, PipeWireConnectMode mode);
void Run();
void CleanupPipeWire();
void CleanupDbus();
void ClosePortalSession();
void HandlePipeWireBuffer();
void UpdateDisplayGeometry(int width, int height);
private:
static constexpr int kFallbackWidth = 1920;
static constexpr int kFallbackHeight = 1080;
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
std::atomic<bool> pipewire_format_ready_{false};
std::atomic<int64_t> pipewire_stream_start_ms_{0};
std::atomic<int64_t> pipewire_last_frame_ms_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
DBusConnection* dbus_connection_ = nullptr;
std::string session_handle_;
std::string display_name_ = "WAYLAND0";
uint32_t pipewire_node_id_ = 0;
int pipewire_fd_ = -1;
pw_thread_loop* pw_thread_loop_ = nullptr;
pw_context* pw_context_ = nullptr;
pw_core* pw_core_ = nullptr;
pw_stream* pw_stream_ = nullptr;
void* stream_listener_ = nullptr;
bool pipewire_initialized_ = false;
bool pipewire_thread_loop_started_ = false;
bool pointer_granted_ = false;
bool shared_session_registered_ = false;
uint32_t spa_video_format_ = 0;
int frame_width_ = 0;
int frame_height_ = 0;
int frame_stride_ = 0;
int logical_width_ = 0;
int logical_height_ = 0;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,46 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WAYLAND_BUILD_H_
#define _SCREEN_CAPTURER_WAYLAND_BUILD_H_
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#define CROSSDESK_WAYLAND_BUILD_ENABLED 1
#include <dbus/dbus.h>
#include <pipewire/keys.h>
#include <pipewire/pipewire.h>
#include <pipewire/stream.h>
#include <pipewire/thread-loop.h>
#include <spa/param/param.h>
#include <spa/param/format-utils.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/video/raw.h>
#include <spa/buffer/meta.h>
#include <spa/utils/result.h>
#if defined(__has_include)
#if __has_include(<spa/param/buffers.h>)
#include <spa/param/buffers.h>
#endif
#endif
#define CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS 1u
#define CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS 2u
#define CROSSDESK_SPA_PARAM_BUFFERS_SIZE 3u
#define CROSSDESK_SPA_PARAM_BUFFERS_STRIDE 4u
#define CROSSDESK_SPA_PARAM_META_TYPE 1u
#define CROSSDESK_SPA_PARAM_META_SIZE 2u
#else
#define CROSSDESK_WAYLAND_BUILD_ENABLED 0
#endif
#endif

View File

@@ -0,0 +1,630 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#if CROSSDESK_WAYLAND_BUILD_ENABLED
#include <chrono>
#include <cstdint>
#include <thread>
#include <unistd.h>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
const char* PipeWireFormatName(uint32_t spa_format) {
switch (spa_format) {
case SPA_VIDEO_FORMAT_BGRx:
return "BGRx";
case SPA_VIDEO_FORMAT_BGRA:
return "BGRA";
#ifdef SPA_VIDEO_FORMAT_RGBx
case SPA_VIDEO_FORMAT_RGBx:
return "RGBx";
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
case SPA_VIDEO_FORMAT_RGBA:
return "RGBA";
#endif
default:
return "unsupported";
}
}
const char* PipeWireConnectModeName(
ScreenCapturerWayland::PipeWireConnectMode mode) {
switch (mode) {
case ScreenCapturerWayland::PipeWireConnectMode::kTargetObject:
return "target-object";
case ScreenCapturerWayland::PipeWireConnectMode::kNodeId:
return "node-id";
case ScreenCapturerWayland::PipeWireConnectMode::kAny:
return "any";
default:
return "unknown";
}
}
int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
struct PipeWireTargetLookupState {
pw_thread_loop* loop = nullptr;
uint32_t target_node_id = 0;
int sync_seq = -1;
bool done = false;
bool found = false;
std::string object_serial;
};
std::string LookupPipeWireTargetObjectSerial(pw_core* core,
pw_thread_loop* loop,
uint32_t node_id) {
if (!core || !loop || node_id == 0) {
return "";
}
PipeWireTargetLookupState state;
state.loop = loop;
state.target_node_id = node_id;
pw_registry* registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
if (!registry) {
return "";
}
spa_hook registry_listener{};
spa_hook core_listener{};
pw_registry_events registry_events{};
registry_events.version = PW_VERSION_REGISTRY_EVENTS;
registry_events.global =
[](void* userdata, uint32_t id, uint32_t permissions, const char* type,
uint32_t version, const spa_dict* props) {
(void)permissions;
(void)version;
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state || !props || id != state->target_node_id || !type) {
return;
}
if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) {
return;
}
const char* object_serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
if (!object_serial || object_serial[0] == '\0') {
object_serial = spa_dict_lookup(props, "object.serial");
}
if (!object_serial || object_serial[0] == '\0') {
return;
}
state->object_serial = object_serial;
state->found = true;
};
pw_core_events core_events{};
core_events.version = PW_VERSION_CORE_EVENTS;
core_events.done = [](void* userdata, uint32_t id, int seq) {
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state || id != PW_ID_CORE || seq != state->sync_seq) {
return;
}
state->done = true;
pw_thread_loop_signal(state->loop, false);
};
core_events.error = [](void* userdata, uint32_t id, int seq, int res,
const char* message) {
(void)id;
(void)seq;
(void)res;
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state) {
return;
}
LOG_WARN("PipeWire registry lookup error: {}",
message ? message : "unknown");
state->done = true;
pw_thread_loop_signal(state->loop, false);
};
pw_registry_add_listener(registry, &registry_listener, &registry_events,
&state);
pw_core_add_listener(core, &core_listener, &core_events, &state);
state.sync_seq = pw_core_sync(core, PW_ID_CORE, 0);
while (!state.done) {
pw_thread_loop_wait(loop);
}
spa_hook_remove(&registry_listener);
spa_hook_remove(&core_listener);
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(registry));
return state.found ? state.object_serial : "";
}
int BytesPerPixel(uint32_t spa_format) {
switch (spa_format) {
case SPA_VIDEO_FORMAT_BGRx:
case SPA_VIDEO_FORMAT_BGRA:
#ifdef SPA_VIDEO_FORMAT_RGBx
case SPA_VIDEO_FORMAT_RGBx:
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
case SPA_VIDEO_FORMAT_RGBA:
#endif
return 4;
default:
return 0;
}
}
} // namespace
bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
PipeWireConnectMode mode) {
if (pipewire_fd_ < 0 || pipewire_node_id_ == 0) {
return false;
}
if (!pipewire_initialized_) {
pw_init(nullptr, nullptr);
pipewire_initialized_ = true;
}
pw_thread_loop_ = pw_thread_loop_new("crossdesk-wayland-capture", nullptr);
if (!pw_thread_loop_) {
LOG_ERROR("Failed to create PipeWire thread loop");
return false;
}
if (pw_thread_loop_start(pw_thread_loop_) < 0) {
LOG_ERROR("Failed to start PipeWire thread loop");
CleanupPipeWire();
return false;
}
pipewire_thread_loop_started_ = true;
pw_thread_loop_lock(pw_thread_loop_);
pw_context_ =
pw_context_new(pw_thread_loop_get_loop(pw_thread_loop_), nullptr, 0);
if (!pw_context_) {
LOG_ERROR("Failed to create PipeWire context");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
pw_core_ = pw_context_connect_fd(pw_context_, pipewire_fd_, nullptr, 0);
if (!pw_core_) {
LOG_ERROR("Failed to connect to PipeWire remote");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
pipewire_fd_ = -1;
pw_properties* stream_props =
pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY,
"Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
if (!stream_props) {
LOG_ERROR("Failed to allocate PipeWire stream properties");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
std::string target_object_serial;
if (mode == PipeWireConnectMode::kTargetObject) {
target_object_serial =
LookupPipeWireTargetObjectSerial(pw_core_, pw_thread_loop_,
pipewire_node_id_);
if (!target_object_serial.empty()) {
pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT,
target_object_serial.c_str());
LOG_INFO("PipeWire target object serial for node {} is {}",
pipewire_node_id_, target_object_serial);
} else {
LOG_WARN("PipeWire target object serial lookup failed for node {}, "
"falling back to direct target id in target-object mode",
pipewire_node_id_);
}
}
pw_stream_ = pw_stream_new(pw_core_, "CrossDesk Wayland Capture",
stream_props);
if (!pw_stream_) {
LOG_ERROR("Failed to create PipeWire stream");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
auto* listener = new spa_hook();
stream_listener_ = listener;
static const pw_stream_events stream_events = [] {
pw_stream_events events{};
events.version = PW_VERSION_STREAM_EVENTS;
events.state_changed =
[](void* userdata, enum pw_stream_state old_state,
enum pw_stream_state state, const char* error_message) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (!self) {
return;
}
if (state == PW_STREAM_STATE_ERROR) {
LOG_ERROR("PipeWire stream error: {}",
error_message ? error_message : "unknown");
self->running_ = false;
return;
}
LOG_INFO("PipeWire stream state: {} -> {}",
pw_stream_state_as_string(old_state),
pw_stream_state_as_string(state));
};
events.param_changed =
[](void* userdata, uint32_t id, const struct spa_pod* param) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (!self || id != SPA_PARAM_Format || !param) {
return;
}
spa_video_info_raw info{};
if (spa_format_video_raw_parse(param, &info) < 0) {
LOG_ERROR("Failed to parse PipeWire video format");
return;
}
self->spa_video_format_ = info.format;
self->frame_width_ = static_cast<int>(info.size.width);
self->frame_height_ = static_cast<int>(info.size.height);
self->frame_stride_ = static_cast<int>(info.size.width) * 4;
bool supported_format =
(self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRx) ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRA);
#ifdef SPA_VIDEO_FORMAT_RGBx
supported_format =
supported_format ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBx);
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
supported_format =
supported_format ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBA);
#endif
if (!supported_format) {
LOG_ERROR("Unsupported PipeWire pixel format: {}",
PipeWireFormatName(self->spa_video_format_));
self->running_ = false;
return;
}
const int bytes_per_pixel = BytesPerPixel(self->spa_video_format_);
if (bytes_per_pixel <= 0 || self->frame_width_ <= 0 ||
self->frame_height_ <= 0) {
LOG_ERROR("Invalid PipeWire frame layout: format={}, size={}x{}",
PipeWireFormatName(self->spa_video_format_),
self->frame_width_, self->frame_height_);
self->running_ = false;
return;
}
self->frame_stride_ = self->frame_width_ * bytes_per_pixel;
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[2];
uint32_t param_count = 0;
params[param_count++] = reinterpret_cast<const spa_pod*>(
spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS,
SPA_POD_CHOICE_RANGE_Int(8, 4, 16),
CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS, SPA_POD_Int(1),
CROSSDESK_SPA_PARAM_BUFFERS_SIZE,
SPA_POD_CHOICE_RANGE_Int(self->frame_stride_ *
self->frame_height_,
self->frame_stride_ *
self->frame_height_,
self->frame_stride_ *
self->frame_height_),
CROSSDESK_SPA_PARAM_BUFFERS_STRIDE,
SPA_POD_CHOICE_RANGE_Int(self->frame_stride_,
self->frame_stride_,
self->frame_stride_)));
params[param_count++] = reinterpret_cast<const spa_pod*>(
spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
CROSSDESK_SPA_PARAM_META_TYPE, SPA_POD_Id(SPA_META_Header),
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_Int(sizeof(struct spa_meta_header))));
if (self->pw_stream_) {
pw_stream_update_params(self->pw_stream_, params, param_count);
}
self->pipewire_format_ready_.store(true);
const int pointer_width =
self->logical_width_ > 0 ? self->logical_width_ : self->frame_width_;
const int pointer_height = self->logical_height_ > 0
? self->logical_height_
: self->frame_height_;
self->UpdateDisplayGeometry(pointer_width, pointer_height);
LOG_INFO(
"PipeWire video format: {}, {}x{} stride={} (pointer space {}x{})",
PipeWireFormatName(self->spa_video_format_),
self->frame_width_, self->frame_height_, self->frame_stride_,
pointer_width, pointer_height);
};
events.process = [](void* userdata) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (self) {
self->HandlePipeWireBuffer();
}
};
return events;
}();
pw_stream_add_listener(pw_stream_, listener, &stream_events, this);
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(NowMs());
pipewire_last_frame_ms_.store(0);
uint8_t buffer[4096];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[8];
int param_count = 0;
const spa_rectangle fixed_size{
static_cast<uint32_t>(logical_width_ > 0 ? logical_width_ : kFallbackWidth),
static_cast<uint32_t>(logical_height_ > 0 ? logical_height_
: kFallbackHeight)};
const spa_rectangle min_size{1u, 1u};
const spa_rectangle max_size{16384u, 16384u};
if (!relaxed_connect) {
auto add_format_param = [&](uint32_t spa_format) {
if (param_count >= static_cast<int>(sizeof(params) / sizeof(params[0]))) {
return;
}
params[param_count++] =
reinterpret_cast<const spa_pod*>(spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_VIDEO_format, SPA_POD_Id(spa_format),
SPA_FORMAT_VIDEO_size,
SPA_POD_CHOICE_RANGE_Rectangle(&fixed_size, &min_size,
&max_size)));
};
add_format_param(SPA_VIDEO_FORMAT_BGRx);
add_format_param(SPA_VIDEO_FORMAT_BGRA);
#ifdef SPA_VIDEO_FORMAT_RGBx
add_format_param(SPA_VIDEO_FORMAT_RGBx);
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
add_format_param(SPA_VIDEO_FORMAT_RGBA);
#endif
if (param_count == 0) {
LOG_ERROR("No valid PipeWire format params were built");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
} else {
LOG_INFO("PipeWire stream using relaxed format negotiation");
}
uint32_t target_id = PW_ID_ANY;
if (mode == PipeWireConnectMode::kNodeId ||
(mode == PipeWireConnectMode::kTargetObject &&
target_object_serial.empty())) {
target_id = pipewire_node_id_;
}
LOG_INFO(
"PipeWire connecting stream: mode={}, node_id={}, target_id={}, "
"target_object_serial={}, relaxed_connect={}, param_count={}, "
"requested_size={}x{}",
PipeWireConnectModeName(mode), pipewire_node_id_, target_id,
target_object_serial.empty() ? "none" : target_object_serial.c_str(),
relaxed_connect, param_count, fixed_size.width, fixed_size.height);
const int ret = pw_stream_connect(
pw_stream_, PW_DIRECTION_INPUT, target_id,
static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS),
param_count > 0 ? params : nullptr, static_cast<uint32_t>(param_count));
pw_thread_loop_unlock(pw_thread_loop_);
if (ret < 0) {
LOG_ERROR("pw_stream_connect failed: {}", spa_strerror(ret));
CleanupPipeWire();
return false;
}
return true;
}
void ScreenCapturerWayland::CleanupPipeWire() {
const bool need_lock = pw_thread_loop_ &&
(pw_stream_ != nullptr || pw_core_ != nullptr ||
pw_context_ != nullptr);
if (need_lock) {
pw_thread_loop_lock(pw_thread_loop_);
}
if (pw_stream_) {
pw_stream_set_active(pw_stream_, false);
pw_stream_disconnect(pw_stream_);
}
if (stream_listener_) {
spa_hook_remove(static_cast<spa_hook*>(stream_listener_));
delete static_cast<spa_hook*>(stream_listener_);
stream_listener_ = nullptr;
}
if (pw_stream_) {
pw_stream_destroy(pw_stream_);
pw_stream_ = nullptr;
}
if (pw_core_) {
pw_core_disconnect(pw_core_);
pw_core_ = nullptr;
}
if (pw_context_) {
pw_context_destroy(pw_context_);
pw_context_ = nullptr;
}
if (need_lock) {
pw_thread_loop_unlock(pw_thread_loop_);
}
if (pw_thread_loop_) {
if (pipewire_thread_loop_started_) {
pw_thread_loop_stop(pw_thread_loop_);
pipewire_thread_loop_started_ = false;
}
pw_thread_loop_destroy(pw_thread_loop_);
pw_thread_loop_ = nullptr;
}
if (pipewire_fd_ >= 0) {
close(pipewire_fd_);
pipewire_fd_ = -1;
}
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(0);
pipewire_last_frame_ms_.store(0);
if (pipewire_initialized_) {
pw_deinit();
pipewire_initialized_ = false;
}
}
void ScreenCapturerWayland::HandlePipeWireBuffer() {
if (!pw_stream_) {
return;
}
pw_buffer* buffer = pw_stream_dequeue_buffer(pw_stream_);
if (!buffer) {
return;
}
auto requeue = [&]() { pw_stream_queue_buffer(pw_stream_, buffer); };
if (paused_) {
requeue();
return;
}
spa_buffer* spa_buffer = buffer->buffer;
if (!spa_buffer || spa_buffer->n_datas == 0 || !spa_buffer->datas[0].data) {
requeue();
return;
}
const spa_data& data = spa_buffer->datas[0];
if (!data.chunk) {
requeue();
return;
}
if (frame_width_ <= 1 || frame_height_ <= 1) {
requeue();
return;
}
uint8_t* src = static_cast<uint8_t*>(data.data);
src += data.chunk->offset;
int stride = frame_stride_;
if (data.chunk->stride > 0) {
stride = data.chunk->stride;
} else if (stride <= 0) {
stride = frame_width_ * 4;
}
int even_width = frame_width_ & ~1;
int even_height = frame_height_ & ~1;
if (even_width <= 0 || even_height <= 0) {
requeue();
return;
}
const size_t y_size = static_cast<size_t>(even_width) * even_height;
const size_t uv_size = y_size / 2;
if (y_plane_.size() != y_size) {
y_plane_.resize(y_size);
}
if (uv_plane_.size() != uv_size) {
uv_plane_.resize(uv_size);
}
libyuv::ARGBToNV12(src, stride, y_plane_.data(), even_width,
uv_plane_.data(), even_width, even_width, even_height);
std::vector<uint8_t> nv12;
nv12.reserve(y_plane_.size() + uv_plane_.size());
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
even_height, display_name_.c_str());
}
pipewire_last_frame_ms_.store(NowMs());
requeue();
}
void ScreenCapturerWayland::UpdateDisplayGeometry(int width, int height) {
if (width <= 0 || height <= 0) {
return;
}
void* stream_handle =
reinterpret_cast<void*>(static_cast<uintptr_t>(pipewire_node_id_));
if (display_info_list_.empty()) {
display_info_list_.push_back(
DisplayInfo(stream_handle, display_name_, true, 0, 0, width, height));
return;
}
auto& display = display_info_list_[0];
display.handle = stream_handle;
display.left = 0;
display.top = 0;
display.right = width;
display.bottom = height;
display.width = width;
display.height = height;
}
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,816 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#include "wayland_portal_shared.h"
#if CROSSDESK_WAYLAND_BUILD_ENABLED
#include <chrono>
#include <cstring>
#include <functional>
#include <string>
#include <unistd.h>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop";
constexpr const char* kPortalRemoteDesktopInterface =
"org.freedesktop.portal.RemoteDesktop";
constexpr const char* kPortalScreenCastInterface =
"org.freedesktop.portal.ScreenCast";
constexpr const char* kPortalRequestInterface =
"org.freedesktop.portal.Request";
constexpr const char* kPortalSessionInterface =
"org.freedesktop.portal.Session";
constexpr const char* kPortalRequestPathPrefix =
"/org/freedesktop/portal/desktop/request/";
constexpr const char* kPortalSessionPathPrefix =
"/org/freedesktop/portal/desktop/session/";
constexpr uint32_t kScreenCastSourceMonitor = 1u;
constexpr uint32_t kCursorModeHidden = 1u;
constexpr uint32_t kCursorModeEmbedded = 2u;
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
std::string MakeToken(const char* prefix) {
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
return std::string(prefix) + "_" + std::to_string(now);
}
void LogDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
void AppendDictEntryString(DBusMessageIter* dict, const char* key,
const std::string& value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
const char* value_cstr = value.c_str();
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryUint32(DBusMessageIter* dict, const char* key,
uint32_t value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryBool(DBusMessageIter* dict, const char* key, bool value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_bool_t bool_value = value ? TRUE : FALSE;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "b", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &bool_value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
bool ReadIntLike(DBusMessageIter* iter, int* value) {
if (!iter || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(iter);
if (type == DBUS_TYPE_INT32) {
int32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = static_cast<int>(temp);
return true;
}
if (type == DBUS_TYPE_UINT32) {
uint32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = static_cast<int>(temp);
return true;
}
return false;
}
bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) {
if (!variant || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(variant);
if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) {
const char* temp = nullptr;
dbus_message_iter_get_basic(variant, &temp);
if (temp && temp[0] != '\0') {
*value = temp;
return true;
}
}
return false;
}
std::string BuildSessionHandleFromRequestPath(
const std::string& request_path, const std::string& session_handle_token) {
if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 ||
session_handle_token.empty()) {
return "";
}
const size_t sender_start = strlen(kPortalRequestPathPrefix);
const size_t token_sep = request_path.find('/', sender_start);
if (token_sep == std::string::npos || token_sep <= sender_start) {
return "";
}
const std::string sender = request_path.substr(sender_start,
token_sep - sender_start);
if (sender.empty()) {
return "";
}
return std::string(kPortalSessionPathPrefix) + sender + "/" +
session_handle_token;
}
struct PortalResponseState {
std::string request_path;
bool received = false;
DBusMessage* message = nullptr;
};
DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
auto* state = static_cast<PortalResponseState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->request_path != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (state->message) {
dbus_message_unref(state->message);
state->message = nullptr;
}
state->message = dbus_message_ref(message);
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
DBusMessage* WaitForPortalResponse(DBusConnection* connection,
const std::string& request_path,
const std::atomic<bool>& running,
int timeout_ms = 120000) {
if (!connection || request_path.empty()) {
return nullptr;
}
PortalResponseState state;
state.request_path = request_path;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalRequestInterface) +
"',member='Response',path='" + request_path + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_add_match", &error);
dbus_error_free(&error);
return nullptr;
}
dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state,
nullptr);
auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (running.load() && !state.received &&
std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
return state.message;
}
bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) {
if (!reply || !request_path) {
return false;
}
const char* path = nullptr;
DBusError error;
dbus_error_init(&error);
const dbus_bool_t ok = dbus_message_get_args(
reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
if (!ok || !path) {
LogDbusError("dbus_message_get_args(request_path)", &error);
dbus_error_free(&error);
return false;
}
*request_path = path;
return true;
}
bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code,
DBusMessageIter* results_array) {
if (!message || !response_code || !results_array) {
return false;
}
DBusMessageIter iter;
if (!dbus_message_iter_init(message, &iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
return false;
}
dbus_message_iter_get_basic(&iter, response_code);
if (!dbus_message_iter_next(&iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
return false;
}
*results_array = iter;
return true;
}
bool SendPortalRequestAndHandleResponse(
DBusConnection* connection, const char* interface_name,
const char* method_name,
const char* action_name,
const std::function<bool(DBusMessage*)>& append_message_args,
const std::atomic<bool>& running,
const std::function<bool(uint32_t, DBusMessageIter*)>& handle_results,
std::string* request_path_out = nullptr) {
if (!connection || !interface_name || interface_name[0] == '\0' ||
!method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message =
dbus_message_new_method_call(kPortalBusName, kPortalObjectPath,
interface_name, method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
if (append_message_args && !append_message_args(message)) {
dbus_message_unref(message);
LOG_ERROR("{} arguments are malformed", method_name);
return false;
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(connection, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(action_name ? action_name : method_name, &error);
dbus_error_free(&error);
return false;
}
std::string request_path;
const bool got_request_path = ExtractRequestPath(reply, &request_path);
dbus_message_unref(reply);
if (!got_request_path) {
return false;
}
if (request_path_out) {
*request_path_out = request_path;
}
DBusMessage* response =
WaitForPortalResponse(connection, request_path, running);
if (!response) {
LOG_ERROR("Timed out waiting for {} response", method_name);
return false;
}
uint32_t response_code = 1;
DBusMessageIter results;
const bool parsed = ExtractPortalResponse(response, &response_code, &results);
if (!parsed) {
dbus_message_unref(response);
LOG_ERROR("{} response was malformed", method_name);
return false;
}
const bool ok = handle_results ? handle_results(response_code, &results)
: (response_code == 0);
dbus_message_unref(response);
return ok;
}
} // namespace
bool ScreenCapturerWayland::CheckPortalAvailability() const {
DBusError error;
dbus_error_init(&error);
DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
if (!connection) {
LogDbusError("dbus_bus_get", &error);
dbus_error_free(&error);
return false;
}
const dbus_bool_t has_owner = dbus_bus_name_has_owner(
connection, kPortalBusName, &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_name_has_owner", &error);
dbus_error_free(&error);
dbus_connection_unref(connection);
return false;
}
dbus_connection_unref(connection);
return has_owner == TRUE;
}
bool ScreenCapturerWayland::ConnectSessionBus() {
if (dbus_connection_) {
return true;
}
DBusError error;
dbus_error_init(&error);
dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
if (!dbus_connection_) {
LogDbusError("dbus_bus_get_private", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE);
return true;
}
bool ScreenCapturerWayland::CreatePortalSession() {
if (!dbus_connection_) {
return false;
}
const std::string session_handle_token = MakeToken("crossdesk_session");
std::string request_path;
const bool ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession",
"CreateSession",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "session_handle_token",
session_handle_token);
AppendDictEntryString(&options, "handle_token", MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_,
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("CreateSession was denied or malformed, response={}",
response_code);
return false;
}
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT &&
strcmp(key, "session_handle") == 0) {
DBusMessageIter variant;
std::string parsed_handle;
dbus_message_iter_recurse(&entry, &variant);
if (ReadPathLikeVariant(&variant, &parsed_handle) &&
!parsed_handle.empty()) {
session_handle_ = parsed_handle;
break;
}
}
}
dbus_message_iter_next(&dict);
}
return true;
},
&request_path);
if (!ok) {
return false;
}
if (session_handle_.empty()) {
const std::string fallback_handle = BuildSessionHandleFromRequestPath(
request_path, session_handle_token);
if (!fallback_handle.empty()) {
LOG_WARN(
"CreateSession response missing session_handle, using derived handle "
"{}",
fallback_handle);
session_handle_ = fallback_handle;
}
}
if (session_handle_.empty()) {
LOG_ERROR("CreateSession response did not include a session handle");
return false;
}
return true;
}
bool ScreenCapturerWayland::SelectPortalSource() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
"SelectSources",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
AppendDictEntryBool(&options, "multiple", false);
AppendDictEntryUint32(
&options, "cursor_mode",
show_cursor_ ? kCursorModeEmbedded : kCursorModeHidden);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_, [](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("SelectSources was denied or malformed, response={}",
response_code);
return false;
}
return true;
});
}
bool ScreenCapturerWayland::SelectPortalDevices() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices",
"SelectDevices",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kRemoteDesktopDevicePointer);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_, [](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("SelectDevices was denied or malformed, response={}",
response_code);
return false;
}
return true;
});
}
bool ScreenCapturerWayland::StartPortalSession() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
const char* parent_window = "";
pointer_granted_ = false;
const bool ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "handle_token", MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_,
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("Start was denied or malformed, response={}", response_code);
return false;
}
uint32_t granted_devices = 0;
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&entry, &variant);
if (strcmp(key, "devices") == 0) {
int granted_devices_int = 0;
if (ReadIntLike(&variant, &granted_devices_int) &&
granted_devices_int >= 0) {
granted_devices = static_cast<uint32_t>(granted_devices_int);
}
} else if (strcmp(key, "streams") == 0) {
DBusMessageIter streams;
dbus_message_iter_recurse(&variant, &streams);
if (dbus_message_iter_get_arg_type(&streams) == DBUS_TYPE_STRUCT) {
DBusMessageIter stream;
dbus_message_iter_recurse(&streams, &stream);
if (dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_UINT32) {
dbus_message_iter_get_basic(&stream, &pipewire_node_id_);
}
if (dbus_message_iter_next(&stream) &&
dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_ARRAY) {
DBusMessageIter props;
int stream_width = 0;
int stream_height = 0;
int logical_width = 0;
int logical_height = 0;
dbus_message_iter_recurse(&stream, &props);
while (dbus_message_iter_get_arg_type(&props) !=
DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&props) ==
DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter prop_entry;
dbus_message_iter_recurse(&props, &prop_entry);
const char* prop_key = nullptr;
dbus_message_iter_get_basic(&prop_entry, &prop_key);
if (prop_key && dbus_message_iter_next(&prop_entry) &&
dbus_message_iter_get_arg_type(&prop_entry) ==
DBUS_TYPE_VARIANT) {
DBusMessageIter prop_variant;
dbus_message_iter_recurse(&prop_entry, &prop_variant);
if (dbus_message_iter_get_arg_type(&prop_variant) ==
DBUS_TYPE_STRUCT) {
DBusMessageIter size_iter;
int width = 0;
int height = 0;
dbus_message_iter_recurse(&prop_variant, &size_iter);
if (ReadIntLike(&size_iter, &width) &&
dbus_message_iter_next(&size_iter) &&
ReadIntLike(&size_iter, &height)) {
if (strcmp(prop_key, "logical_size") == 0) {
logical_width = width;
logical_height = height;
} else if (strcmp(prop_key, "size") == 0) {
stream_width = width;
stream_height = height;
}
}
}
}
}
dbus_message_iter_next(&props);
}
const int picked_width =
logical_width > 0 ? logical_width : stream_width;
const int picked_height =
logical_height > 0 ? logical_height : stream_height;
LOG_INFO(
"Wayland portal stream geometry: stream_size={}x{}, "
"logical_size={}x{}, pointer_space={}x{}",
stream_width, stream_height, logical_width,
logical_height, picked_width, picked_height);
if (logical_width > 0 && logical_height > 0) {
logical_width_ = logical_width;
logical_height_ = logical_height;
UpdateDisplayGeometry(logical_width_, logical_height_);
} else if (stream_width > 0 && stream_height > 0) {
logical_width_ = stream_width;
logical_height_ = stream_height;
UpdateDisplayGeometry(logical_width_, logical_height_);
}
}
}
}
}
}
dbus_message_iter_next(&dict);
}
pointer_granted_ =
(granted_devices & kRemoteDesktopDevicePointer) != 0;
return true;
});
if (!ok) {
return false;
}
if (pipewire_node_id_ == 0) {
LOG_ERROR("Start response did not include a PipeWire node id");
return false;
}
if (!pointer_granted_) {
LOG_ERROR("Start response did not grant pointer control");
return false;
}
shared_session_registered_ = PublishSharedWaylandPortalSession(
SharedWaylandPortalSessionInfo{
dbus_connection_, session_handle_, pipewire_node_id_, logical_width_,
logical_height_, pointer_granted_});
if (!shared_session_registered_) {
LOG_WARN("Failed to publish shared Wayland portal session");
}
LOG_INFO("Wayland screencast ready, node_id={}", pipewire_node_id_);
return true;
}
bool ScreenCapturerWayland::OpenPipeWireRemote() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, kPortalScreenCastInterface,
"OpenPipeWireRemote");
if (!message) {
LOG_ERROR("Failed to allocate OpenPipeWireRemote message");
return false;
}
DBusMessageIter iter;
DBusMessageIter options;
const char* session_handle = session_handle_.c_str();
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &options);
dbus_message_iter_close_container(&iter, &options);
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_connection_, message, -1,
&error);
dbus_message_unref(message);
if (!reply) {
LogDbusError("OpenPipeWireRemote", &error);
dbus_error_free(&error);
return false;
}
DBusMessageIter reply_iter;
if (!dbus_message_iter_init(reply, &reply_iter) ||
dbus_message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_UNIX_FD) {
LOG_ERROR("OpenPipeWireRemote returned an unexpected payload");
dbus_message_unref(reply);
return false;
}
int received_fd = -1;
dbus_message_iter_get_basic(&reply_iter, &received_fd);
dbus_message_unref(reply);
if (received_fd < 0) {
LOG_ERROR("OpenPipeWireRemote returned an invalid fd");
return false;
}
pipewire_fd_ = dup(received_fd);
if (pipewire_fd_ < 0) {
LOG_ERROR("Failed to duplicate PipeWire remote fd");
return false;
}
return true;
}
void ScreenCapturerWayland::CleanupDbus() {
if (!dbus_connection_) {
return;
}
if (shared_session_registered_) {
return;
}
dbus_connection_close(dbus_connection_);
dbus_connection_unref(dbus_connection_);
dbus_connection_ = nullptr;
}
void ScreenCapturerWayland::ClosePortalSession() {
if (shared_session_registered_) {
DBusConnection* close_connection = nullptr;
std::string close_session_handle;
ReleaseSharedWaylandPortalSession(&close_connection, &close_session_handle);
shared_session_registered_ = false;
if (close_connection) {
CloseWaylandPortalSessionAndConnection(close_connection,
close_session_handle,
"Session.Close");
}
dbus_connection_ = nullptr;
} else if (dbus_connection_ && !session_handle_.empty()) {
CloseWaylandPortalSessionAndConnection(dbus_connection_, session_handle_,
"Session.Close");
dbus_connection_ = nullptr;
}
session_handle_.clear();
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
pointer_granted_ = false;
}
} // namespace crossdesk
#endif

View File

@@ -1,16 +1,72 @@
#include "screen_capturer_x11.h"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/Xrandr.h>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <thread>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::atomic<int> g_x11_last_error_code{0};
std::mutex g_x11_error_handler_mutex;
int CaptureX11ErrorHandler([[maybe_unused]] Display* display,
XErrorEvent* error_event) {
if (error_event) {
g_x11_last_error_code.store(error_event->error_code);
} else {
g_x11_last_error_code.store(-1);
}
return 0;
}
class ScopedX11ErrorTrap {
public:
explicit ScopedX11ErrorTrap(Display* display)
: display_(display), lock_(g_x11_error_handler_mutex) {
g_x11_last_error_code.store(0);
previous_handler_ = XSetErrorHandler(CaptureX11ErrorHandler);
}
~ScopedX11ErrorTrap() {
if (display_) {
XSync(display_, False);
}
XSetErrorHandler(previous_handler_);
}
int SyncAndGetError() const {
if (display_) {
XSync(display_, False);
}
return g_x11_last_error_code.load();
}
private:
Display* display_ = nullptr;
int (*previous_handler_)(Display*, XErrorEvent*) = nullptr;
std::unique_lock<std::mutex> lock_;
};
} // namespace
ScreenCapturerX11::ScreenCapturerX11() {}
ScreenCapturerX11::~ScreenCapturerX11() { Destroy(); }
int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
Destroy();
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Cannot connect to X server");
@@ -22,6 +78,7 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
if (!screen_res_) {
LOG_ERROR("Failed to get screen resources");
XCloseDisplay(display_);
display_ = nullptr;
return 1;
}
@@ -34,9 +91,21 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
XRRCrtcInfo* crtc_info =
XRRGetCrtcInfo(display_, screen_res_, output_info->crtc);
display_info_list_.push_back(
DisplayInfo((void*)display_, output_info->name, true, crtc_info->x,
crtc_info->y, crtc_info->width, crtc_info->height));
std::string name(output_info->name);
if (name.empty()) {
name = "Display" + std::to_string(i + 1);
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
display_info_list_.push_back(DisplayInfo(
(void*)display_, name, true, crtc_info->x, crtc_info->y,
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height));
XRRFreeCrtcInfo(crtc_info);
}
@@ -52,8 +121,16 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
width_ = attr.width;
height_ = attr.height;
if (width_ % 2 != 0 || height_ % 2 != 0) {
LOG_ERROR("Width and height must be even numbers");
if ((width_ & 1) != 0 || (height_ & 1) != 0) {
LOG_WARN(
"X11 root size {}x{} is not even, aligning down to {}x{} for NV12",
width_, height_, width_ & ~1, height_ & ~1);
width_ &= ~1;
height_ &= ~1;
}
if (width_ <= 1 || height_ <= 1) {
LOG_ERROR("Invalid capture size after alignment: {}x{}", width_, height_);
return -2;
}
@@ -63,6 +140,11 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
y_plane_.resize(width_ * height_);
uv_plane_.resize((width_ / 2) * (height_ / 2) * 2);
if (!ProbeCapture()) {
LOG_ERROR("X11 backend probe failed, XGetImage is not usable");
return -3;
}
return 0;
}
@@ -84,13 +166,28 @@ int ScreenCapturerX11::Destroy() {
return 0;
}
int ScreenCapturerX11::Start() {
int ScreenCapturerX11::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
running_ = true;
paused_ = false;
capture_error_count_ = 0;
thread_ = std::thread([this]() {
using clock = std::chrono::steady_clock;
const auto frame_interval =
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
while (running_) {
if (!paused_) OnFrame();
const auto frame_start = clock::now();
if (!paused_) {
OnFrame();
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
clock::now() - frame_start);
if (elapsed < frame_interval) {
std::this_thread::sleep_for(frame_interval - elapsed);
}
}
});
return 0;
@@ -118,6 +215,10 @@ int ScreenCapturerX11::SwitchTo(int monitor_index) {
return 0;
}
int ScreenCapturerX11::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() {
return display_info_list_;
}
@@ -128,19 +229,58 @@ void ScreenCapturerX11::OnFrame() {
return;
}
if (monitor_index_ < 0 || monitor_index_ >= display_info_list_.size()) {
LOG_ERROR("Invalid monitor index: {}", monitor_index_.load());
const int monitor_index = monitor_index_.load();
if (monitor_index < 0 ||
monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index);
return;
}
left_ = display_info_list_[monitor_index_].left;
top_ = display_info_list_[monitor_index_].top;
width_ = display_info_list_[monitor_index_].width;
height_ = display_info_list_[monitor_index_].height;
left_ = display_info_list_[monitor_index].left;
top_ = display_info_list_[monitor_index].top;
width_ = display_info_list_[monitor_index].width & ~1;
height_ = display_info_list_[monitor_index].height & ~1;
XImage* image = XGetImage(display_, root_, left_, top_, width_, height_,
AllPlanes, ZPixmap);
if (!image) return;
if (width_ <= 1 || height_ <= 1) {
LOG_ERROR("Invalid capture size: {}x{}", width_, height_);
return;
}
XImage* image = nullptr;
int x11_error = 0;
{
ScopedX11ErrorTrap trap(display_);
image = XGetImage(display_, root_, left_, top_, width_, height_, AllPlanes,
ZPixmap);
x11_error = trap.SyncAndGetError();
}
if (x11_error != 0 || !image) {
if (image) {
XDestroyImage(image);
}
++capture_error_count_;
if (capture_error_count_ == 1 || capture_error_count_ % 120 == 0) {
LOG_WARN("X11 capture failed: x11_error={}, image={}, consecutive={}",
x11_error, image ? "valid" : "null", capture_error_count_);
}
return;
}
capture_error_count_ = 0;
// if enable show cursor, draw cursor
if (show_cursor_) {
Window root_return, child_return;
int root_x, root_y, win_x, win_y;
unsigned int mask;
if (XQueryPointer(display_, root_, &root_return, &child_return, &root_x,
&root_y, &win_x, &win_y, &mask)) {
if (root_x >= left_ && root_x < left_ + width_ && root_y >= top_ &&
root_y < top_ + height_) {
DrawCursor(image, root_x - left_, root_y - top_);
}
}
}
bool needs_copy = image->bytes_per_line != width_ * 4;
std::vector<uint8_t> argb_buf;
@@ -157,6 +297,16 @@ void ScreenCapturerX11::OnFrame() {
src_argb = reinterpret_cast<uint8_t*>(image->data);
}
const size_t y_size =
static_cast<size_t>(width_) * static_cast<size_t>(height_);
const size_t uv_size = y_size / 2;
if (y_plane_.size() != y_size) {
y_plane_.resize(y_size);
}
if (uv_plane_.size() != uv_size) {
uv_plane_.resize(uv_size);
}
libyuv::ARGBToNV12(src_argb, width_ * 4, y_plane_.data(), width_,
uv_plane_.data(), width_, width_, height_);
@@ -167,8 +317,115 @@ void ScreenCapturerX11::OnFrame() {
if (callback_) {
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
display_info_list_[monitor_index_].name.c_str());
display_info_list_[monitor_index].name.c_str());
}
XDestroyImage(image);
}
}
void ScreenCapturerX11::DrawCursor(XImage* image, int x, int y) {
if (!display_ || !image) {
return;
}
// check XFixes extension
int event_base, error_base;
if (!XFixesQueryExtension(display_, &event_base, &error_base)) {
return;
}
XFixesCursorImage* cursor_image = XFixesGetCursorImage(display_);
if (!cursor_image) {
return;
}
int cursor_width = cursor_image->width;
int cursor_height = cursor_image->height;
int draw_x = x - cursor_image->xhot;
int draw_y = y - cursor_image->yhot;
// draw cursor on image
for (int cy = 0; cy < cursor_height; ++cy) {
for (int cx = 0; cx < cursor_width; ++cx) {
int img_x = draw_x + cx;
int img_y = draw_y + cy;
if (img_x < 0 || img_x >= image->width || img_y < 0 ||
img_y >= image->height) {
continue;
}
unsigned long cursor_pixel = cursor_image->pixels[cy * cursor_width + cx];
unsigned char a = (cursor_pixel >> 24) & 0xFF;
// if alpha is 0, skip
if (a == 0) {
continue;
}
unsigned long img_pixel = XGetPixel(image, img_x, img_y);
unsigned char img_r = (img_pixel >> 16) & 0xFF;
unsigned char img_g = (img_pixel >> 8) & 0xFF;
unsigned char img_b = img_pixel & 0xFF;
unsigned char cursor_r = (cursor_pixel >> 16) & 0xFF;
unsigned char cursor_g = (cursor_pixel >> 8) & 0xFF;
unsigned char cursor_b = cursor_pixel & 0xFF;
// alpha mix
unsigned char final_r, final_g, final_b;
if (a == 255) {
// if alpha is 255, use cursor color
final_r = cursor_r;
final_g = cursor_g;
final_b = cursor_b;
} else {
float alpha = a / 255.0f;
float inv_alpha = 1.0f - alpha;
final_r =
static_cast<unsigned char>(cursor_r * alpha + img_r * inv_alpha);
final_g =
static_cast<unsigned char>(cursor_g * alpha + img_g * inv_alpha);
final_b =
static_cast<unsigned char>(cursor_b * alpha + img_b * inv_alpha);
}
// set pixel
unsigned long new_pixel = (final_r << 16) | (final_g << 8) | final_b;
XPutPixel(image, img_x, img_y, new_pixel);
}
}
XFree(cursor_image);
}
bool ScreenCapturerX11::ProbeCapture() {
if (!display_ || display_info_list_.empty()) {
return false;
}
const auto& first_display = display_info_list_[0];
XImage* probe_image = nullptr;
int x11_error = 0;
{
ScopedX11ErrorTrap trap(display_);
probe_image = XGetImage(display_, root_, first_display.left,
first_display.top, 1, 1, AllPlanes, ZPixmap);
x11_error = trap.SyncAndGetError();
}
if (probe_image) {
XDestroyImage(probe_image);
}
if (x11_error != 0 || !probe_image) {
LOG_WARN("X11 probe XGetImage failed: x11_error={}, image={}", x11_error,
probe_image ? "valid" : "null");
return false;
}
return true;
}
} // namespace crossdesk

View File

@@ -7,11 +7,17 @@
#ifndef _SCREEN_CAPTURER_X11_H_
#define _SCREEN_CAPTURER_X11_H_
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xrandr.h>
// forward declarations for X11 types
struct _XDisplay;
typedef struct _XDisplay Display;
typedef unsigned long Window;
struct _XRRScreenResources;
typedef struct _XRRScreenResources XRRScreenResources;
struct _XImage;
typedef struct _XImage XImage;
#include <atomic>
#include <cctype>
#include <cstring>
#include <functional>
#include <iostream>
@@ -20,6 +26,8 @@
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerX11 : public ScreenCapturer {
public:
ScreenCapturerX11();
@@ -28,18 +36,23 @@ class ScreenCapturerX11 : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
void OnFrame();
private:
void DrawCursor(XImage* image, int x, int y);
bool ProbeCapture();
private:
Display* display_ = nullptr;
Window root_ = 0;
@@ -52,13 +65,15 @@ class ScreenCapturerX11 : public ScreenCapturer {
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_;
std::vector<DisplayInfo> display_info_list_;
int capture_error_count_ = 0;
// 缓冲区
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
#endif
} // namespace crossdesk
#endif

View File

@@ -2,6 +2,8 @@
#include "rd_log.h"
namespace crossdesk {
ScreenCapturerSck::ScreenCapturerSck() {}
ScreenCapturerSck::~ScreenCapturerSck() {}
@@ -26,8 +28,8 @@ int ScreenCapturerSck::Destroy() {
return 0;
}
int ScreenCapturerSck::Start() {
screen_capturer_sck_impl_->Start();
int ScreenCapturerSck::Start(bool show_cursor) {
screen_capturer_sck_impl_->Start(show_cursor);
return 0;
}
@@ -60,6 +62,13 @@ int ScreenCapturerSck::SwitchTo(int monitor_index) {
return -1;
}
int ScreenCapturerSck::ResetToInitialMonitor() {
if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->ResetToInitialMonitor();
}
return -1;
}
std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->GetDisplayInfoList();
@@ -70,4 +79,5 @@ std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
void ScreenCapturerSck::OnFrame() {}
void ScreenCapturerSck::CleanUp() {}
void ScreenCapturerSck::CleanUp() {}
} // namespace crossdesk

View File

@@ -16,6 +16,8 @@
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerSck : public ScreenCapturer {
public:
ScreenCapturerSck();
@@ -24,13 +26,14 @@ class ScreenCapturerSck : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
@@ -55,5 +58,5 @@ class ScreenCapturerSck : public ScreenCapturer {
private:
std::unique_ptr<ScreenCapturer> screen_capturer_sck_impl_;
};
} // namespace crossdesk
#endif

View File

@@ -22,9 +22,12 @@
#include "display_info.h"
#include "rd_log.h"
static const int kFullDesktopScreenId = -1;
using namespace crossdesk;
class ScreenCapturerSckImpl;
static const int kFullDesktopScreenId = -1;
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
@@ -54,7 +57,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Start() override;
int Start(bool show_cursor) override;
int SwitchTo(int monitor_index) override;
@@ -67,6 +70,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int Resume(int monitor_index) override { return 0; }
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
int ResetToInitialMonitor() override;
private:
std::vector<DisplayInfo> display_info_list_;
@@ -77,6 +81,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool show_cursor_ = false;
public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
@@ -109,6 +114,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
// support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0;
int initial_monitor_index_ = 0;
};
std::string GetDisplayName(CGDirectDisplayID display_id) {
@@ -192,14 +198,26 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
_on_data = cb;
fps_ = fps;
if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess();
if (!has_permission) {
LOG_ERROR("Screen recording permission not granted");
return -1;
}
}
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block SCShareableContent *content = nil;
__block NSError *capture_error = nil;
[SCShareableContent
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
if (error) {
NSLog(@"Failed to get shareable content: %@", error);
capture_error = error;
LOG_ERROR("Failed to get shareable content: {}",
std::string([error.localizedDescription UTF8String]));
} else {
content = result;
}
@@ -207,9 +225,10 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (!content || content.displays.count == 0) {
LOG_ERROR("Failed to get display info");
return 0;
if (capture_error || !content || content.displays.count == 0) {
LOG_ERROR("Failed to get display info, error: {}",
std::string([capture_error.localizedDescription UTF8String]));
return -1;
}
CGDirectDisplayID displays[10];
@@ -222,13 +241,17 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
CGRect bounds = CGDisplayBounds(display_id);
bool is_primary = CGDisplayIsMain(display_id);
std::string name;
name = GetDisplayName(display_id);
std::string name = GetDisplayName(display_id);
if (name.empty()) {
name = "Display " + std::to_string(unnamed_count++);
name = "Display" + std::to_string(unnamed_count++);
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(), [](unsigned char c) { return !std::isalnum(c); }),
name.end());
DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary,
static_cast<int>(bounds.origin.x), static_cast<int>(bounds.origin.y),
static_cast<int>(bounds.origin.x + bounds.size.width),
@@ -240,10 +263,22 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
display_id_name_map_[display_id] = name;
}
initial_monitor_index_ = 0;
return 0;
}
int ScreenCapturerSckImpl::Start() {
int ScreenCapturerSckImpl::Start(bool show_cursor) {
if (permanent_error_) {
LOG_ERROR("Cannot start capturer: permanent error occurred");
return -1;
}
if (display_info_list_.empty()) {
LOG_ERROR("Cannot start capturer: display info not initialized");
return -1;
}
show_cursor_ = show_cursor;
StartOrReconfigureCapturer();
return 0;
}
@@ -263,6 +298,25 @@ int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
return 0;
}
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1;
CGDirectDisplayID target_display = display_id_map_[target];
if (current_display_ == target_display) return 0;
if (stream_) {
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
std::lock_guard<std::mutex> lock(lock_);
stream_ = nil;
current_display_ = target_display;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = target_display;
StartOrReconfigureCapturer();
}
return 0;
}
int ScreenCapturerSckImpl::Destroy() {
std::lock_guard<std::mutex> lock(lock_);
if (stream_) {
@@ -298,17 +352,17 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
return;
}
if (!content.displays.count) {
if (!content.displays || content.displays.count == 0) {
LOG_ERROR("getShareableContent returned no displays");
permanent_error_ = true;
return;
}
SCDisplay *captured_display;
SCDisplay *captured_display = nil;
{
std::lock_guard<std::mutex> lock(lock_);
for (SCDisplay *display in content.displays) {
if (current_display_ == display.displayID) {
if (current_display_ != 0 && current_display_ == display.displayID) {
LOG_WARN("current display: {}, name: {}", current_display_,
display_id_name_map_[current_display_]);
captured_display = display;
@@ -317,15 +371,35 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
}
if (!captured_display) {
captured_display = content.displays.firstObject;
current_display_ = captured_display.displayID;
if (captured_display) {
current_display_ = captured_display.displayID;
}
}
}
if (!captured_display) {
LOG_ERROR("Failed to find valid display");
permanent_error_ = true;
return;
}
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:captured_display
excludingWindows:@[]];
if (!filter) {
LOG_ERROR("Failed to create SCContentFilter");
permanent_error_ = true;
return;
}
SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init];
if (!config) {
LOG_ERROR("Failed to create SCStreamConfiguration");
permanent_error_ = true;
return;
}
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
config.showsCursor = false;
config.showsCursor = show_cursor_;
config.width = filter.contentRect.size.width * filter.pointPixelScale;
config.height = filter.contentRect.size.height * filter.pointPixelScale;
config.captureResolution = SCCaptureResolutionAutomatic;
@@ -414,20 +488,33 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
}
void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
// The copy is needed to avoid capturing `this` in the Objective-C block. Accessing `helper_`
// inside the block is equivalent to `this->helper_` and would crash (UAF) if `this` is
// deleted before the block is executed.
if (permanent_error_) {
LOG_ERROR("Cannot reconfigure capturer: permanent error occurred");
return;
}
if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess();
if (!has_permission) {
LOG_ERROR("Screen recording permission not granted");
permanent_error_ = true;
return;
}
}
SckHelper *local_helper = helper_;
auto handler = ^(SCShareableContent *content, NSError *error) {
if (error) {
LOG_ERROR("getShareableContent failed: {}",
std::string([error.localizedDescription UTF8String]));
[local_helper onShareableContentCreated:nil];
return;
}
[local_helper onShareableContentCreated:content];
};
[SCShareableContent getShareableContentWithCompletionHandler:handler];
}
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
return std::make_unique<ScreenCapturerSckImpl>();
}
@implementation SckHelper {
// This lock is to prevent the capturer being destroyed while an instance
// method is still running on another thread.
@@ -485,4 +572,8 @@ std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
_capturer = nullptr;
}
@end
@end
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
return std::make_unique<ScreenCapturerSckImpl>();
}

View File

@@ -11,6 +11,8 @@
#include "display_info.h"
namespace crossdesk {
class ScreenCapturer {
public:
typedef std::function<void(unsigned char*, int, int, int, const char*)>
@@ -22,13 +24,14 @@ class ScreenCapturer {
public:
virtual int Init(const int fps, cb_desktop_data cb) = 0;
virtual int Destroy() = 0;
virtual int Start() = 0;
virtual int Start(bool show_cursor) = 0;
virtual int Stop() = 0;
virtual int Pause(int monitor_index) = 0;
virtual int Resume(int monitor_index) = 0;
virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0;
virtual int SwitchTo(int monitor_index) = 0;
virtual int ResetToInitialMonitor() = 0;
};
} // namespace crossdesk
#endif

View File

@@ -8,14 +8,16 @@
#define _SCREEN_CAPTURER_FACTORY_H_
#ifdef _WIN32
#include "screen_capturer_wgc.h"
#include "screen_capturer_win.h"
#elif __linux__
#include "screen_capturer_x11.h"
#include "screen_capturer_linux.h"
#elif __APPLE__
// #include "screen_capturer_avf.h"
#include "screen_capturer_sck.h"
#endif
namespace crossdesk {
class ScreenCapturerFactory {
public:
virtual ~ScreenCapturerFactory() {}
@@ -23,9 +25,9 @@ class ScreenCapturerFactory {
public:
ScreenCapturer* Create() {
#ifdef _WIN32
return new ScreenCapturerWgc();
return new ScreenCapturerWin();
#elif __linux__
return new ScreenCapturerX11();
return new ScreenCapturerLinux();
#elif __APPLE__
// return new ScreenCapturerAvf();
return new ScreenCapturerSck();
@@ -34,5 +36,5 @@ class ScreenCapturerFactory {
#endif
}
};
#endif
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,356 @@
#include "screen_capturer_dxgi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
return name;
}
} // namespace
ScreenCapturerDxgi::ScreenCapturerDxgi() {}
ScreenCapturerDxgi::~ScreenCapturerDxgi() {
Stop();
Destroy();
}
int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("DXGI: callback is null");
return -1;
}
if (!InitializeDxgi()) {
LOG_ERROR("DXGI: initialize DXGI failed");
return -2;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found");
return -3;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerDxgi::Destroy() {
Stop();
ReleaseDuplication();
outputs_.clear();
d3d_context_.Reset();
d3d_device_.Reset();
dxgi_factory_.Reset();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerDxgi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -1;
}
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDxgi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
ReleaseDuplication();
return 0;
}
int ScreenCapturerDxgi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDxgi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1;
}
paused_ = true;
monitor_index_ = monitor_index;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -2;
}
paused_ = false;
LOG_INFO("DXGI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerDxgi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
if (monitor_index_ == target) return 0;
if (running_) {
paused_ = true;
monitor_index_ = target;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
paused_ = false;
return -2;
}
paused_ = false;
LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
} else {
monitor_index_ = target;
}
return 0;
}
bool ScreenCapturerDxgi::InitializeDxgi() {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL feature_levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0};
D3D_FEATURE_LEVEL out_level{};
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels,
ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags,
feature_levels, ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr);
return false;
}
}
hr = CreateDXGIFactory1(
__uuidof(IDXGIFactory1),
reinterpret_cast<void**>(dxgi_factory_.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
return false;
}
return true;
}
void ScreenCapturerDxgi::EnumerateDisplays() {
display_info_list_.clear();
outputs_.clear();
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
for (UINT a = 0;
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++a) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++o) {
DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc))) {
continue;
}
std::string name = CleanDisplayName(desc.DeviceName);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) {
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)desc.Monitor, name, is_primary,
mi.rcMonitor.left, mi.rcMonitor.top,
mi.rcMonitor.right, mi.rcMonitor.bottom);
// primary first
if (is_primary)
display_info_list_.insert(display_info_list_.begin(), info);
else
display_info_list_.push_back(info);
outputs_.push_back(output);
}
}
}
}
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface(
IID_PPV_ARGS(output1.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
return false;
}
duplication_.Reset();
hr = output1->DuplicateOutput(d3d_device_.Get(), duplication_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: DuplicateOutput failed, hr={}", (int)hr);
return false;
}
staging_.Reset();
return true;
}
void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset();
if (duplication_) {
duplication_->ReleaseFrame();
}
duplication_.Reset();
}
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!duplication_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
DXGI_OUTDUPL_FRAME_INFO frame_info{};
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
HRESULT hr = duplication_->AcquireNextFrame(
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
continue;
}
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
// attempt to recreate duplication
ReleaseDuplication();
CreateDuplicationForMonitor(monitor_index_);
continue;
}
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
if (desktop_resource) {
hr = desktop_resource->QueryInterface(
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
} else {
duplication_->ReleaseFrame();
continue;
}
D3D11_TEXTURE2D_DESC src_desc{};
acquired_tex->GetDesc(&src_desc);
if (!staging_) {
D3D11_TEXTURE2D_DESC staging_desc = src_desc;
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.BindFlags = 0;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
staging_desc.MiscFlags = 0;
hr = d3d_device_->CreateTexture2D(&staging_desc, nullptr,
staging_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
duplication_->ReleaseFrame();
continue;
}
}
d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
int logical_width = static_cast<int>(src_desc.Width);
int even_width = logical_width & ~1;
int even_height = static_cast<int>(src_desc.Height) & ~1;
if (even_width <= 0 || even_height <= 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
continue;
}
int nv12_size = even_width * even_height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != even_width ||
nv12_height_ != even_height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = even_width;
nv12_height_ = even_height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
static_cast<int>(mapped.RowPitch), nv12_frame_,
even_width, nv12_frame_ + even_width * even_height,
even_width, even_width, even_height);
if (callback_) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[monitor_index_].name.c_str());
}
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
}
}
} // namespace crossdesk

View File

@@ -0,0 +1,81 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DXGI_H_
#define _SCREEN_CAPTURER_DXGI_H_
#include <Windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDxgi : public ScreenCapturer {
public:
ScreenCapturerDxgi();
~ScreenCapturerDxgi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
bool InitializeDxgi();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index);
void CaptureLoop();
void ReleaseDuplication();
private:
std::vector<DisplayInfo> display_info_list_;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif

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