Compare commits

...

14 Commits

Author SHA1 Message Date
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
28 changed files with 4009 additions and 3677 deletions

View File

@@ -58,12 +58,6 @@ jobs:
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 ./scripts/linux/pkg_amd64.sh
@@ -123,12 +117,6 @@ jobs:
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 }}
@@ -192,12 +180,6 @@ jobs:
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 }}
@@ -301,12 +283,6 @@ jobs:
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
run: |

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
# Xmake cache
.xmake/
build/
certs/
# MacOS Cache
.DS_Store

View File

@@ -214,7 +214,7 @@ sudo docker run -d \
**注意**
- **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
@@ -232,16 +232,30 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. 点击点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="140" 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>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**)。<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
4. 后续如果自托管服务器被重置或因其他原因导致证书更换,可以点击`重置证书指纹`按钮重置客户端保存的证书指纹。<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><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)。

View File

@@ -222,7 +222,7 @@ sudo docker run -d \
**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`.
- 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/`.
@@ -243,16 +243,31 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
### 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><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` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**).<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><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 `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
```
4. If the self-hosted server is later reset or the certificate is replaced for any reason, you can click the `Reset Certificate Fingerprint` button to clear the certificate fingerprint saved on the client.<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
### Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -15,21 +15,18 @@ 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" \
@@ -71,7 +68,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
@@ -85,32 +81,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"

View File

@@ -15,21 +15,18 @@ 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" \
@@ -70,7 +67,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
@@ -84,30 +80,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

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"
@@ -98,11 +95,6 @@ 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"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -140,17 +132,8 @@ EOF
chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
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}"

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"
@@ -98,11 +95,6 @@ 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"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -140,17 +132,8 @@ EOF
chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
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}"

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
@@ -99,11 +96,6 @@ 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"

View File

@@ -35,11 +35,8 @@ int main(int argc, char* argv[]) {
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cert_path =
(path_manager->GetCertPath() / "crossdesk.cn_root.crt").string();
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini",
cert_path);
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
}

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

@@ -5,11 +5,8 @@
namespace crossdesk {
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) {
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
Load();
}
@@ -70,71 +67,6 @@ int ConfigCenter::Load() {
} else {
coturn_server_port_ = 0;
}
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
} else {
cert_file_path_ = "";
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr && strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
} else {
cert_fingerprint_ = "";
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
} else {
cert_fingerprint_server_host_ = "";
}
const char* default_cert_fingerprint_value =
ini_.GetValue(section_, "default_cert_fingerprint", nullptr);
if (default_cert_fingerprint_value != nullptr &&
strlen(default_cert_fingerprint_value) > 0) {
default_cert_fingerprint_ = default_cert_fingerprint_value;
} else {
default_cert_fingerprint_ = "";
}
const char* default_cert_fingerprint_server_host_value =
ini_.GetValue(section_, "default_cert_fingerprint_server_host", nullptr);
if (default_cert_fingerprint_server_host_value != nullptr &&
strlen(default_cert_fingerprint_server_host_value) > 0) {
default_cert_fingerprint_server_host_ =
default_cert_fingerprint_server_host_value;
} else {
default_cert_fingerprint_server_host_ = "";
}
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
!cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
if (!enable_self_hosted_ && !default_cert_fingerprint_.empty() &&
!default_cert_fingerprint_server_host_.empty() &&
signal_server_host_default_ != default_cert_fingerprint_server_host_) {
LOG_INFO(
"Default server IP changed from {} to {}, clearing old fingerprint",
default_cert_fingerprint_server_host_, signal_server_host_default_);
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -165,19 +97,6 @@ int ConfigCenter::Save() {
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
}
if (!default_cert_fingerprint_.empty()) {
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -270,15 +189,6 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
}
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
signal_server_host != signal_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
signal_server_host_, signal_server_host);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
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());
@@ -310,67 +220,6 @@ int ConfigCenter::SetCoturnServerPort(int coturn_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());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetCertFingerprint(const std::string& fingerprint) {
cert_fingerprint_ = fingerprint;
cert_fingerprint_server_host_ = signal_server_host_;
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetDefaultCertFingerprint(const std::string& fingerprint) {
default_cert_fingerprint_ = fingerprint;
default_cert_fingerprint_server_host_ = signal_server_host_default_;
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearCertFingerprint() {
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearDefaultCertFingerprint() {
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -397,45 +246,12 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr &&
strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
}
if (!cert_fingerprint_.empty() && !cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
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_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -523,16 +339,6 @@ int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
std::string ConfigCenter::GetCertFingerprint() const {
return cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultCertFingerprint() const {
return default_cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
@@ -545,10 +351,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
std::string ConfigCenter::GetDefaultCertFilePath() const {
return cert_file_path_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }

View File

@@ -21,9 +21,7 @@ class ConfigCenter {
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
@@ -37,11 +35,6 @@ class ConfigCenter {
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetCertFilePath(const std::string& cert_file_path);
int SetCertFingerprint(const std::string& fingerprint);
int SetDefaultCertFingerprint(const std::string& fingerprint);
int ClearCertFingerprint();
int ClearDefaultCertFingerprint();
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
@@ -59,13 +52,9 @@ class ConfigCenter {
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetCertFilePath() const;
std::string GetCertFingerprint() const;
std::string GetDefaultCertFingerprint() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
@@ -92,12 +81,6 @@ class ConfigCenter {
int server_port_default_ = 9099;
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
std::string cert_file_path_ = "";
std::string cert_file_path_default_ = "";
std::string cert_fingerprint_ = "";
std::string cert_fingerprint_server_host_ = "";
std::string default_cert_fingerprint_ = "";
std::string default_cert_fingerprint_server_host_ = "";
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;

File diff suppressed because it is too large Load Diff

View File

@@ -112,13 +112,8 @@ static std::vector<std::string> self_hosted_server_port = {
reinterpret_cast<const char*>(u8"信令服务端口:"), "Signal Service Port:"};
static std::vector<std::string> self_hosted_server_coturn_server_port = {
reinterpret_cast<const char*>(u8"中继服务端口:"), "Relay Service 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> reset_cert_fingerprint = {
reinterpret_cast<const char*>(u8"重置证书指纹"),
"Reset Certificate Fingerprint"};
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
"OK"};
static std::vector<std::string> cancel = {
@@ -197,6 +192,12 @@ static std::vector<std::string> completed = {
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
static std::vector<std::string> failed = {
reinterpret_cast<const char*>(u8"失败"), "Failed"};
static std::vector<std::string> controller = {
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
static std::vector<std::string> file_transfer = {
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
static std::vector<std::string> connection_status = {
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {

View File

@@ -2,6 +2,12 @@
#include <libyuv.h>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
@@ -21,10 +27,123 @@
#include "screen_capturer_factory.h"
#include "version_checker.h"
#if defined(__APPLE__)
#include "window_util_mac.h"
#endif
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
namespace crossdesk {
namespace {
#if defined(__linux__) && !defined(__APPLE__)
inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out,
::Window* x11_window_out) {
if (!window || !display_out || !x11_window_out) {
return false;
}
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
// SDL build does not expose X11 window properties.
return false;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
Display* display = (Display*)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
const Sint64 x11_window_num =
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
const ::Window x11_window = (::Window)x11_window_num;
if (!display || !x11_window) {
return false;
}
*display_out = display;
*x11_window_out = x11_window;
return true;
#endif
}
inline void X11SendNetWmState(Display* display, ::Window x11_window,
long action, Atom state1, Atom state2 = 0) {
if (!display || !x11_window) {
return;
}
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
XEvent event;
memset(&event, 0, sizeof(event));
event.xclient.type = ClientMessage;
event.xclient.serial = 0;
event.xclient.send_event = True;
event.xclient.message_type = wm_state;
event.xclient.window = x11_window;
event.xclient.format = 32;
event.xclient.data.l[0] = action;
event.xclient.data.l[1] = (long)state1;
event.xclient.data.l[2] = (long)state2;
event.xclient.data.l[3] = 1; // normal source indication
event.xclient.data.l[4] = 0;
XSendEvent(display, DefaultRootWindow(display), False,
SubstructureRedirectMask | SubstructureNotifyMask, &event);
}
inline void X11SetWindowTypeUtility(Display* display, ::Window x11_window) {
if (!display || !x11_window) {
return;
}
const Atom wm_window_type =
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
const Atom wm_window_type_utility =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
PropModeReplace, (unsigned char*)&wm_window_type_utility, 1);
}
inline void X11SetWindowAlwaysOnTop(SDL_Window* window) {
Display* display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
const Atom state_stays_on_top =
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
XFlush(display);
}
inline void X11SetWindowSkipTaskbar(SDL_Window* window) {
Display* display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom skip_taskbar =
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
const Atom skip_pager =
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
// Hint the WM that this is an auxiliary/utility window.
X11SetWindowTypeUtility(display, x11_window);
XFlush(display);
}
#endif
} // namespace
std::vector<char> Render::SerializeRemoteAction(const RemoteAction& action) {
std::vector<char> buffer;
buffer.push_back(static_cast<char>(action.type));
@@ -131,6 +250,20 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window,
return SDL_HITTEST_NORMAL;
}
// Server window: OS-level dragging for the title bar, but keep the left-side
// collapse/expand button clickable.
if (render->server_window_ && window == render->server_window_) {
const float title_h = render->server_window_title_bar_height_;
const float button_w = title_h;
if (area->y >= 0 && area->y < title_h) {
if (area->x >= 0 && area->x < button_w) {
return SDL_HITTEST_NORMAL;
}
return SDL_HITTEST_DRAGGABLE;
}
return SDL_HITTEST_NORMAL;
}
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
@@ -539,13 +672,11 @@ int Render::CreateConnectionPeer() {
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
std::string tls_cert_fingerprint;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
tls_cert_fingerprint = config_center_->GetCertFingerprint();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
bool use_cached_id = false;
@@ -606,7 +737,6 @@ int Render::CreateConnectionPeer() {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
tls_cert_fingerprint = config_center_->GetDefaultCertFingerprint();
params_.user_id = client_id_with_password_;
}
@@ -630,7 +760,6 @@ int Render::CreateConnectionPeer() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
// peer config
strncpy((char*)params_.signal_server_ip, signal_server_ip.c_str(),
@@ -651,30 +780,6 @@ int Render::CreateConnectionPeer() {
strncpy((char*)params_.turn_server_password, "crossdeskpw",
sizeof(params_.turn_server_password) - 1);
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
strncpy(params_.tls_cert_fingerprint, tls_cert_fingerprint.c_str(),
sizeof(params_.tls_cert_fingerprint) - 1);
params_.tls_cert_fingerprint[sizeof(params_.tls_cert_fingerprint) - 1] = '\0';
if (config_center_->IsSelfHosted()) {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetCertFingerprint(fingerprint);
LOG_INFO("Saved self-hosted certificate fingerprint: {}", fingerprint);
}
};
params_.fingerprint_user_data = this;
} else {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetDefaultCertFingerprint(fingerprint);
LOG_INFO("Saved default server certificate fingerprint: {}",
fingerprint);
}
};
params_.fingerprint_user_data = this;
}
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
@@ -696,7 +801,7 @@ int Render::CreateConnectionPeer() {
params_.on_signal_status = OnSignalStatusCb;
params_.on_connection_status = OnConnectionStatusCb;
params_.net_status_report = NetStatusReport;
params_.on_net_status_report = OnNetStatusReport;
params_.user_data = this;
@@ -815,6 +920,12 @@ int Render::CreateMainWindow() {
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
server_window_normal_width_ =
(int)(server_window_width_default_ * dpi_scale_);
server_window_normal_height_ =
(int)(server_window_height_default_ * dpi_scale_);
SDL_SetWindowSize(main_window_, (int)main_window_width_,
(int)main_window_height_);
@@ -963,6 +1074,40 @@ int Render::CreateServerWindow() {
return -1;
}
#if _WIN32
// Hide server window from the taskbar by making it a tool window.
{
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
if (server_hwnd) {
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
ex_style |= WS_EX_TOOLWINDOW;
ex_style &= ~WS_EX_APPWINDOW;
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
// Keep the server window above normal windows.
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
// Best-effort keep above other windows on X11.
X11SetWindowAlwaysOnTop(server_window_);
// Best-effort hide from taskbar on X11.
X11SetWindowSkipTaskbar(server_window_);
#endif
#if defined(__APPLE__)
// Best-effort keep above other windows on macOS.
MacSetWindowAlwaysOnTop(server_window_, true);
// Best-effort exclude from Window menu / window cycling.
MacSetWindowExcludedFromWindowMenu(server_window_, true);
#endif
// Set window position to bottom-right corner
SDL_Rect display_bounds;
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
@@ -1285,13 +1430,11 @@ int Render::Run() {
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
cert_path_ =
(path_manager_->GetCertPath() / "crossdesk.cn_root.crt").string();
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = path_manager_->GetLogPath().string();
cache_path_ = path_manager_->GetCachePath().string();
config_center_ =
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini", cert_path_);
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
@@ -1305,8 +1448,6 @@ int Render::Run() {
} else {
signal_server_port_self_[0] = '\0';
}
strncpy(cert_file_path_, cert_path_.c_str(), sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
} else {
std::cerr << "Failed to create PathManager" << std::endl;
return -1;
@@ -1448,49 +1589,6 @@ void Render::MainLoop() {
}
UpdateInteractions();
if (need_to_send_host_info_) {
RemoteAction remote_action;
remote_action.i.display_num = display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1, display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
display_info_list_[i].name.c_str(),
display_info_list_[i].name.length());
remote_action.i.display_list[i][display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = display_info_list_[i].left;
remote_action.i.top[i] = display_info_list_[i].top;
remote_action.i.right[i] = display_info_list_[i].right;
remote_action.i.bottom[i] = display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
control_data_label_.c_str());
FreeRemoteAction(remote_action);
if (0 == ret) {
need_to_send_host_info_ = false;
}
}
}
}
@@ -1607,13 +1705,19 @@ void Render::CleanupFactories() {
void Render::CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props) {
SDL_FlushEvent(STREAM_REFRESH_EVENT);
if (props->dst_buffer_) {
size_t buffer_size = props->dst_buffer_capacity_;
std::vector<unsigned char> buffer_copy(buffer_size);
memcpy(buffer_copy.data(), props->dst_buffer_, buffer_size);
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
int video_width = props->video_width_;
int video_height = props->video_height_;
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
std::vector<unsigned char> buffer_copy(*frame_snapshot);
std::string remote_id = props->remote_id_;
std::string remote_host_name = props->remote_host_name_;
std::string password =
@@ -1694,22 +1798,47 @@ void Render::CleanSubStreamWindowProperties(
props->stream_texture_ = nullptr;
}
if (props->dst_buffer_) {
delete[] props->dst_buffer_;
props->dst_buffer_ = nullptr;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
}
std::shared_ptr<Render::SubStreamWindowProperties>
Render::GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id) {
if (remote_id.empty()) {
return nullptr;
}
std::shared_lock lock(client_properties_mutex_);
auto it = client_properties_.find(remote_id);
if (it == client_properties_.end()) {
return nullptr;
}
return it->second;
}
void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
const std::filesystem::path& file_path,
const std::string& file_label) {
if (!props || !props->peer_) {
LOG_ERROR("StartFileTransfer: invalid props or peer");
const std::string& file_label,
const std::string& remote_id) {
const bool is_global = (props == nullptr);
PeerPtr* peer = is_global ? peer_ : props->peer_;
if (!peer) {
LOG_ERROR("StartFileTransfer: invalid peer");
return;
}
bool expected = false;
if (!props->file_sending_.compare_exchange_strong(expected, true)) {
if (!(is_global ? file_transfer_.file_sending_
: props->file_transfer_.file_sending_)
.compare_exchange_strong(expected, true)) {
// Already sending, this should not happen if called correctly
LOG_WARN(
"StartFileTransfer called but file_sending_ is already true, "
@@ -1718,13 +1847,19 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
return;
}
auto peer = props->peer_;
auto props_weak = std::weak_ptr<SubStreamWindowProperties>(props);
Render* render_ptr = this;
std::thread([peer, file_path, file_label, props_weak, render_ptr]() {
std::thread([peer, file_path, file_label, props_weak, render_ptr, remote_id,
is_global]() {
auto props_locked = props_weak.lock();
if (!props_locked) {
FileTransferState* state = nullptr;
if (props_locked) {
state = &props_locked->file_transfer_;
} else if (is_global) {
state = &render_ptr->file_transfer_;
} else {
return;
}
@@ -1732,49 +1867,50 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
uint64_t total_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
props_locked->file_sending_ = false;
state->file_sending_ = false;
return;
}
props_locked->file_sent_bytes_ = 0;
props_locked->file_total_bytes_ = total_size;
props_locked->file_send_rate_bps_ = 0;
props_locked->file_transfer_window_visible_ = true;
state->file_sent_bytes_ = 0;
state->file_total_bytes_ = total_size;
state->file_send_rate_bps_ = 0;
state->file_transfer_window_visible_ = true;
{
std::lock_guard<std::mutex> lock(props_locked->file_transfer_mutex_);
props_locked->file_send_start_time_ = std::chrono::steady_clock::now();
props_locked->file_send_last_update_time_ =
props_locked->file_send_start_time_;
props_locked->file_send_last_bytes_ = 0;
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
state->file_send_start_time_ = std::chrono::steady_clock::now();
state->file_send_last_update_time_ = state->file_send_start_time_;
state->file_send_last_bytes_ = 0;
}
LOG_INFO(
"File transfer started: {} ({} bytes), file_sending_={}, "
"total_bytes_={}",
file_path.filename().string(), total_size,
props_locked->file_sending_.load(),
props_locked->file_total_bytes_.load());
file_path.filename().string(), total_size, state->file_sending_.load(),
state->file_total_bytes_.load());
FileSender sender;
uint32_t file_id = FileSender::NextFileId();
{
if (props_locked) {
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_[file_id] = props_weak;
} else {
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_transfer_state_mutex_);
render_ptr->file_id_to_transfer_state_[file_id] = state;
}
props_locked->current_file_id_ = file_id;
state->current_file_id_ = file_id;
// Update file transfer list: mark as sending
// Find the queued file that matches the exact file path
{
std::lock_guard<std::mutex> lock(props_locked->file_transfer_list_mutex_);
for (auto& info : props_locked->file_transfer_list_) {
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
if (info.file_path == file_path &&
info.status ==
SubStreamWindowProperties::FileTransferStatus::Queued) {
info.status = SubStreamWindowProperties::FileTransferStatus::Sending;
info.status == FileTransferState::FileTransferStatus::Queued) {
info.status = FileTransferState::FileTransferStatus::Sending;
info.file_id = file_id;
info.file_size = total_size;
info.sent_bytes = 0;
@@ -1783,81 +1919,88 @@ void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
}
}
props_locked->file_transfer_window_visible_ = true;
state->file_transfer_window_visible_ = true;
// Progress will be updated via ACK from receiver
int ret = sender.SendFile(
file_path, file_path.filename().string(),
[peer, file_label](const char* buf, size_t sz) -> int {
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
[peer, file_label, remote_id](const char* buf, size_t sz) -> int {
if (remote_id.empty()) {
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
} else {
return SendReliableDataFrameToPeer(
peer, buf, sz, file_label.c_str(), remote_id.c_str(),
remote_id.size());
}
},
64 * 1024, file_id);
// file_sending_ should remain true until we receive the final ACK from
// receiver
auto props_locked_final = props_weak.lock();
if (props_locked_final) {
// On error, set file_sending_ to false immediately to allow next file
if (ret != 0) {
props_locked_final->file_sending_ = false;
props_locked_final->file_transfer_window_visible_ = false;
props_locked_final->file_sent_bytes_ = 0;
props_locked_final->file_total_bytes_ = 0;
props_locked_final->file_send_rate_bps_ = 0;
props_locked_final->current_file_id_ = 0;
// On error, set file_sending_ to false immediately to allow next file
if (ret != 0) {
state->file_sending_ = false;
state->file_transfer_window_visible_ = false;
state->file_sent_bytes_ = 0;
state->file_total_bytes_ = 0;
state->file_send_rate_bps_ = 0;
state->current_file_id_ = 0;
// Unregister file_id mapping on error
{
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_.erase(file_id);
}
// Unregister file_id mapping on error
if (props_locked) {
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_.erase(file_id);
} else {
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_transfer_state_mutex_);
render_ptr->file_id_to_transfer_state_.erase(file_id);
}
// Update file transfer list: mark as failed
{
std::lock_guard<std::mutex> lock(
props_locked_final->file_transfer_list_mutex_);
for (auto& info : props_locked_final->file_transfer_list_) {
if (info.file_id == file_id) {
info.status =
SubStreamWindowProperties::FileTransferStatus::Failed;
break;
}
// Update file transfer list: mark as failed
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
if (info.file_id == file_id) {
info.status = FileTransferState::FileTransferStatus::Failed;
break;
}
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
render_ptr->ProcessFileQueue(props_locked_final);
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
render_ptr->ProcessFileQueue(props_locked);
}
}).detach();
}
void Render::ProcessFileQueue(
std::shared_ptr<SubStreamWindowProperties> props) {
if (!props) {
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
if (!state) {
return;
}
if (props->file_sending_.load()) {
if (state->file_sending_.load()) {
return;
}
SubStreamWindowProperties::QueuedFile queued_file;
FileTransferState::QueuedFile queued_file;
{
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
if (props->file_send_queue_.empty()) {
std::lock_guard<std::mutex> lock(state->file_queue_mutex_);
if (state->file_send_queue_.empty()) {
return;
}
queued_file = props->file_send_queue_.front();
props->file_send_queue_.pop();
queued_file = state->file_send_queue_.front();
state->file_send_queue_.pop();
}
LOG_INFO("Processing next file in queue: {}",
queued_file.file_path.string().c_str());
StartFileTransfer(props, queued_file.file_path, queued_file.file_label);
StartFileTransfer(props, queued_file.file_path, queued_file.file_label,
queued_file.remote_id);
}
void Render::UpdateRenderRect() {
@@ -1949,10 +2092,22 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
{
// std::shared_lock lock(client_properties_mutex_);
for (auto& [host_name, props] : client_properties_) {
thumbnail_->SaveToThumbnail(
(char*)props->dst_buffer_, props->video_width_,
props->video_height_, host_name, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
thumbnail_->SaveToThumbnail(
(char*)frame_snapshot->data(), video_width, video_height,
host_name, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
}
if (props->peer_) {
std::string client_id = (host_name == client_id_)
@@ -2028,10 +2183,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
}
break;
case SDL_EVENT_DROP_FILE:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
ProcessFileDropEvent(event);
}
ProcessFileDropEvent(event);
break;
case SDL_EVENT_MOUSE_MOTION:
@@ -2049,18 +2201,52 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
if (!props) {
break;
}
if (props->video_width_ <= 0 || props->video_height_ <= 0) {
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
bool render_rect_dirty = false;
bool cleanup_pending = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
cleanup_pending = props->stream_cleanup_pending_;
if (!cleanup_pending) {
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
render_rect_dirty = props->render_rect_dirty_;
}
if (cleanup_pending) {
if (props->stream_texture_) {
SDL_DestroyTexture(props->stream_texture_);
props->stream_texture_ = nullptr;
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->stream_cleanup_pending_ = false;
}
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
break;
}
if (!props->dst_buffer_) {
if (video_width <= 0 || video_height <= 0) {
break;
}
if (!frame_snapshot || frame_snapshot->empty()) {
break;
}
if (props->stream_texture_) {
if (props->video_width_ != props->texture_width_ ||
props->video_height_ != props->texture_height_) {
props->texture_width_ = props->video_width_;
props->texture_height_ = props->video_height_;
if (video_width != props->texture_width_ ||
video_height != props->texture_height_) {
props->texture_width_ = video_width;
props->texture_height_ = video_height;
SDL_DestroyTexture(props->stream_texture_);
// props->stream_texture_ = SDL_CreateTexture(
@@ -2085,8 +2271,8 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
SDL_DestroyProperties(nvProps);
}
} else {
props->texture_width_ = props->video_width_;
props->texture_height_ = props->video_height_;
props->texture_width_ = video_width;
props->texture_height_ = video_height;
// props->stream_texture_ = SDL_CreateTexture(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
@@ -2107,69 +2293,124 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
SDL_DestroyProperties(nvProps);
}
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
props->texture_width_);
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
}
break;
}
}
void Render::ProcessFileDropEvent(const SDL_Event& event) {
if (!((stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) ||
(server_window_ &&
SDL_GetWindowID(server_window_) == event.window.windowID))) {
return;
}
if (event.type != SDL_EVENT_DROP_FILE) {
return;
}
if (!stream_window_inited_) {
return;
}
std::shared_lock lock(client_properties_mutex_);
for (auto& [_, props] : client_properties_) {
if (props->tab_selected_) {
if (event.drop.data == nullptr) {
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
break;
}
if (!props || !props->peer_) {
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
break;
}
std::string utf8_path = static_cast<const char*>(event.drop.data);
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
// Check if file exists
std::error_code ec;
if (!std::filesystem::exists(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
file_path.string().c_str());
break;
}
// Check if it's a regular file
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
file_path.string().c_str());
break;
}
// Get file size
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
ec.message().c_str());
break;
}
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
file_size);
// Use ProcessSelectedFile to handle the file processing
ProcessSelectedFile(utf8_path, props, props->file_label_);
break;
if (SDL_GetWindowID(stream_window_) == event.window.windowID) {
if (!stream_window_inited_) {
return;
}
std::shared_lock lock(client_properties_mutex_);
for (auto& [_, props] : client_properties_) {
if (props->tab_selected_) {
if (event.drop.data == nullptr) {
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
break;
}
if (!props || !props->peer_) {
LOG_ERROR("ProcessFileDropEvent: invalid props or peer");
break;
}
std::string utf8_path = static_cast<const char*>(event.drop.data);
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
// Check if file exists
std::error_code ec;
if (!std::filesystem::exists(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
file_path.string().c_str());
break;
}
// Check if it's a regular file
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
file_path.string().c_str());
break;
}
// Get file size
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
ec.message().c_str());
break;
}
LOG_INFO("Drop file [{}] to send (size: {} bytes)", event.drop.data,
file_size);
// Use ProcessSelectedFile to handle the file processing
ProcessSelectedFile(utf8_path, props, props->file_label_);
break;
}
}
} else if (SDL_GetWindowID(server_window_) == event.window.windowID) {
if (!server_window_inited_) {
return;
}
if (event.drop.data == nullptr) {
LOG_ERROR("ProcessFileDropEvent: drop event data is null");
return;
}
std::string utf8_path = static_cast<const char*>(event.drop.data);
std::filesystem::path file_path = std::filesystem::u8path(utf8_path);
// Check if file exists
std::error_code ec;
if (!std::filesystem::exists(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: file does not exist: {}",
file_path.string().c_str());
return;
}
// Check if it's a regular file
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("ProcessFileDropEvent: path is not a regular file: {}",
file_path.string().c_str());
return;
}
// Get file size
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("ProcessFileDropEvent: failed to get file size: {}",
ec.message().c_str());
return;
}
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
event.drop.data, file_size);
// Handle the dropped file on server window as needed
}
}
} // namespace crossdesk

View File

@@ -42,6 +42,41 @@
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;
@@ -88,8 +123,13 @@ class Render {
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;
@@ -129,38 +169,10 @@ class Render {
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_;
// File transfer progress
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::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_;
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
};
public:
@@ -193,9 +205,13 @@ class Render {
void ProcessFileDropEvent(const SDL_Event& event);
void ProcessSelectedFile(const std::string& path,
std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label);
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();
@@ -204,12 +220,12 @@ class Render {
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();
@@ -220,6 +236,7 @@ class Render {
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,
@@ -272,11 +289,11 @@ class Render {
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);
@@ -317,7 +334,8 @@ class Render {
// 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& file_label,
const std::string& remote_id = "");
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
int AudioDeviceInit();
@@ -362,7 +380,6 @@ class Render {
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_;
@@ -418,6 +435,9 @@ class Render {
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool foucs_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;
@@ -467,7 +487,7 @@ class Render {
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_ = "";
SDL_Event last_mouse_event;
SDL_AudioStream* output_stream_;
uint32_t STREAM_REFRESH_EVENT = 0;
@@ -504,14 +524,14 @@ class Render {
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 300;
int server_window_height_default_ = 450;
float server_window_width_ = 300;
float server_window_height_ = 450;
float server_window_title_bar_height_ = 50.0f;
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_ = 300;
int server_window_normal_height_ = 450;
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;
@@ -573,6 +593,10 @@ class Render {
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;
@@ -586,8 +610,8 @@ class Render {
uint64_t last_frame_time_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
uint64_t new_version_icon_last_trigger_time_ = 0;
uint64_t new_version_icon_render_start_time_ = 0;
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
@@ -607,7 +631,6 @@ class Render {
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
char cert_file_path_[256] = "";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
@@ -626,7 +649,6 @@ class Render {
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
std::string tls_cert_path_self_ = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = "";
@@ -647,6 +669,10 @@ class Render {
/* ------ 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_ = "";
FileTransferState file_transfer_;
};
} // namespace crossdesk
#endif

View File

@@ -4,6 +4,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <unordered_map>
#include "clipboard.h"
@@ -237,31 +238,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
render->client_properties_.find(remote_id)->second.get();
if (props->connection_established_) {
if (!props->dst_buffer_) {
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
if (props->dst_buffer_capacity_ < video_frame->size) {
delete props->dst_buffer_;
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
if (!props->back_frame_) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(video_frame->size);
}
if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
}
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
bool need_to_update_render_rect = false;
if (props->video_width_ != props->video_width_last_ ||
props->video_height_ != props->video_height_last_) {
need_to_update_render_rect = true;
props->video_width_last_ = props->video_width_;
props->video_height_last_ = props->video_height_;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
if (need_to_update_render_rect) {
render->UpdateRenderRect();
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->front_frame_.swap(props->back_frame_);
}
SDL_Event event;
@@ -320,7 +321,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
receiver.SetOnSendAck([render](const FileTransferAck& ack) -> int {
receiver.SetOnSendAck([render,
remote_user_id](const FileTransferAck& ack) -> int {
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
if (props) {
PeerPtr* peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
render->peer_, reinterpret_cast<const char*>(&ack),
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
@@ -361,42 +375,100 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
}
}
Render::FileTransferState* state = nullptr;
if (!props) {
LOG_WARN("FileTransferAck: no props found for file_id={}", ack.file_id);
return;
{
std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
if (it != render->file_id_to_transfer_state_.end()) {
state = it->second;
}
}
if (!state) {
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
ack.file_id);
return;
}
} else {
state = &props->file_transfer_;
}
// Update progress based on ACK
props->file_sent_bytes_ = ack.acked_offset;
props->file_total_bytes_ = ack.total_size;
state->file_sent_bytes_ = ack.acked_offset;
state->file_total_bytes_ = ack.total_size;
uint32_t rate_bps = 0;
{
uint32_t data_channel_bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (props) {
uint32_t data_channel_bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (data_channel_bitrate > 0 && props->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
if (data_channel_bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
uint32_t current_rate = props->file_send_rate_bps_.load();
if (current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
uint32_t current_rate = state->file_send_rate_bps_.load();
if (current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps =
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
rate_bps = state->file_send_rate_bps_.load();
}
} else {
rate_bps = props->file_send_rate_bps_.load();
// Global transfer: no per-connection bitrate available.
// Estimate send rate from ACKed bytes delta over time.
const uint32_t current_rate = state->file_send_rate_bps_.load();
uint32_t estimated_rate_bps = 0;
const auto now = std::chrono::steady_clock::now();
uint64_t last_bytes = 0;
std::chrono::steady_clock::time_point last_time;
{
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
last_bytes = state->file_send_last_bytes_;
last_time = state->file_send_last_update_time_;
}
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
const double delta_seconds =
std::chrono::duration<double>(now - last_time).count();
if (delta_seconds > 0.0 && delta_bytes > 0) {
const double bps =
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
if (bps > 0.0) {
const double capped = (std::min)(
bps,
static_cast<double>((std::numeric_limits<uint32_t>::max)()));
estimated_rate_bps = static_cast<uint32_t>(capped);
}
}
}
if (estimated_rate_bps > 0 && current_rate > 0) {
// 70% old + 30% new for smoother display
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
estimated_rate_bps * 0.3);
} else if (estimated_rate_bps > 0) {
rate_bps = estimated_rate_bps;
} else {
rate_bps = current_rate;
}
}
props->file_send_rate_bps_ = rate_bps;
props->file_send_last_bytes_ = ack.acked_offset;
state->file_send_rate_bps_ = rate_bps;
state->file_send_last_bytes_ = ack.acked_offset;
auto now = std::chrono::steady_clock::now();
props->file_send_last_update_time_ = now;
state->file_send_last_update_time_ = now;
}
// Update file transfer list: update progress and rate
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) {
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.sent_bytes = ack.acked_offset;
info.file_size = ack.total_size;
@@ -410,8 +482,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
if ((ack.flags & 0x01) != 0) {
// Transfer completed - receiver has finished receiving the file
// Reopen window if it was closed by user
props->file_transfer_window_visible_ = true;
props->file_sending_ = false; // Mark sending as finished
state->file_transfer_window_visible_ = true;
state->file_sending_ = false; // Mark sending as finished
LOG_INFO(
"File transfer completed via ACK, file_id={}, total_size={}, "
"acked_offset={}",
@@ -419,11 +491,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Update file transfer list: mark as completed
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) {
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto& info : state->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.status =
SubStreamWindowProperties::FileTransferStatus::Completed;
Render::FileTransferState::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size;
break;
}
@@ -432,9 +504,15 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
// Unregister file_id mapping after completion
{
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
if (props) {
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
} else {
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_transfer_state_mutex_);
render->file_id_to_transfer_state_.erase(ack.file_id);
}
}
// Process next file in queue
@@ -456,24 +534,39 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(render->client_properties_mutex_);
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// local
auto props = render->client_properties_.find(remote_id)->second;
if (remote_action.type == ControlType::host_infomation &&
props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
if (remote_action.type == ControlType::host_infomation) {
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// client mode
auto props = render->client_properties_.find(remote_id)->second;
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
FreeRemoteAction(remote_action);
} else {
// server mode
render->connection_host_names_[remote_id] = std::string(
remote_action.i.host_name, remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]",
render->connection_host_names_[remote_id]);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
render->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
FreeRemoteAction(remote_action);
}
FreeRemoteAction(remote_action);
} else {
// remote
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
@@ -568,7 +661,49 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) {
case ConnectionStatus::Connected: {
render->need_to_send_host_info_ = true;
{
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
if (!render->need_to_create_stream_window_ &&
!render->client_properties_.empty()) {
render->need_to_create_stream_window_ = true;
@@ -585,12 +720,22 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Closed: {
props->connection_established_ = false;
props->mouse_control_button_pressed_ = false;
if (props->dst_buffer_ && props->stream_texture_) {
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
props->texture_width_);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
render->CleanSubStreamWindowProperties(props);
SDL_Event event;
event.type = render->STREAM_REFRESH_EVENT;
event.user.data1 = props.get();
SDL_PushEvent(&event);
break;
}
@@ -623,11 +768,54 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
switch (status) {
case ConnectionStatus::Connected: {
{
RemoteAction remote_action;
remote_action.i.display_num = render->display_info_list_.size();
remote_action.i.display_list =
(char**)malloc(remote_action.i.display_num * sizeof(char*));
remote_action.i.left =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int*)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int*)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
render->display_info_list_[i].name);
remote_action.i.display_list[i] =
(char*)malloc(render->display_info_list_[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
render->display_info_list_[i].name.c_str(),
render->display_info_list_[i].name.length());
remote_action.i
.display_list[i][render->display_info_list_[i].name.length()] =
'\0';
remote_action.i.left[i] = render->display_info_list_[i].left;
remote_action.i.top[i] = render->display_info_list_[i].top;
remote_action.i.right[i] = render->display_info_list_[i].right;
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
render->control_data_label_.c_str());
FreeRemoteAction(remote_action);
}
render->need_to_create_server_window_ = true;
render->need_to_send_host_info_ = true;
render->is_server_mode_ = true;
render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true;
render->remote_client_id_ = remote_id;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
@@ -656,7 +844,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->need_to_send_host_info_ = false;
render->remote_client_id_ = "";
if (props) props->connection_established_ = false;
if (render->audio_capture_) {
render->StopSpeakerCapturer();
@@ -681,11 +869,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
}
}
void Render::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) {
void Render::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) {
Render* render = (Render*)user_data;
if (!render) {
return;

View File

@@ -15,18 +15,6 @@
namespace crossdesk {
std::string 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 : "";
}
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
@@ -53,13 +41,29 @@ int LossRateDisplay(float loss_rate) {
return 0;
}
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, std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label) {
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);
@@ -74,47 +78,51 @@ void Render::ProcessSelectedFile(
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
SubStreamWindowProperties::FileTransferInfo info;
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 = SubStreamWindowProperties::FileTransferStatus::Queued;
info.status = FileTransferState::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
props->file_transfer_list_.push_back(info);
file_transfer_state->file_transfer_list_.push_back(info);
}
props->file_transfer_window_visible_ = true;
file_transfer_state->file_transfer_window_visible_ = true;
if (props->file_sending_.load()) {
if (file_transfer_state->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
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;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
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);
StartFileTransfer(props, file_path, file_label, remote_id);
if (props->file_sending_.load()) {
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(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
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;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
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 "
@@ -275,7 +283,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string title =
localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title);
this->ProcessSelectedFile(path, props, file_label_);
ProcessSelectedFile(path, props, file_label_);
}
ImGui::SameLine();

View File

@@ -3,7 +3,8 @@
#include "rd_log.h"
#include "render.h"
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
namespace crossdesk {
@@ -15,14 +16,28 @@ int Render::TitleBar(bool main_window) {
float title_bar_button_width = title_bar_button_width_;
float title_bar_button_height = title_bar_button_height_;
if (main_window) {
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 + 0.01f);
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;
// 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 + 0.01f);
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_;
@@ -92,13 +107,11 @@ int Render::TitleBar(bool main_window) {
std::string about_str = localization::about[localization_language_index_];
if (update_available_) {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
@@ -106,9 +119,9 @@ int Render::TitleBar(bool main_window) {
// render for 1 second
if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
if (now_time - new_version_icon_render_start_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
} else {
@@ -137,13 +150,11 @@ int Render::TitleBar(bool main_window) {
}
if (update_available_ && show_new_version_icon_in_menu_) {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
const double now_time = ImGui::GetTime();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
kNewVersionIconBlinkIntervalSec) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
@@ -152,14 +163,13 @@ int Render::TitleBar(bool main_window) {
// render for 1 second
if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos(
ImVec2(bar_pos_x + title_bar_button_width * 0.15f,
bar_pos_y - title_bar_button_width * 0.325f));
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
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_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
kNewVersionIconBlinkOnTimeSec) {
show_new_version_icon_ = false;
}
}
@@ -187,6 +197,11 @@ int Render::TitleBar(bool main_window) {
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(

View File

@@ -30,12 +30,14 @@ int BitrateDisplay(int bitrate) {
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(props->file_transfer_list_mutex_);
file_list = props->file_transfer_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
@@ -66,7 +68,7 @@ int Render::FileTransferWindow(
// 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 (!props->file_transfer_window_visible_) {
if (!state->file_transfer_window_visible_) {
return 0;
}
@@ -115,7 +117,7 @@ int Render::FileTransferWindow(
// Close button handling
if (!window_opened) {
props->file_transfer_window_visible_ = false;
state->file_transfer_window_visible_ = false;
ImGui::End();
return 0;
}

View File

@@ -28,98 +28,6 @@ std::vector<std::string> GetRootEntries() {
return roots;
}
int Render::ShowSimpleFileBrowser() {
std::string display_text;
if (selected_current_file_path_.empty()) {
selected_current_file_path_ = std::filesystem::current_path().string();
}
if (!tls_cert_path_self_.empty()) {
display_text =
std::filesystem::path(tls_cert_path_self_).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 (show_file_browser_) {
ImGui::PushItemFlag(ImGuiItemFlags_AutoClosePopups, false);
float fixed_width = title_bar_button_width_ * 3.8f;
ImGui::SetNextItemWidth(fixed_width);
ImGui::SetNextWindowSizeConstraints(ImVec2(fixed_width, 0),
ImVec2(fixed_width, 100.0f));
if (ImGui::BeginCombo("##select_a_file", display_text.c_str(), 0)) {
ImGui::SetWindowFontScale(0.5f);
bool file_selected = false;
auto roots = GetRootEntries();
if (selected_current_file_path_ == "Root" ||
!std::filesystem::exists(selected_current_file_path_) ||
!std::filesystem::is_directory(selected_current_file_path_)) {
for (const auto& root : roots) {
if (ImGui::Selectable(root.c_str())) {
selected_current_file_path_ = root;
tls_cert_path_self_.clear();
}
}
} else {
std::filesystem::path p(selected_current_file_path_);
if (ImGui::Selectable("..")) {
if (std::find(roots.begin(), roots.end(),
selected_current_file_path_) != roots.end()) {
selected_current_file_path_ = "Root";
} else if (p.has_parent_path()) {
selected_current_file_path_ = p.parent_path().string();
} else {
selected_current_file_path_ = "Root";
}
tls_cert_path_self_.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();
tls_cert_path_self_.clear();
}
} else {
if (ImGui::Selectable(name.c_str())) {
tls_cert_path_self_ = entry.path().string();
file_selected = true;
show_file_browser_ = false;
}
}
}
} catch (const std::exception& e) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: %s", e.what());
}
}
ImGui::EndCombo();
}
ImGui::PopItemFlag();
} else {
show_file_browser_ = true;
}
return 0;
}
int Render::SelfHostedServerWindow() {
ImGuiIO& io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) {
@@ -128,12 +36,12 @@ int Render::SelfHostedServerWindow() {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.41f));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
} else {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.41f));
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
}
self_hosted_server_config_window_pos_reset_ = false;
@@ -212,35 +120,6 @@ int Render::SelfHostedServerWindow() {
IM_ARRAYSIZE(coturn_server_port_self_));
}
ImGui::Separator();
// {
// ImGui::AlignTextToFramePadding();
// ImGui::Text(
// "%s",
// localization::reset_cert_fingerprint[localization_language_index_]
// .c_str());
// ImGui::SameLine();
// if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
// ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
// } else {
// ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
// }
// ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
// ShowSimpleFileBrowser();
// }
{
ImGui::AlignTextToFramePadding();
if (ImGui::Button(localization::reset_cert_fingerprint
[localization_language_index_]
.c_str())) {
config_center_->ClearCertFingerprint();
LOG_INFO("Certificate fingerprint cleared by user");
}
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
@@ -263,7 +142,6 @@ int Render::SelfHostedServerWindow() {
config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_server_port_self_));
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
config_center_->SetCertFilePath(tls_cert_path_self_);
strncpy(signal_server_ip_, signal_server_ip_self_,
sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
@@ -273,9 +151,6 @@ int Render::SelfHostedServerWindow() {
strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1);
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
strncpy(cert_file_path_, tls_cert_path_self_.c_str(),
sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
self_hosted_server_config_window_pos_reset_ = true;
}
@@ -306,7 +181,6 @@ int Render::SelfHostedServerWindow() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
}
ImGui::SetWindowFontScale(1.0f);

View File

@@ -1,48 +1,46 @@
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <string>
#include <vector>
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
namespace {
constexpr float kDragThresholdPx = 3.0f;
int CountDigits(int number) {
if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1;
}
// Handles dragging for the *last submitted ImGui item*.
// `reset_on_deactivate` should be false when the caller needs to know whether a
// deactivation was a click (no drag) vs a drag-release (dragging==true).
inline void HandleWindowDragForLastItem(SDL_Window* window, bool& dragging,
float& start_mouse_x,
float& start_mouse_y, int& start_win_x,
int& start_win_y,
bool reset_on_deactivate = true) {
if (!window) {
return;
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);
}
}
if (ImGui::IsItemActivated()) {
SDL_GetGlobalMouseState(&start_mouse_x, &start_mouse_y);
SDL_GetWindowPosition(window, &start_win_x, &start_win_y);
dragging = false;
}
if (ImGui::IsItemActive()) {
if (!dragging &&
ImGui::IsMouseDragging(ImGuiMouseButton_Left, kDragThresholdPx)) {
dragging = true;
}
if (dragging) {
float mouse_x = 0.0f;
float mouse_y = 0.0f;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
const int dx = (int)(mouse_x - start_mouse_x);
const int dy = (int)(mouse_y - start_mouse_y);
SDL_SetWindowPosition(window, start_win_x + dx, start_win_y + dy);
}
}
if (reset_on_deactivate && ImGui::IsItemDeactivated()) {
dragging = false;
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
@@ -57,43 +55,7 @@ int Render::ServerWindow() {
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
// Collapsed mode: no buttons; drag to move; click to restore.
if (server_window_collapsed_) {
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::InvisibleButton("##server_collapsed_area",
ImVec2(server_window_width_, server_window_height_));
HandleWindowDragForLastItem(server_window_,
server_window_collapsed_dragging_,
server_window_collapsed_drag_start_mouse_x_,
server_window_collapsed_drag_start_mouse_y_,
server_window_collapsed_drag_start_win_x_,
server_window_collapsed_drag_start_win_y_,
/*reset_on_deactivate=*/false);
const bool request_restore =
ImGui::IsItemDeactivated() && !server_window_collapsed_dragging_;
if (ImGui::IsItemDeactivated()) {
server_window_collapsed_dragging_ = false;
}
if (request_restore && server_window_) {
int w = 0;
int x = 0;
int y = 0;
int h = 0;
SDL_GetWindowSize(server_window_, &w, &h);
SDL_GetWindowPosition(server_window_, &x, &y);
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;
}
ImGui::End();
return 0;
}
server_window_title_bar_height_ = title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
@@ -105,97 +67,302 @@ int Render::ServerWindow() {
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
float server_title_bar_button_width = server_window_title_bar_height_;
float server_title_bar_button_height = server_window_title_bar_height_;
// Drag area: the title bar excluding the right-side buttons.
// Collapse/expand toggle button (FontAwesome icon).
{
const float drag_w =
server_window_width_ - server_title_bar_button_width * 2;
const float drag_h = server_title_bar_button_height;
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
ImGui::InvisibleButton("##server_title_drag_area", ImVec2(drag_w, drag_h));
HandleWindowDragForLastItem(
server_window_, server_window_dragging_,
server_window_drag_start_mouse_x_, server_window_drag_start_mouse_y_,
server_window_drag_start_win_x_, server_window_drag_start_win_y_);
}
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_button_pos_x =
server_window_width_ - server_title_bar_button_width * 2;
ImGui::SetCursorPos(ImVec2(minimize_button_pos_x, 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";
if (ImGui::Button(toggle_label.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
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);
float minimize_pos_x =
minimize_button_pos_x + server_title_bar_button_width * 0.33f;
float minimize_pos_y = server_title_bar_button_height * 0.5f;
std::string server_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(server_minimize_button.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
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);
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;
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);
}
draw_list->AddLine(
ImVec2(minimize_pos_x, minimize_pos_y),
ImVec2(minimize_pos_x + server_title_bar_button_width * 0.33f,
minimize_pos_y),
IM_COL32(0, 0, 0, 255));
ImGui::PopStyleColor(3);
float xmark_button_pos_x =
server_window_width_ - server_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 + server_title_bar_button_width * 0.5f;
float xmark_pos_y = server_title_bar_button_height * 0.5f;
float xmark_size = server_title_bar_button_width * 0.33f;
std::string server_close_button = "##xmark"; // ICON_FA_XMARK;
if (ImGui::Button(server_close_button.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
LOG_ERROR("Close button clicked");
LeaveConnection(peer_, self_hosted_id_);
}
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::PopStyleVar();
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_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::string> remote_hostnames;
remote_hostnames.reserve(connection_host_names_.size());
for (const auto& kv : connection_host_names_) {
remote_hostnames.push_back(kv.second);
}
auto find_remote_id_by_hostname =
[this](const std::string& hostname) -> std::string {
for (const auto& kv : connection_host_names_) {
if (kv.second == hostname) {
return kv.first;
}
}
return {};
};
if (!selected_server_remote_hostname_.empty()) {
if (std::find(remote_hostnames.begin(), remote_hostnames.end(),
selected_server_remote_hostname_) == remote_hostnames.end()) {
selected_server_remote_hostname_.clear();
selected_server_remote_id_.clear();
}
}
if (selected_server_remote_hostname_.empty() && !remote_hostnames.empty()) {
selected_server_remote_hostname_ = remote_hostnames.front();
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
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_hostnames.size()); i++) {
const bool selected =
(remote_hostnames[i] == selected_server_remote_hostname_);
if (ImGui::Selectable(remote_hostnames[i].c_str(), selected)) {
selected_server_remote_hostname_ = remote_hostnames[i];
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
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, 5.0f);
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

@@ -40,20 +40,6 @@ std::filesystem::path PathManager::GetLogPath() {
#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";
#endif
}
bool PathManager::CreateDirectories(const std::filesystem::path& p) {
std::error_code ec;
bool created = std::filesystem::create_directories(p, ec);

View File

@@ -26,8 +26,6 @@ class PathManager {
std::filesystem::path GetLogPath();
std::filesystem::path GetCertPath();
bool CreateDirectories(const std::filesystem::path& p);
private:

View File

@@ -70,6 +70,9 @@ target("common")
set_kind("object")
add_deps("rd_log")
add_files("src/common/*.cpp")
if is_os("macosx") then
add_files("src/common/*.mm")
end
add_includedirs("src/common", {public = true})
target("path_manager")