[refactor] reorganize platform and wire sources

This commit is contained in:
dijunkun
2026-09-01 01:29:30 +08:00
parent fb0ab4f14e
commit 90184f5251
354 changed files with 2837 additions and 2456 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,16 @@
// Application icon resource; load by the resource name IDI_ICON1.
IDI_ICON1 ICON "crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
#ifdef CROSSDESK_DEBUG
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
#else
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
#endif
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
#include "version_info.rcinc"
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Portable Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,13 @@
// Portable build resource. The app itself runs as the current user; service
// installation raises a separate UAC prompt only when the user chooses it.
IDI_ICON1 ICON "crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Portable"
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
#include "version_info.rcinc"
@@ -0,0 +1,4 @@
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Service"
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_service.exe"
#include "version_info.rcinc"
@@ -0,0 +1,18 @@
// Session helper icon and manifest resources.
IDI_ICON1 ICON "crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
#ifdef CROSSDESK_PORTABLE
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
#elif defined(CROSSDESK_DEBUG)
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
#else
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
#endif
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
#include "version_info.rcinc"
@@ -0,0 +1,54 @@
#include <winver.h>
#ifndef CROSSDESK_VERSION_STRING
#define CROSSDESK_VERSION_STRING "0.0.0"
#endif
#ifndef CROSSDESK_VERSION_NUMERIC
#define CROSSDESK_VERSION_NUMERIC 0,0,0,0
#endif
#ifndef CROSSDESK_FILE_DESCRIPTION
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
#endif
#ifndef CROSSDESK_ORIGINAL_FILENAME
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
#endif
#ifndef CROSSDESK_FILE_TYPE
#define CROSSDESK_FILE_TYPE VFT_APP
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION CROSSDESK_VERSION_NUMERIC
PRODUCTVERSION CROSSDESK_VERSION_NUMERIC
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef CROSSDESK_DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE CROSSDESK_FILE_TYPE
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "CrossDesk\0"
VALUE "FileDescription", CROSSDESK_FILE_DESCRIPTION "\0"
VALUE "FileVersion", CROSSDESK_VERSION_STRING "\0"
VALUE "InternalName", CROSSDESK_ORIGINAL_FILENAME "\0"
VALUE "LegalCopyright", "Copyright (C) CrossDesk contributors\0"
VALUE "OriginalFilename", CROSSDESK_ORIGINAL_FILENAME "\0"
VALUE "ProductName", "CrossDesk\0"
VALUE "ProductVersion", CROSSDESK_VERSION_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0409, 1200
END
END
@@ -0,0 +1,5 @@
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Windows Graphics Capture Plugin"
#define CROSSDESK_ORIGINAL_FILENAME "wgc_plugin.dll"
#define CROSSDESK_FILE_TYPE VFT_DLL
#include "version_info.rcinc"
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 3 || $# -gt 4 ]]; then
echo "Usage: $0 <debian-arch> <xmake-arch> <version> [extra-recommendation]" >&2
exit 2
fi
DEBIAN_ARCH="$1"
XMAKE_ARCH="$2"
INPUT_VERSION="$3"
EXTRA_RECOMMENDATION="${4:-}"
PKG_NAME="crossdesk"
APP_NAME="CrossDesk"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
find_slint_runtime() {
local needed="$1"
local resolved=""
resolved="$(ldd "$BUILD_BINARY" 2>/dev/null | awk -v needed="$needed" \
'$1 == needed && $2 == "=>" && $3 ~ /^\// { print $3; exit }')"
if [[ -n "$resolved" && -f "$resolved" ]]; then
echo "$resolved"
return 0
fi
local package_roots=()
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
fi
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
local package_root=""
local candidate=""
for package_root in "${package_roots[@]}"; do
[[ -d "$package_root/s/slint" ]] || continue
candidate="$(find "$package_root/s/slint" -path "*/lib/$needed" -print -quit 2>/dev/null || true)"
if [[ -n "$candidate" ]]; then
echo "$candidate"
return 0
fi
done
return 1
}
APP_VERSION="$(normalize_app_version "$INPUT_VERSION")"
DEB_VERSION="${APP_VERSION#v}"
if [[ ! "$DEB_VERSION" =~ ^[0-9][0-9A-Za-z.+:~_-]*$ ]]; then
echo "Invalid Debian version: $DEB_VERSION" >&2
exit 2
fi
BUILD_BINARY="$PROJECT_ROOT/build/linux/$XMAKE_ARCH/release/crossdesk"
if [[ ! -x "$BUILD_BINARY" ]]; then
echo "Missing release binary: $BUILD_BINARY" >&2
echo "Build it first with: xmake f -m release -y && xmake b -vy crossdesk" >&2
exit 1
fi
for command_name in readelf ldd dpkg-deb; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required packaging command is missing: $command_name" >&2
exit 1
fi
done
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-deb.XXXXXX")"
trap 'rm -rf "$WORK_DIR"' EXIT
DEB_DIR="$WORK_DIR/${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
PRIVATE_DIR="$DEB_DIR/usr/lib/$PKG_NAME"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
# runtime while using the host runtime on newer Ubuntu releases.
if readelf -d "$BUILD_BINARY" \
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
exit 1
fi
# Slint is intentionally built as a shared library. Keep that runtime private
# to CrossDesk instead of relying on a non-existent distribution package.
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
's/.*Shared library: \[\(libslint_cpp[^]]*\)\].*/\1/p' | head -n 1)"
if [[ -n "$SLINT_NEEDED" ]]; then
SLINT_LIBRARY="$(find_slint_runtime "$SLINT_NEEDED" || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "Unable to locate the required Slint runtime: $SLINT_NEEDED" >&2
exit 1
fi
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
fi
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
#!/bin/sh
private_lib_dir="/usr/lib/crossdesk"
if [ -n "${LD_LIBRARY_PATH:-}" ]; then
export LD_LIBRARY_PATH="$private_lib_dir:$LD_LIBRARY_PATH"
else
export LD_LIBRARY_PATH="$private_lib_dir"
fi
exec /usr/lib/crossdesk/crossdesk-bin "$@"
EOF
chmod 0755 "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
for size in 16 24 32 48 64 96 128 256; do
install -d "$ICON_BASE_DIR/${size}x${size}/apps"
install -m 0644 "$PROJECT_ROOT/apps/desktop/resources/linux/crossdesk_${size}x${size}.png" \
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
done
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
fi
cat > "$DEBIAN_DIR/control" <<EOF
Package: $PKG_NAME
Version: $DEB_VERSION
Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
Recommends: $RECOMMENDS
Priority: optional
Section: utils
EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" <<EOF
[Desktop Entry]
Version=$DEB_VERSION
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME
Icon=$PKG_NAME
Terminal=false
Type=Application
Categories=Utility;
EOF
rm -f "$OUTPUT_FILE"
dpkg-deb --root-owner-group --build "$DEB_DIR" "$OUTPUT_FILE"
dpkg-deb --info "$OUTPUT_FILE" >/dev/null
dpkg-deb --contents "$OUTPUT_FILE" > "$WORK_DIR/package-contents.txt"
if [[ -n "$SLINT_NEEDED" ]] && \
! grep -q "usr/lib/$PKG_NAME/$SLINT_NEEDED" "$WORK_DIR/package-contents.txt"; then
echo "Packaged artifact is missing $SLINT_NEEDED" >&2
exit 1
fi
echo "Deb package created: $OUTPUT_FILE"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
exit 2
fi
exec bash "$SCRIPT_DIR/package_deb.sh" amd64 x86_64 "$1" nvidia-cuda-toolkit
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
exit 2
fi
exec bash "$SCRIPT_DIR/package_deb.sh" arm64 arm64 "$1"
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
for command_name in codesign install_name_tool lipo otool; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "Required bundling command is missing: $command_name" >&2
exit 1
fi
done
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \
'$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')"
if [[ -z "$SLINT_REFERENCE" ]]; then
echo "The app executable does not reference the Slint runtime" >&2
exit 1
fi
SLINT_NAME="${SLINT_REFERENCE##*/}"
SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" '
$1 == dependency {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}')"
slint_runtime_version() {
local library="$1"
otool -L "$library" | awk -v name="$SLINT_NAME" '
index($1, "/" name) || $1 == "@rpath/" name {
for (i = 1; i <= NF; ++i) {
if ($i == "current") {
version = $(i + 2)
gsub(/[),]/, "", version)
print version
exit
}
}
}'
}
find_slint_runtime() {
local candidate=""
local candidate_version=""
local package_root=""
local package_roots=()
if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then
if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "$SLINT_REFERENCE"
return 0
fi
fi
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
fi
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
for package_root in "${package_roots[@]}"; do
[[ -d "$package_root/s/slint" ]] || continue
while IFS= read -r candidate; do
if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
continue
fi
candidate_version="$(slint_runtime_version "$candidate")"
if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then
continue
fi
echo "$candidate"
return 0
done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null)
done
return 1
}
SLINT_LIBRARY="$(find_slint_runtime || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2
exit 1
fi
install -d "$FRAMEWORKS_DIR"
install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME"
install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME"
if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then
install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE"
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
fi
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
# first, then seal the complete app bundle so local packages remain launchable.
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
codesign --force --deep --sign - "$APP_BUNDLE"
echo "Bundled Slint runtime: $SLINT_LIBRARY"
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/arm64/release/crossdesk"
PLATFORM="macos"
ARCH="arm64"
BINARY_ARCH="arm64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
cd "$PROJECT_ROOT"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
VOL_NAME="Install ${APP_NAME_UPPER}"
echo "delete old files"
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
mkdir -p build_pkg_temp
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
if [ -f "${ICON_PATH}" ]; then
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
else
ICON_KEY=""
fi
echo "generate Info.plist"
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundleIdentifier</key>
<string>${IDENTIFIER}</string>
<key>CFBundleVersion</key>
<string>${APP_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${APP_VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
${ICON_KEY}
<key>LSMinimumSystemVersion</key>
<string>${MACOS_MIN_VERSION}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>应用需要访问摄像头</string>
<key>NSMicrophoneUsageDescription</key>
<string>应用需要访问麦克风</string>
<key>NSAppleEventsUsageDescription</key>
<string>应用需要发送 Apple 事件</string>
<key>NSScreenCaptureUsageDescription</key>
<string>应用需要录屏权限以捕获屏幕内容</string>
</dict>
</plist>
EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully."
mkdir -p build_pkg_scripts
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..."
pkgbuild \
--identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \
--install-location "/Applications" \
--component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg
cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>${INSTALLER_TITLE}</title>
<welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
<choices-outline>
<line choice="default">
<line choice="${IDENTIFIER}"/>
</line>
</choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
<pkg-ref id="${IDENTIFIER}"/>
</choice>
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
</installer-gui-script>
EOF
productbuild \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/x86_64/release/crossdesk"
PLATFORM="macos"
ARCH="x64"
BINARY_ARCH="x86_64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
MACOS_MIN_VERSION="14.0"
cd "$PROJECT_ROOT"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
VOL_NAME="Install ${APP_NAME_UPPER}"
echo "delete old files"
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
mkdir -p build_pkg_temp
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
if [ -f "${ICON_PATH}" ]; then
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
else
ICON_KEY=""
fi
echo "generate Info.plist"
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundleDisplayName</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundleIdentifier</key>
<string>${IDENTIFIER}</string>
<key>CFBundleVersion</key>
<string>${APP_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${APP_VERSION}</string>
<key>CFBundleExecutable</key>
<string>${APP_NAME_UPPER}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
${ICON_KEY}
<key>LSMinimumSystemVersion</key>
<string>${MACOS_MIN_VERSION}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>应用需要访问摄像头</string>
<key>NSMicrophoneUsageDescription</key>
<string>应用需要访问麦克风</string>
<key>NSAppleEventsUsageDescription</key>
<string>应用需要发送 Apple 事件</string>
<key>NSScreenCaptureUsageDescription</key>
<string>应用需要录屏权限以捕获屏幕内容</string>
</dict>
</plist>
EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
echo ".app created successfully."
mkdir -p build_pkg_scripts
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..."
pkgbuild \
--identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \
--install-location "/Applications" \
--component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg
cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>${INSTALLER_TITLE}</title>
<welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
<choices-outline>
<line choice="default">
<line choice="${IDENTIFIER}"/>
</line>
</choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
<pkg-ref id="${IDENTIFIER}"/>
</choice>
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
</installer-gui-script>
EOF
productbuild \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
@@ -0,0 +1,112 @@
#!/bin/bash
set -e
APP_IDENTIFIER="cn.crossdesk.app"
# Keep known historical identifiers here. tccutil only resets identifiers that
# Launch Services can currently resolve, so path/db cleanup below remains a
# best-effort fallback for stale entries from unsigned or removed builds.
BUNDLE_IDENTIFIERS=(
"cn.crossdesk.app"
"cn.crossdesk.CrossDesk"
"com.crossdesk.app"
"com.crossdesk.CrossDesk"
"com.kunkundi.crossdesk"
"com.kunkundi.CrossDesk"
)
TCC_SERVICES=(
"ScreenCapture"
"Accessibility"
"Microphone"
"AudioCapture"
)
run_tccutil() {
local user_name="$1"
local user_id="$2"
local service="$3"
local bundle_id="$4"
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
/bin/launchctl asuser "$user_id" \
/usr/bin/sudo -u "$user_name" \
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
else
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
fi
}
reset_bundle_tcc() {
local user_name="$1"
local user_id="$2"
local bundle_id
local service
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
continue
fi
for service in "${TCC_SERVICES[@]}"; do
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
done
done
}
cleanup_tcc_db() {
local db_path="$1"
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
return
fi
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
DELETE FROM access
WHERE service IN (
'kTCCServiceScreenCapture',
'kTCCServiceAccessibility',
'kTCCServiceMicrophone',
'kTCCServiceAudioCapture'
)
AND (
client IN (
'cn.crossdesk.app',
'cn.crossdesk.CrossDesk',
'com.crossdesk.app',
'com.crossdesk.CrossDesk',
'com.kunkundi.crossdesk',
'com.kunkundi.CrossDesk'
)
OR lower(client) LIKE '%crossdesk%'
);
SQL
}
cleanup_user_tcc_db() {
local user_name="$1"
local home_dir
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
/usr/bin/awk '{print $2}')
if [ -z "$home_dir" ]; then
return
fi
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
}
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
if [ -n "$CONSOLE_USER" ] &&
[ "$CONSOLE_USER" != "root" ] &&
[ "$CONSOLE_USER" != "loginwindow" ]; then
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
cleanup_user_tcc_db "$CONSOLE_USER"
fi
# Also clear any system/root-scoped decisions as a harmless fallback.
reset_bundle_tcc "" ""
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
exit 0
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
if [[ ! -x "$APP_EXECUTABLE" ]]; then
echo "Missing app executable: $APP_EXECUTABLE" >&2
exit 1
fi
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$APP_EXECUTABLE" >&2 || true
exit 1
fi
if ! otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
END { exit(found ? 0 : 1) }'; then
echo "Missing app rpath: @executable_path/../Frameworks" >&2
exit 1
fi
verify_dependencies() {
local macho="$1"
local dependency=""
local relative_path=""
while IFS= read -r dependency; do
case "$dependency" in
/System/Library/*|/usr/lib/*)
;;
@rpath/*)
relative_path="${dependency#@rpath/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@executable_path/../Frameworks/*)
relative_path="${dependency#@executable_path/../Frameworks/}"
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
@loader_path/*)
relative_path="${dependency#@loader_path/}"
if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then
echo "Unbundled dependency in $macho: $dependency" >&2
return 1
fi
;;
*)
echo "External non-system dependency in $macho: $dependency" >&2
return 1
;;
esac
done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }')
}
verify_dependencies "$APP_EXECUTABLE"
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
if [[ -z "$SLINT_LIBRARY" ]]; then
echo "The packaged app does not contain libslint_cpp.dylib" >&2
exit 1
fi
if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2
lipo -archs "$SLINT_LIBRARY" >&2 || true
exit 1
fi
verify_dependencies "$SLINT_LIBRARY"
PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")"
BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next }
build_version && $1 == "minos" { print $2; exit }
version_min && $1 == "version" { print $2; exit }')"
if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then
echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2
exit 1
fi
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
verify_sdk_version() {
local macho="$1"
local actual_sdk=""
actual_sdk="$(otool -l "$macho" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
build_version && $1 == "sdk" { print $2; exit }')"
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
return 1
fi
}
verify_sdk_version "$APP_EXECUTABLE"
verify_sdk_version "$SLINT_LIBRARY"
fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
Binary file not shown.
@@ -0,0 +1,246 @@
; Set search path
!addincludedir "${__FILEDIR__}"
; Installer initial constants
!define PRODUCT_NAME "CrossDesk"
!define PRODUCT_VERSION "${VERSION}"
!define PRODUCT_VERSION_NUMERIC "${VERSION_NUMERIC}"
!define PRODUCT_PUBLISHER "CrossDesk"
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
!define APP_NAME "CrossDesk"
!define UNINSTALL_REG_KEY "CrossDesk"
!define PRODUCT_SERVICE_NAME "CrossDeskService"
; Installer icon path
!define MUI_ICON "${__FILEDIR__}\..\..\resources\windows\crossdesk.ico"
; Compression settings
SetCompressor /FINAL lzma
; Request admin privileges (needed to write HKLM)
RequestExecutionLevel admin
; ------ MUI Modern UI Definition ------
!include "MUI.nsh"
!define MUI_ABORTWARNING
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
; Add run-after-install option
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_TEXT "Run ${PRODUCT_NAME}"
!define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
; ------ End of MUI Definition ------
; Include LogicLib for process handling
!include "LogicLib.nsh"
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
VIProductVersion "${PRODUCT_VERSION_NUMERIC}"
VIAddVersionKey /LANG=2052 "CompanyName" "CrossDesk"
VIAddVersionKey /LANG=2052 "FileDescription" "CrossDesk Installer"
VIAddVersionKey /LANG=2052 "FileVersion" "${PRODUCT_VERSION}"
VIAddVersionKey /LANG=2052 "LegalCopyright" "Copyright (C) CrossDesk contributors"
VIAddVersionKey /LANG=2052 "OriginalFilename" "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
VIAddVersionKey /LANG=2052 "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey /LANG=2052 "ProductVersion" "${PRODUCT_VERSION}"
InstallDir "$PROGRAMFILES\CrossDesk"
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
ShowInstDetails show
Section "MainSection"
; Check if CrossDesk is running
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
${If} $R0 = 0 ;
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and continue the installation?" IDYES closeApp IDNO cancelInstall
${Else}
Goto installApp
${EndIf}
closeApp:
nsProcess::_KillProcess "$1"
Pop $R0
Sleep 500
Goto installApp
cancelInstall:
SetDetailsPrint both
MessageBox MB_ICONEXCLAMATION|MB_OK "Installation has been aborted."
Abort
installApp:
Call StopInstalledService
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
; Main application executable path
File /oname=CrossDesk.exe "..\..\..\..\build\windows\x64\release\crossdesk.exe"
; Bundle service-side binaries required by the Windows service flow
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
; Bundle runtime DLLs from the release output directory
File "..\..\..\..\build\windows\x64\release\*.dll"
Call RegisterInstalledService
; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayName" "${PRODUCT_NAME}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
SectionEnd
Section -AdditionalIcons
; Desktop shortcut
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
; Start menu shortcut
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
SectionEnd
Section "Uninstall"
; Check if CrossDesk is running
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
${If} $R0 = 0
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and uninstall?" IDYES closeApp IDNO cancelUninstall
${Else}
Goto uninstallApp
${EndIf}
closeApp:
nsProcess::_KillProcess "$1"
Pop $R0
Sleep 500
Goto uninstallApp
cancelUninstall:
SetDetailsPrint both
MessageBox MB_ICONEXCLAMATION|MB_OK "Uninstallation has been aborted."
Abort
uninstallApp:
Call un.UnregisterInstalledService
; Delete main executable and uninstaller
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\crossdesk_service.exe"
Delete "$INSTDIR\crossdesk_session_helper.exe"
Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory
RMDir /r "$INSTDIR"
; Delete desktop and start menu shortcuts
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
; Delete registry uninstall entry
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}"
; Delete remembered install dir
DeleteRegKey HKCU "Software\${PRODUCT_NAME}"
; Recursively delete CrossDesk folder in user AppData
RMDir /r "$APPDATA\CrossDesk"
RMDir /r "$LOCALAPPDATA\CrossDesk"
SectionEnd
; ------ Functions ------
Function LaunchApp
Exec "$INSTDIR\CrossDesk.exe"
FunctionEnd
Function StopInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
DetailPrint "Stopping existing CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
${If} $0 = 0
Return
${EndIf}
stop_with_sc:
DetailPrint "Stopping existing CrossDesk service via Service Control Manager"
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
${AndIf} $0 != 1062
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted."
Abort
${EndIf}
Sleep 1500
FunctionEnd
Function RegisterInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary
IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary
DetailPrint "Registering CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0
${If} $0 != 0
MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted."
Abort
${EndIf}
DetailPrint "CrossDesk service registered for on-demand start"
Return
missing_service_binary:
MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted."
Abort
FunctionEnd
Function un.UnregisterInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
DetailPrint "Stopping CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
${If} $0 = 0
DetailPrint "Removing CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0
${If} $0 = 0
Return
${EndIf}
${EndIf}
unregister_with_sc:
DetailPrint "Removing CrossDesk service via Service Control Manager"
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
${AndIf} $0 != 1062
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted."
Abort
${EndIf}
Sleep 1500
ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted."
Abort
${EndIf}
FunctionEnd
+68
View File
@@ -0,0 +1,68 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
function Normalize-AppVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion
)
$prefix = ""
$body = $InputVersion
if ($body.StartsWith("v")) {
$prefix = "v"
$body = $body.Substring(1)
}
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
}
return $InputVersion
}
function Convert-To-WindowsVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion
)
$body = $InputVersion -replace '^v', ''
$baseMatch = [regex]::Match($body, '^[0-9]+(?:\.[0-9]+){0,3}')
if (!$baseMatch.Success) {
return "0.0.0.0"
}
$parts = [System.Collections.Generic.List[int]]::new()
foreach ($part in $baseMatch.Value.Split('.')) {
$value = [int]$part
if ($value -gt 65535) {
$value = 0
}
$parts.Add($value)
}
while ($parts.Count -lt 4) {
$parts.Add(0)
}
return ($parts -join '.')
}
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
$windowsVersion = Convert-To-WindowsVersion -InputVersion $normalizedVersion
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Push-Location $scriptDir
try {
& makensis "/DVERSION=$normalizedVersion" "/DVERSION_NUMERIC=$windowsVersion" "nsis_script.nsi"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
} finally {
Pop-Location
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<parameters>
<parameter name="version" required="true" />
</parameters>
<pe-file path="crossdesk-win-x64-${version}.exe"
product-name="CrossDesk"
product-version="${version}"
file-version="${version}"
company-name="CrossDesk"
copyright="Copyright (C) CrossDesk contributors"
original-filename="crossdesk-win-x64-${version}.exe">
<authenticode-sign />
</pe-file>
</artifact-configuration>
+76
View File
@@ -0,0 +1,76 @@
#include "daemon.h"
#include <chrono>
#include <iostream>
#include <thread>
#include <utility>
#include "platform/daemon_backend.h"
namespace {
constexpr int kRestartDelayMs = 1000;
} // namespace
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
void Daemon::stop() {
running_.store(false);
crossdesk::platform::RequestDaemonStop();
}
bool Daemon::isRunning() const {
return running_.load() && !crossdesk::platform::DaemonStopRequested();
}
bool Daemon::start(MainLoopFunc loop) {
crossdesk::platform::ResetDaemonStop();
if (!crossdesk::platform::PrepareDaemon()) return false;
running_.store(true);
return runWithRestart(std::move(loop));
}
bool Daemon::runWithRestart(MainLoopFunc loop) {
int restart_count = 0;
const std::string executable_path =
crossdesk::platform::GetDaemonExecutablePath();
if (executable_path.empty()) {
std::cerr
<< "Failed to get executable path, falling back to direct execution"
<< std::endl;
while (isRunning()) {
try {
loop();
break;
} catch (...) {
++restart_count;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(kRestartDelayMs));
}
}
return true;
}
while (isRunning()) {
const crossdesk::platform::DaemonChildResult result =
crossdesk::platform::RunDaemonChild(
executable_path, [this] { return isRunning(); });
if (!isRunning() || result.normal_exit) break;
++restart_count;
if (!result.started) {
std::cerr << "Failed to create child process";
} else if (result.terminated_by_signal) {
std::cerr << "Child process crashed with signal " << result.exit_code;
} else {
std::cerr << "Child process exited with code " << result.exit_code;
}
std::cerr << ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
}
return true;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-19
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <atomic>
#include <functional>
#include <string>
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
Daemon(const std::string& name);
bool start(MainLoopFunc loop);
void stop();
bool isRunning() const;
private:
std::string name_;
bool runWithRestart(MainLoopFunc loop);
std::atomic<bool> running_;
};
#endif
+231
View File
@@ -0,0 +1,231 @@
#ifdef _WIN32
#ifdef CROSSDESK_DEBUG
#pragma comment(linker, "/subsystem:\"console\"")
#else
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
#endif
#endif
#include <cstring>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#ifdef _WIN32
#include <cstdio>
#include "service_host.h"
#endif
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "render.h"
#ifdef _WIN32
namespace {
void EnsureConsoleForCli() {
static bool console_ready = false;
if (console_ready) {
return;
}
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
DWORD error = GetLastError();
if (error != ERROR_ACCESS_DENIED) {
AllocConsole();
}
}
FILE* stream = nullptr;
freopen_s(&stream, "CONOUT$", "w", stdout);
freopen_s(&stream, "CONOUT$", "w", stderr);
freopen_s(&stream, "CONIN$", "r", stdin);
SetConsoleOutputCP(CP_UTF8);
console_ready = true;
}
void PrintServiceCliUsage() {
std::cout
<< "CrossDesk service management commands\n"
<< " --service-install Install the sibling crossdesk_service.exe\n"
<< " --service-uninstall Remove the installed Windows service\n"
<< " --service-start Start the Windows service\n"
<< " --service-stop Stop the Windows service\n"
<< " --service-sas Ask the service to send Secure Attention "
"Sequence\n"
<< " --service-ping Ping the service over named pipe IPC\n"
<< " --service-status Query service runtime status\n"
<< " --service-help Show this help\n";
}
std::wstring GetCurrentExecutablePathW() {
wchar_t path[MAX_PATH] = {0};
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return L"";
}
return std::wstring(path, length);
}
std::filesystem::path GetSiblingServiceExecutablePath() {
std::wstring current_executable = GetCurrentExecutablePathW();
if (current_executable.empty()) {
return {};
}
return std::filesystem::path(current_executable).parent_path() /
L"crossdesk_service.exe";
}
bool IsServiceCliCommand(const char* arg) {
if (arg == nullptr) {
return false;
}
return std::strcmp(arg, "--service-install") == 0 ||
std::strcmp(arg, "--service-uninstall") == 0 ||
std::strcmp(arg, "--service-start") == 0 ||
std::strcmp(arg, "--service-stop") == 0 ||
std::strcmp(arg, "--service-sas") == 0 ||
std::strcmp(arg, "--service-ping") == 0 ||
std::strcmp(arg, "--service-status") == 0 ||
std::strcmp(arg, "--service-help") == 0;
}
void TryStartManagedWindowsService() {
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
if (service_path.empty() || !std::filesystem::exists(service_path)) {
return;
}
if (!crossdesk::IsCrossDeskServiceInstalled()) {
return;
}
crossdesk::StartCrossDeskService();
}
int HandleServiceCliCommand(const std::string& command) {
EnsureConsoleForCli();
if (command == "--service-help") {
PrintServiceCliUsage();
return 0;
}
if (command == "--service-install") {
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
if (service_path.empty()) {
std::cerr << "Failed to locate crossdesk_service.exe" << std::endl;
return 1;
}
if (!std::filesystem::exists(service_path)) {
std::cerr << "Service binary not found: " << service_path.string()
<< std::endl;
return 1;
}
bool success = crossdesk::InstallCrossDeskService(service_path.wstring());
std::cout << (success ? "install ok" : "install failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-uninstall") {
bool success = crossdesk::UninstallCrossDeskService();
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-start") {
bool success = crossdesk::StartCrossDeskService();
std::cout << (success ? "start ok" : "start failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-stop") {
bool success = crossdesk::StopCrossDeskService();
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-sas") {
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
return 0;
}
if (command == "--service-ping") {
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
return 0;
}
if (command == "--service-status") {
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
return 0;
}
PrintServiceCliUsage();
return 1;
}
} // namespace
#endif
int main(int argc, char* argv[]) {
#ifdef _WIN32
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
}
#endif
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
}
if (is_child) {
// child process: run render directly
crossdesk::Render render;
render.Run();
return 0;
}
#ifdef _WIN32
TryStartManagedWindowsService();
#endif
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
}
if (enable_daemon) {
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
crossdesk::Render render;
render.Run();
daemon.stop();
};
// start daemon and return result
bool success = daemon.start(main_loop);
return success ? 0 : 1;
}
// run without daemon: direct execution
crossdesk::Render render;
render.Run();
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
#include "autostart.h"
#include "platform/autostart_backend.h"
namespace crossdesk {
bool EnableAutostart(const std::string& app_name) {
const std::string executable_path = platform::GetAutostartExecutablePath();
return !executable_path.empty() &&
platform::EnableAutostart(app_name, executable_path);
}
bool DisableAutostart(const std::string& app_name) {
return platform::DisableAutostart(app_name);
}
bool IsAutostartEnabled(const std::string& app_name) {
return platform::IsAutostartEnabled(app_name);
}
} // namespace crossdesk
+21
View File
@@ -0,0 +1,21 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-18
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _AUTOSTART_H_
#define _AUTOSTART_H_
#include <string>
namespace crossdesk {
bool EnableAutostart(const std::string& appName);
bool DisableAutostart(const std::string& appName);
bool IsAutostartEnabled(const std::string& appName);
} // namespace crossdesk
#endif
+46
View File
@@ -0,0 +1,46 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-05-15
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISPLAY_INFO_H_
#define _DISPLAY_INFO_H_
#include <string>
namespace crossdesk {
class DisplayInfo {
public:
DisplayInfo(std::string name, int left, int top, int right, int bottom)
: name(name), left(left), top(top), right(right), bottom(bottom) {
width = right - left;
height = bottom - top;
}
DisplayInfo(void* handle, std::string name, bool is_primary, int left,
int top, int right, int bottom)
: handle(handle),
name(name),
is_primary(is_primary),
left(left),
top(top),
right(right),
bottom(bottom) {
width = right - left;
height = bottom - top;
}
~DisplayInfo() {}
void* handle = nullptr;
std::string name = "";
bool is_primary = false;
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
int width = 0;
int height = 0;
};
} // namespace crossdesk
#endif
+21
View File
@@ -0,0 +1,21 @@
#ifndef CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
#define CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
#include <filesystem>
#include <string>
#include <string_view>
namespace crossdesk {
inline std::filesystem::path PathFromUtf8(std::string_view value) {
#if defined(__cpp_char8_t)
const auto *begin = reinterpret_cast<const char8_t *>(value.data());
return std::filesystem::path(std::u8string(begin, begin + value.size()));
#else
return std::filesystem::u8path(value.begin(), value.end());
#endif
}
} // namespace crossdesk
#endif // CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
+62
View File
@@ -0,0 +1,62 @@
#include "nv12_scaler.h"
#include <cstddef>
#include <limits>
#include "libyuv/planar_functions.h"
namespace crossdesk {
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_uv, int src_stride_uv, int src_width,
int src_height, uint8_t* dst_y, int dst_stride_y,
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
int dst_height, libyuv::FilterMode filtering,
std::vector<uint8_t>* scratch_buffer) {
if (!src_y || !src_uv || !dst_y || !dst_uv || !scratch_buffer ||
src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 ||
(src_width & 1) != 0 || (src_height & 1) != 0 || (dst_width & 1) != 0 ||
(dst_height & 1) != 0) {
return -1;
}
const int src_chroma_width = src_width / 2;
const int src_chroma_height = src_height / 2;
const int dst_chroma_width = dst_width / 2;
const int dst_chroma_height = dst_height / 2;
const size_t src_chroma_size =
static_cast<size_t>(src_chroma_width) * src_chroma_height;
const size_t dst_chroma_size =
static_cast<size_t>(dst_chroma_width) * dst_chroma_height;
const size_t max_size = std::numeric_limits<size_t>::max();
if (src_chroma_size > max_size / 2 || dst_chroma_size > max_size / 2) {
return -1;
}
const size_t src_planes_size = 2 * src_chroma_size;
const size_t dst_planes_size = 2 * dst_chroma_size;
if (src_planes_size > max_size - dst_planes_size) {
return -1;
}
scratch_buffer->resize(src_planes_size + dst_planes_size);
uint8_t* src_u = scratch_buffer->data();
uint8_t* src_v = src_u + src_chroma_size;
uint8_t* dst_u = src_v + src_chroma_size;
uint8_t* dst_v = dst_u + dst_chroma_size;
libyuv::SplitUVPlane(src_uv, src_stride_uv, src_u, src_chroma_width, src_v,
src_chroma_width, src_chroma_width, src_chroma_height);
const int result = libyuv::I420Scale(
src_y, src_stride_y, src_u, src_chroma_width, src_v, src_chroma_width,
src_width, src_height, dst_y, dst_stride_y, dst_u, dst_chroma_width,
dst_v, dst_chroma_width, dst_width, dst_height, filtering);
if (result != 0) {
return result;
}
libyuv::MergeUVPlane(dst_u, dst_chroma_width, dst_v, dst_chroma_width, dst_uv,
dst_stride_uv, dst_chroma_width, dst_chroma_height);
return 0;
}
} // namespace crossdesk
+23
View File
@@ -0,0 +1,23 @@
#ifndef CROSSDESK_COMMON_NV12_SCALER_H_
#define CROSSDESK_COMMON_NV12_SCALER_H_
#include <cstdint>
#include <vector>
#include "libyuv/scale.h"
namespace crossdesk {
// Scales NV12 by separating the interleaved chroma plane before filtering.
// This avoids the downward chroma rounding bias in libyuv::NV12Scale while
// preserving the requested filtering quality.
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_uv, int src_stride_uv, int src_width,
int src_height, uint8_t* dst_y, int dst_stride_y,
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
int dst_height, libyuv::FilterMode filtering,
std::vector<uint8_t>* scratch_buffer);
} // namespace crossdesk
#endif // CROSSDESK_COMMON_NV12_SCALER_H_
+19
View File
@@ -0,0 +1,19 @@
/*
* @Author: DI JUNKUN
* @Date: 2023-12-18
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PLATFORM_H_
#define _PLATFORM_H_
#include <string>
namespace crossdesk {
std::string GetMac();
std::string GetHostName();
bool IsWaylandSession();
} // namespace crossdesk
#endif
@@ -0,0 +1,63 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk
@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif
@@ -0,0 +1,44 @@
#include "shared_cursor_state.h"
#include <remote_action.h>
#include <mutex>
namespace crossdesk {
namespace {
std::mutex g_cursor_state_mutex;
SharedCursorState g_cursor_state;
bool g_cursor_state_available = false;
} // namespace
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = visible;
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
g_cursor_state_available = true;
}
bool GetSharedCursorState(SharedCursorState* state) {
if (!state) {
return false;
}
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
if (!g_cursor_state_available) {
return false;
}
*state = g_cursor_state;
return true;
}
void ClearSharedCursorState() {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = false;
g_cursor_state.shape = RemoteCursorShape::none;
g_cursor_state_available = false;
}
} // namespace crossdesk
@@ -0,0 +1,22 @@
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#include <cstdint>
#include <remote_cursor_shape.h>
namespace crossdesk {
struct SharedCursorState {
uint64_t generation = 0;
bool visible = false;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
bool GetSharedCursorState(SharedCursorState* state);
void ClearSharedCursorState();
} // namespace crossdesk
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,460 @@
#include "config_center.h"
#include "autostart.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
bool IsValidTurnModeValue(long value) {
return value >= static_cast<long>(ConfigCenter::TURN_MODE::DISABLED) &&
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
}
} // namespace
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
Load();
}
ConfigCenter::~ConfigCenter() {}
int ConfigCenter::Load() {
SI_Error rc = ini_.LoadFile(config_path_.c_str());
if (rc < 0) {
Save();
return -1;
}
bool persist_config_migration = false;
persist_config_migration |= ini_.Delete(section_, "video_content_type");
persist_config_migration |= ini_.Delete(section_, "screen_content");
persist_config_migration |=
ini_.Delete(section_, "enable_desktop_quality_optimization");
persist_config_migration |=
ini_.Delete(section_, "enable_minimize_to_tray");
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
ini_.GetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_)));
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
hardware_video_codec_);
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
const long parsed_turn_mode = ini_.GetLongValue(
section_, "turn_mode", static_cast<long>(turn_mode_));
if (IsValidTurnModeValue(parsed_turn_mode)) {
turn_mode_ = static_cast<TURN_MODE>(parsed_turn_mode);
} else {
LOG_WARN("Invalid TURN mode [{}], using auto UDP/TCP",
parsed_turn_mode);
turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
}
} else {
const bool legacy_enable_turn = ini_.GetBoolValue(
section_, "enable_turn", turn_mode_ != TURN_MODE::DISABLED);
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
: TURN_MODE::DISABLED;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
persist_config_migration = true;
}
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
} else {
signal_server_host_ = "";
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
} else {
signal_server_port_ = 0;
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
} else {
coturn_server_port_ = 0;
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
} else {
file_transfer_save_path_ = "";
}
if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
return -1;
}
return 0;
}
int ConfigCenter::Save() {
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// only save when self hosted
if (enable_self_hosted_) {
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// setters
int ConfigCenter::SetLanguage(LANGUAGE language) {
language_ = language;
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
video_quality_ = video_quality;
ini_.SetLongValue(section_, "video_quality",
static_cast<long>(video_quality_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
video_frame_rate_ = video_frame_rate;
ini_.SetLongValue(section_, "video_frame_rate",
static_cast<long>(video_frame_rate_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetVideoEncodeFormat(
VIDEO_ENCODE_FORMAT video_encode_format) {
video_encode_format_ = video_encode_format;
ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
hardware_video_codec_ = hardware_video_codec;
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
return -1;
}
turn_mode_ = turn_mode;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetTurn(bool enable_turn) {
if (!enable_turn) {
return SetTurnMode(TURN_MODE::DISABLED);
}
if (turn_mode_ == TURN_MODE::DISABLED) {
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
}
return SetTurnMode(turn_mode_);
}
int ConfigCenter::SetSrtp(bool enable_srtp) {
enable_srtp_ = enable_srtp;
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetServerPort(int signal_server_port) {
signal_server_port_ = signal_server_port;
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
coturn_server_port_ = coturn_server_port;
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// load from config if self hosted is enabled
if (enable_self_hosted_) {
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "signal_server_port", 0));
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
if (enable_autostart) {
success = EnableAutostart("CrossDesk");
} else {
success = DisableAutostart("CrossDesk");
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
if (!success) {
LOG_ERROR("SetAutostart failed");
return -1;
}
return 0;
}
int ConfigCenter::SetDaemon(bool enable_daemon) {
enable_daemon_ = enable_daemon;
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
portable_service_prompt_suppressed_ = suppressed;
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
return video_quality_;
}
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
return video_frame_rate_;
}
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
return video_encode_format_;
}
bool ConfigCenter::IsHardwareVideoCodec() const {
return hardware_video_codec_;
}
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
return turn_mode_;
}
bool ConfigCenter::IsEnableTurn() const {
return turn_mode_ != TURN_MODE::DISABLED;
}
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
std::string ConfigCenter::GetSignalServerHost() const {
return signal_server_host_;
}
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
int ConfigCenter::GetDefaultSignalServerPort() const {
return server_port_default_;
}
int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
return file_transfer_save_path_;
}
} // namespace crossdesk
@@ -0,0 +1,101 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CONFIG_CENTER_H_
#define _CONFIG_CENTER_H_
#include <string>
#include "SimpleIni.h"
namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE {
DISABLED = 0,
AUTO_UDP_TCP = 1,
FORCE_UDP = 2,
FORCE_TCP = 3
};
public:
explicit ConfigCenter(const std::string& config_path = "config.ini");
~ConfigCenter();
// write config
int SetLanguage(LANGUAGE language);
int SetVideoQuality(VIDEO_QUALITY video_quality);
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp);
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetSelfHosted(bool enable_self_hosted);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path);
// read config
LANGUAGE GetLanguage() const;
VIDEO_QUALITY GetVideoQuality() const;
VIDEO_FRAME_RATE GetVideoFrameRate() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
bool IsHardwareVideoCodec() const;
TURN_MODE GetTurnMode() const;
bool IsEnableTurn() const;
bool IsEnableSrtp() const;
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
bool IsSelfHosted() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const;
int Load();
int Save();
private:
std::string config_path_;
CSimpleIniA ini_;
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
bool enable_srtp_ = false;
std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn";
int signal_server_port_ = 0;
int server_port_default_ = 9099;
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
bool enable_self_hosted_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = "";
};
} // namespace crossdesk
#endif
@@ -0,0 +1,52 @@
/*
* @Author: DI JUNKUN
* @Date: 2023-12-14
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_CONTROLLER_H_
#define _DEVICE_CONTROLLER_H_
#include <cstdint>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
// int key_code, bool is_down, uint32_t scan_code, bool extended
using OnKeyAction = void (*)(int, bool, uint32_t, bool, void*);
class DeviceController {
public:
virtual ~DeviceController() = default;
};
class MouseController : public DeviceController {
public:
~MouseController() override = default;
virtual int Init(std::vector<DisplayInfo> display_info_list) = 0;
virtual int Destroy() = 0;
virtual int SendMouseCommand(RemoteAction remote_action,
int display_index) = 0;
virtual void UpdateDisplayInfoList(
const std::vector<DisplayInfo>&) {}
};
class KeyboardCapturer : public DeviceController {
public:
~KeyboardCapturer() override = default;
virtual int Hook(OnKeyAction on_key_action, void* user_ptr) = 0;
virtual int Unhook() = 0;
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false) = 0;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,25 @@
/*
* @Author: DI JUNKUN
* @Date: 2023-12-14
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_CONTROLLER_FACTORY_H_
#define _DEVICE_CONTROLLER_FACTORY_H_
#include "device_controller.h"
namespace crossdesk {
class DeviceControllerFactory {
public:
enum Device { Mouse = 0, Keyboard };
public:
virtual ~DeviceControllerFactory() {}
public:
DeviceController* Create(Device device);
};
} // namespace crossdesk
#endif
@@ -0,0 +1,748 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-11-25
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _KEYBOARD_CONVERTER_H_
#define _KEYBOARD_CONVERTER_H_
#include <map>
namespace crossdesk {
// Windows vkCode to macOS CGKeyCode (104 keys)
std::map<int, int> vkCodeToCGKeyCode = {
// A-Z
{0x41, 0x00}, // A
{0x42, 0x0B}, // B
{0x43, 0x08}, // C
{0x44, 0x02}, // D
{0x45, 0x0E}, // E
{0x46, 0x03}, // F
{0x47, 0x05}, // G
{0x48, 0x04}, // H
{0x49, 0x22}, // I
{0x4A, 0x26}, // J
{0x4B, 0x28}, // K
{0x4C, 0x25}, // L
{0x4D, 0x2E}, // M
{0x4E, 0x2D}, // N
{0x4F, 0x1F}, // O
{0x50, 0x23}, // P
{0x51, 0x0C}, // Q
{0x52, 0x0F}, // R
{0x53, 0x01}, // S
{0x54, 0x11}, // T
{0x55, 0x20}, // U
{0x56, 0x09}, // V
{0x57, 0x0D}, // W
{0x58, 0x07}, // X
{0x59, 0x10}, // Y
{0x5A, 0x06}, // Z
// 0-9
{0x30, 0x1D}, // 0
{0x31, 0x12}, // 1
{0x32, 0x13}, // 2
{0x33, 0x14}, // 3
{0x34, 0x15}, // 4
{0x35, 0x17}, // 5
{0x36, 0x16}, // 6
{0x37, 0x1A}, // 7
{0x38, 0x1C}, // 8
{0x39, 0x19}, // 9
// F1-F12
{0x70, 0x7A}, // F1
{0x71, 0x78}, // F2
{0x72, 0x63}, // F3
{0x73, 0x76}, // F4
{0x74, 0x60}, // F5
{0x75, 0x61}, // F6
{0x76, 0x62}, // F7
{0x77, 0x64}, // F8
{0x78, 0x65}, // F9
{0x79, 0x6D}, // F10
{0x7A, 0x67}, // F11
{0x7B, 0x6F}, // F12
// control keys
{0x1B, 0x35}, // Escape
{0x0D, 0x24}, // Enter
{0x20, 0x31}, // Space
{0x08, 0x33}, // Backspace
{0x09, 0x30}, // Tab
{0x2C, 0x69}, // Print Screen(F13)
{0x2D, 0x72}, // Insert
{0x2E, 0x75}, // Delete
{0x24, 0x73}, // Home
{0x23, 0x77}, // End
{0x21, 0x74}, // Page Up
{0x22, 0x79}, // Page Down
// arrow keys
{0x25, 0x7B}, // Left Arrow
{0x27, 0x7C}, // Right Arrow
{0x26, 0x7E}, // Up Arrow
{0x28, 0x7D}, // Down Arrow
// numpad
{0x60, 0x52}, // Numpad 0
{0x61, 0x53}, // Numpad 1
{0x62, 0x54}, // Numpad 2
{0x63, 0x55}, // Numpad 3
{0x64, 0x56}, // Numpad 4
{0x65, 0x57}, // Numpad 5
{0x66, 0x58}, // Numpad 6
{0x67, 0x59}, // Numpad 7
{0x68, 0x5B}, // Numpad 8
{0x69, 0x5C}, // Numpad 9
{0x90, 0x47}, // Num Lock / Keypad Clear
{0x6E, 0x41}, // Numpad .
{0x6F, 0x4B}, // Numpad /
{0x6A, 0x43}, // Numpad *
{0x6D, 0x4E}, // Numpad -
{0x6B, 0x45}, // Numpad +
// symbol keys
{0xBA, 0x29}, // ; (Semicolon)
{0xDE, 0x27}, // ' (Quote)
{0xC0, 0x32}, // ` (Backtick)
{0xBC, 0x2B}, // , (Comma)
{0xBE, 0x2F}, // . (Period)
{0xBF, 0x2C}, // / (Slash)
{0xDC, 0x2A}, // \ (Backslash)
{0xDB, 0x21}, // [ (Left Bracket)
{0xDD, 0x1E}, // ] (Right Bracket)
{0xBD, 0x1B}, // - (Minus)
{0xBB, 0x18}, // = (Equals)
// modifier keys
{0x14, 0x39}, // Caps Lock
{0xA0, 0x38}, // Shift (Left)
{0xA1, 0x3C}, // Shift (Right)
{0xA2, 0x3B}, // Ctrl (Left)
{0xA3, 0x3E}, // Ctrl (Right)
{0xA4, 0x3A}, // Alt (Left)
{0xA5, 0x3D}, // Alt (Right)
{0x5B, 0x37}, // Left Command (Windows key)
{0x5C, 0x36}, // Right Command
};
// macOS CGKeyCode to Windows vkCode
std::map<int, int> CGKeyCodeToVkCode = {
// A-Z
{0x00, 0x41}, // A
{0x0B, 0x42}, // B
{0x08, 0x43}, // C
{0x02, 0x44}, // D
{0x0E, 0x45}, // E
{0x03, 0x46}, // F
{0x05, 0x47}, // G
{0x04, 0x48}, // H
{0x22, 0x49}, // I
{0x26, 0x4A}, // J
{0x28, 0x4B}, // K
{0x25, 0x4C}, // L
{0x2E, 0x4D}, // M
{0x2D, 0x4E}, // N
{0x1F, 0x4F}, // O
{0x23, 0x50}, // P
{0x0C, 0x51}, // Q
{0x0F, 0x52}, // R
{0x01, 0x53}, // S
{0x11, 0x54}, // T
{0x20, 0x55}, // U
{0x09, 0x56}, // V
{0x0D, 0x57}, // W
{0x07, 0x58}, // X
{0x10, 0x59}, // Y
{0x06, 0x5A}, // Z
// 0-9
{0x1D, 0x30}, // 0
{0x12, 0x31}, // 1
{0x13, 0x32}, // 2
{0x14, 0x33}, // 3
{0x15, 0x34}, // 4
{0x17, 0x35}, // 5
{0x16, 0x36}, // 6
{0x1A, 0x37}, // 7
{0x1C, 0x38}, // 8
{0x19, 0x39}, // 9
// F1-F12
{0x7A, 0x70}, // F1
{0x78, 0x71}, // F2
{0x63, 0x72}, // F3
{0x76, 0x73}, // F4
{0x60, 0x74}, // F5
{0x61, 0x75}, // F6
{0x62, 0x76}, // F7
{0x64, 0x77}, // F8
{0x65, 0x78}, // F9
{0x6D, 0x79}, // F10
{0x67, 0x7A}, // F11
{0x6F, 0x7B}, // F12
// control keys
{0x35, 0x1B}, // Escape
{0x24, 0x0D}, // Enter
{0x31, 0x20}, // Space
{0x33, 0x08}, // Backspace
{0x30, 0x09}, // Tab
{0x69, 0x2C}, // Print Screen(F13)
{0x72, 0x2D}, // Insert
{0x75, 0x2E}, // Delete
{0x73, 0x24}, // Home
{0x77, 0x23}, // End
{0x74, 0x21}, // Page Up
{0x79, 0x22}, // Page Down
// arrow keys
{0x7B, 0x25}, // Left Arrow
{0x7C, 0x27}, // Right Arrow
{0x7E, 0x26}, // Up Arrow
{0x7D, 0x28}, // Down Arrow
// numpad
{0x52, 0x60}, // Numpad 0
{0x53, 0x61}, // Numpad 1
{0x54, 0x62}, // Numpad 2
{0x55, 0x63}, // Numpad 3
{0x56, 0x64}, // Numpad 4
{0x57, 0x65}, // Numpad 5
{0x58, 0x66}, // Numpad 6
{0x59, 0x67}, // Numpad 7
{0x5B, 0x68}, // Numpad 8
{0x5C, 0x69}, // Numpad 9
{0x47, 0x90}, // Num Lock / Keypad Clear
{0x41, 0x6E}, // Numpad .
{0x4B, 0x6F}, // Numpad /
{0x43, 0x6A}, // Numpad *
{0x4E, 0x6D}, // Numpad -
{0x45, 0x6B}, // Numpad +
// symbol keys
{0x29, 0xBA}, // ; (Semicolon)
{0x27, 0xDE}, // ' (Quote)
{0x32, 0xC0}, // ` (Backtick)
{0x2B, 0xBC}, // , (Comma)
{0x2F, 0xBE}, // . (Period)
{0x2C, 0xBF}, // / (Slash)
{0x2A, 0xDC}, // \ (Backslash)
{0x21, 0xDB}, // [ (Left Bracket)
{0x1E, 0xDD}, // ] (Right Bracket)
{0x1B, 0xBD}, // - (Minus)
{0x18, 0xBB}, // = (Equals)
// modifier keys
{0x39, 0x14}, // Caps Lock
{0x38, 0xA0}, // Shift (Left)
{0x3C, 0xA1}, // Shift (Right)
{0x3B, 0xA2}, // Control (Left)
{0x3E, 0xA3}, // Control (Right)
{0x3A, 0xA4}, // Alt (Left)
{0x3D, 0xA5}, // Alt (Right)
{0x37, 0x5B}, // Left Command (Windows key)
{0x36, 0x5C}, // Right Command
};
// Windows vkCode to X11 KeySym
std::map<int, int> vkCodeToX11KeySym = {
// A-Z
{0x41, 0x0041}, // A
{0x42, 0x0042}, // B
{0x43, 0x0043}, // C
{0x44, 0x0044}, // D
{0x45, 0x0045}, // E
{0x46, 0x0046}, // F
{0x47, 0x0047}, // G
{0x48, 0x0048}, // H
{0x49, 0x0049}, // I
{0x4A, 0x004A}, // J
{0x4B, 0x004B}, // K
{0x4C, 0x004C}, // L
{0x4D, 0x004D}, // M
{0x4E, 0x004E}, // N
{0x4F, 0x004F}, // O
{0x50, 0x0050}, // P
{0x51, 0x0051}, // Q
{0x52, 0x0052}, // R
{0x53, 0x0053}, // S
{0x54, 0x0054}, // T
{0x55, 0x0055}, // U
{0x56, 0x0056}, // V
{0x57, 0x0057}, // W
{0x58, 0x0058}, // X
{0x59, 0x0059}, // Y
{0x5A, 0x005A}, // Z
// 0-9
{0x30, 0x0030}, // 0
{0x31, 0x0031}, // 1
{0x32, 0x0032}, // 2
{0x33, 0x0033}, // 3
{0x34, 0x0034}, // 4
{0x35, 0x0035}, // 5
{0x36, 0x0036}, // 6
{0x37, 0x0037}, // 7
{0x38, 0x0038}, // 8
{0x39, 0x0039}, // 9
// F1-F12
{0x70, 0xFFBE}, // F1
{0x71, 0xFFBF}, // F2
{0x72, 0xFFC0}, // F3
{0x73, 0xFFC1}, // F4
{0x74, 0xFFC2}, // F5
{0x75, 0xFFC3}, // F6
{0x76, 0xFFC4}, // F7
{0x77, 0xFFC5}, // F8
{0x78, 0xFFC6}, // F9
{0x79, 0xFFC7}, // F10
{0x7A, 0xFFC8}, // F11
{0x7B, 0xFFC9}, // F12
// control keys
{0x1B, 0xFF1B}, // Escape
{0x0D, 0xFF0D}, // Enter
{0x20, 0x0020}, // Space
{0x08, 0xFF08}, // Backspace
{0x09, 0xFF09}, // Tab
{0x2C, 0xFF15}, // Print Screen
{0x91, 0xFF14}, // Scroll Lock
{0x13, 0xFF13}, // Pause/Break
{0x2D, 0xFF63}, // Insert
{0x2E, 0xFFFF}, // Delete
{0x24, 0xFF50}, // Home
{0x23, 0xFF57}, // End
{0x21, 0xFF55}, // Page Up
{0x22, 0xFF56}, // Page Down
// arrow keys
{0x25, 0xFF51}, // Left Arrow
{0x27, 0xFF53}, // Right Arrow
{0x26, 0xFF52}, // Up Arrow
{0x28, 0xFF54}, // Down Arrow
// numpad
{0x60, 0xFFB0}, // Numpad 0
{0x61, 0xFFB1}, // Numpad 1
{0x62, 0xFFB2}, // Numpad 2
{0x63, 0xFFB3}, // Numpad 3
{0x64, 0xFFB4}, // Numpad 4
{0x65, 0xFFB5}, // Numpad 5
{0x66, 0xFFB6}, // Numpad 6
{0x67, 0xFFB7}, // Numpad 7
{0x68, 0xFFB8}, // Numpad 8
{0x69, 0xFFB9}, // Numpad 9
{0x90, 0xFF7F}, // Num Lock
{0x6E, 0xFFAE}, // Numpad .
{0x6F, 0xFFAF}, // Numpad /
{0x6A, 0xFFAA}, // Numpad *
{0x6D, 0xFFAD}, // Numpad -
{0x6B, 0xFFAB}, // Numpad +
// symbol keys
{0xBA, 0x003B}, // ; (Semicolon)
{0xDE, 0x0027}, // ' (Quote)
{0xC0, 0x007E}, // ` (Grave)
{0xBC, 0x002C}, // , (Comma)
{0xBE, 0x002E}, // . (Period)
{0xBF, 0x002F}, // / (Slash)
{0xDC, 0x005C}, // \ (Backslash)
{0xDB, 0x005B}, // [ (Left Bracket)
{0xDD, 0x005D}, // ] (Right Bracket)
{0xBD, 0x002D}, // - (Minus)
{0xBB, 0x003D}, // = (Equals)
// modifier keys
{0x14, 0xFFE5}, // Caps Lock
{0xA0, 0xFFE1}, // Shift (Left)
{0xA1, 0xFFE2}, // Shift (Right)
{0xA2, 0xFFE3}, // Ctrl (Left)
{0xA3, 0xFFE4}, // Ctrl (Right)
{0xA4, 0xFFE9}, // Alt (Left)
{0xA5, 0xFFEA}, // Alt (Right)
{0x5B, 0xFFEB}, // Left Command (Windows key)
{0x5C, 0xFFEC}, // Right Command
};
// X11 KeySym to Windows vkCode
// std::map<int, int> x11KeySymToVkCode = []() {
// std::map<int, int> result;
// for (const auto& pair : vkCodeToX11KeySym) {
// result[pair.second] = pair.first;
// }
// return result;
// }();
std::map<int, int> x11KeySymToVkCode = {
// A-Z
{0x0041, 0x41}, // A
{0x0042, 0x42}, // B
{0x0043, 0x43}, // C
{0x0044, 0x44}, // D
{0x0045, 0x45}, // E
{0x0046, 0x46}, // F
{0x0047, 0x47}, // G
{0x0048, 0x48}, // H
{0x0049, 0x49}, // I
{0x004A, 0x4A}, // J
{0x004B, 0x4B}, // K
{0x004C, 0x4C}, // L
{0x004D, 0x4D}, // M
{0x004E, 0x4E}, // N
{0x004F, 0x4F}, // O
{0x0050, 0x50}, // P
{0x0051, 0x51}, // Q
{0x0052, 0x52}, // R
{0x0053, 0x53}, // S
{0x0054, 0x54}, // T
{0x0055, 0x55}, // U
{0x0056, 0x56}, // V
{0x0057, 0x57}, // W
{0x0058, 0x58}, // X
{0x0059, 0x59}, // Y
{0x005A, 0x5A}, // Z
// 0-9
{0x0030, 0x30}, // 0
{0x0031, 0x31}, // 1
{0x0032, 0x32}, // 2
{0x0033, 0x33}, // 3
{0x0034, 0x34}, // 4
{0x0035, 0x35}, // 5
{0x0036, 0x36}, // 6
{0x0037, 0x37}, // 7
{0x0038, 0x38}, // 8
{0x0039, 0x39}, // 9
// F1-F12
{0xFFBE, 0x70}, // F1
{0xFFBF, 0x71}, // F2
{0xFFC0, 0x72}, // F3
{0xFFC1, 0x73}, // F4
{0xFFC2, 0x74}, // F5
{0xFFC3, 0x75}, // F6
{0xFFC4, 0x76}, // F7
{0xFFC5, 0x77}, // F8
{0xFFC6, 0x78}, // F9
{0xFFC7, 0x79}, // F10
{0xFFC8, 0x7A}, // F11
{0xFFC9, 0x7B}, // F12
// control keys
{0xFF1B, 0x1B}, // Escape
{0xFF0D, 0x0D}, // Enter
{0x0020, 0x20}, // Space
{0xFF08, 0x08}, // Backspace
{0xFF09, 0x09}, // Tab
{0xFF15, 0x2C}, // Print Screen
{0xFF14, 0x91}, // Scroll Lock
{0xFF13, 0x13}, // Pause/Break
{0xFF63, 0x2D}, // Insert
{0xFFFF, 0x2E}, // Delete
{0xFF50, 0x24}, // Home
{0xFF57, 0x23}, // End
{0xFF55, 0x21}, // Page Up
{0xFF56, 0x22}, // Page Down
// arrow keys
{0xFF51, 0x25}, // Left Arrow
{0xFF53, 0x27}, // Right Arrow
{0xFF52, 0x26}, // Up Arrow
{0xFF54, 0x28}, // Down Arrow
// numpad
{0xFFB0, 0x60}, // Numpad 0
{0xFFB1, 0x61}, // Numpad 1
{0xFFB2, 0x62}, // Numpad 2
{0xFFB3, 0x63}, // Numpad 3
{0xFFB4, 0x64}, // Numpad 4
{0xFFB5, 0x65}, // Numpad 5
{0xFFB6, 0x66}, // Numpad 6
{0xFFB7, 0x67}, // Numpad 7
{0xFFB8, 0x68}, // Numpad 8
{0xFFB9, 0x69}, // Numpad 9
{0xFF7F, 0x90}, // Num Lock
{0xFFAE, 0x6E}, // Numpad .
{0xFFAF, 0x6F}, // Numpad /
{0xFFAA, 0x6A}, // Numpad *
{0xFFAD, 0x6D}, // Numpad -
{0xFFAB, 0x6B}, // Numpad +
// symbol keys
{0x003B, 0xBA}, // ; (Semicolon)
{0x0027, 0xDE}, // ' (Quote)
{0x007E, 0xC0}, // ` (Grave)
{0x002C, 0xBC}, // , (Comma)
{0x002E, 0xBE}, // . (Period)
{0x002F, 0xBF}, // / (Slash)
{0x005C, 0xDC}, // \ (Backslash)
{0x005B, 0xDB}, // [ (Left Bracket)
{0x005D, 0xDD}, // ] (Right Bracket)
{0x002D, 0xBD}, // - (Minus)
{0x003D, 0xBB}, // = (Equals)
// modifier keys
{0xFFE5, 0x14}, // Caps Lock
{0xFFE1, 0xA0}, // Shift (Left)
{0xFFE2, 0xA1}, // Shift (Right)
{0xFFE3, 0xA2}, // Ctrl (Left)
{0xFFE4, 0xA3}, // Ctrl (Right)
{0xFFE9, 0xA4}, // Alt (Left)
{0xFFEA, 0xA5}, // Alt (Right)
{0xFFEB, 0x5B}, // Left Command (Windows key)
{0xFFEC, 0x5C}, // Right Command
};
// macOS CGKeyCode to X11 KeySym
std::map<int, int> cgKeyCodeToX11KeySym = {
// A-Z
{0x00, 0x0041}, // A
{0x0B, 0x0042}, // B
{0x08, 0x0043}, // C
{0x02, 0x0044}, // D
{0x0E, 0x0045}, // E
{0x03, 0x0046}, // F
{0x05, 0x0047}, // G
{0x04, 0x0048}, // H
{0x22, 0x0049}, // I
{0x26, 0x004A}, // J
{0x28, 0x004B}, // K
{0x25, 0x004C}, // L
{0x2E, 0x004D}, // M
{0x2D, 0x004E}, // N
{0x1F, 0x004F}, // O
{0x23, 0x0050}, // P
{0x0C, 0x0051}, // Q
{0x0F, 0x0052}, // R
{0x01, 0x0053}, // S
{0x11, 0x0054}, // T
{0x20, 0x0055}, // U
{0x09, 0x0056}, // V
{0x0D, 0x0057}, // W
{0x07, 0x0058}, // X
{0x10, 0x0059}, // Y
{0x06, 0x005A}, // Z
// 0-9
{0x1D, 0x0030}, // 0
{0x12, 0x0031}, // 1
{0x13, 0x0032}, // 2
{0x14, 0x0033}, // 3
{0x15, 0x0034}, // 4
{0x17, 0x0035}, // 5
{0x16, 0x0036}, // 6
{0x1A, 0x0037}, // 7
{0x1C, 0x0038}, // 8
{0x19, 0x0039}, // 9
// F1-F12
{0x7A, 0xFFBE}, // F1
{0x78, 0xFFBF}, // F2
{0x63, 0xFFC0}, // F3
{0x76, 0xFFC1}, // F4
{0x60, 0xFFC2}, // F5
{0x61, 0xFFC3}, // F6
{0x62, 0xFFC4}, // F7
{0x64, 0xFFC5}, // F8
{0x65, 0xFFC6}, // F9
{0x6D, 0xFFC7}, // F10
{0x67, 0xFFC8}, // F11
{0x6F, 0xFFC9}, // F12
// control keys
{0x35, 0xFF1B}, // Escape
{0x24, 0xFF0D}, // Enter
{0x31, 0x0020}, // Space
{0x33, 0xFF08}, // Backspace
{0x30, 0xFF09}, // Tab
{0x69, 0xFF15}, // Print Screen(F13)
{0x72, 0xFF63}, // Insert
{0x75, 0xFFFF}, // Delete
{0x73, 0xFF50}, // Home
{0x77, 0xFF57}, // End
{0x74, 0xFF55}, // Page Up
{0x79, 0xFF56}, // Page Down
// arrow keys
{0x7B, 0xFF51}, // Left Arrow
{0x7C, 0xFF53}, // Right Arrow
{0x7E, 0xFF52}, // Up Arrow
{0x7D, 0xFF54}, // Down Arrow
// numpad
{0x52, 0xFFB0}, // Numpad 0
{0x53, 0xFFB1}, // Numpad 1
{0x54, 0xFFB2}, // Numpad 2
{0x55, 0xFFB3}, // Numpad 3
{0x56, 0xFFB4}, // Numpad 4
{0x57, 0xFFB5}, // Numpad 5
{0x58, 0xFFB6}, // Numpad 6
{0x59, 0xFFB7}, // Numpad 7
{0x5B, 0xFFB8}, // Numpad 8
{0x5C, 0xFFB9}, // Numpad 9
{0x47, 0xFF7F}, // Num Lock / Keypad Clear
{0x41, 0xFFAE}, // Numpad .
{0x4B, 0xFFAF}, // Numpad /
{0x43, 0xFFAA}, // Numpad *
{0x4E, 0xFFAD}, // Numpad -
{0x45, 0xFFAB}, // Numpad +
// symbol keys
{0x29, 0x003B}, // ; (Semicolon)
{0x27, 0x0027}, // ' (Quote)
{0x32, 0x007E}, // ` (Backtick)
{0x2B, 0x002C}, // , (Comma)
{0x2F, 0x002E}, // . (Period)
{0x2C, 0x002F}, // / (Slash)
{0x2A, 0x005C}, // \ (Backslash)
{0x21, 0x005B}, // [ (Left Bracket)
{0x1E, 0x005D}, // ] (Right Bracket)
{0x1B, 0x002D}, // - (Minus)
{0x18, 0x003D}, // = (Equals)
// modifier keys
{0x39, 0xFFE5}, // Caps Lock
{0x38, 0xFFE1}, // Shift (Left)
{0x3C, 0xFFE2}, // Shift (Right)
{0x3B, 0xFFE3}, // Control (Left)
{0x3E, 0xFFE4}, // Control (Right)
{0x3A, 0xFFE9}, // Alt (Left)
{0x3D, 0xFFEA}, // Alt (Right)
{0x37, 0xFFEB}, // Left Command (Windows key)
{0x36, 0xFFEC}, // Right Command
};
// X11 KeySym to macOS CGKeyCode
// std::map<int, int> x11KeySymToCgKeyCode = []() {
// std::map<int, int> result;
// for (const auto& pair : cgKeyCodeToX11KeySym) {
// result[pair.second] = pair.first;
// }
// return result;
// }();
std::map<int, int> x11KeySymToCgKeyCode = {
// A-Z
{0x0041, 0x00}, // A
{0x0042, 0x0B}, // B
{0x0043, 0x08}, // C
{0x0044, 0x02}, // D
{0x0045, 0x0E}, // E
{0x0046, 0x03}, // F
{0x0047, 0x05}, // G
{0x0048, 0x04}, // H
{0x0049, 0x22}, // I
{0x004A, 0x26}, // J
{0x004B, 0x28}, // K
{0x004C, 0x25}, // L
{0x004D, 0x2E}, // M
{0x004E, 0x2D}, // N
{0x004F, 0x1F}, // O
{0x0050, 0x23}, // P
{0x0051, 0x0C}, // Q
{0x0052, 0x0F}, // R
{0x0053, 0x01}, // S
{0x0054, 0x11}, // T
{0x0055, 0x20}, // U
{0x0056, 0x09}, // V
{0x0057, 0x0D}, // W
{0x0058, 0x07}, // X
{0x0059, 0x10}, // Y
{0x005A, 0x06}, // Z
// 0-9
{0x0030, 0x1D}, // 0
{0x0031, 0x12}, // 1
{0x0032, 0x13}, // 2
{0x0033, 0x14}, // 3
{0x0034, 0x15}, // 4
{0x0035, 0x17}, // 5
{0x0036, 0x16}, // 6
{0x0037, 0x1A}, // 7
{0x0038, 0x1C}, // 8
{0x0039, 0x19}, // 9
// F1-F12
{0xFFBE, 0x7A}, // F1
{0xFFBF, 0x78}, // F2
{0xFFC0, 0x63}, // F3
{0xFFC1, 0x76}, // F4
{0xFFC2, 0x60}, // F5
{0xFFC3, 0x61}, // F6
{0xFFC4, 0x62}, // F7
{0xFFC5, 0x64}, // F8
{0xFFC6, 0x65}, // F9
{0xFFC7, 0x6D}, // F10
{0xFFC8, 0x67}, // F11
{0xFFC9, 0x6F}, // F12
// control keys
{0xFF1B, 0x35}, // Escape
{0xFF0D, 0x24}, // Enter
{0x0020, 0x31}, // Space
{0xFF08, 0x33}, // Backspace
{0xFF09, 0x30}, // Tab
{0xFF15, 0x69}, // Print Screen(F13)
{0xFF63, 0x72}, // Insert
{0xFFFF, 0x75}, // Delete
{0xFF50, 0x73}, // Home
{0xFF57, 0x77}, // End
{0xFF55, 0x74}, // Page Up
{0xFF56, 0x79}, // Page Down
// arrow keys
{0xFF51, 0x7B}, // Left Arrow
{0xFF53, 0x7C}, // Right Arrow
{0xFF52, 0x7E}, // Up Arrow
{0xFF54, 0x7D}, // Down Arrow
// numpad
{0xFFB0, 0x52}, // Numpad 0
{0xFFB1, 0x53}, // Numpad 1
{0xFFB2, 0x54}, // Numpad 2
{0xFFB3, 0x55}, // Numpad 3
{0xFFB4, 0x56}, // Numpad 4
{0xFFB5, 0x57}, // Numpad 5
{0xFFB6, 0x58}, // Numpad 6
{0xFFB7, 0x59}, // Numpad 7
{0xFFB8, 0x5B}, // Numpad 8
{0xFFB9, 0x5C}, // Numpad 9
{0xFF7F, 0x47}, // Num Lock / Keypad Clear
{0xFFAE, 0x41}, // Numpad .
{0xFFAF, 0x4B}, // Numpad /
{0xFFAA, 0x43}, // Numpad *
{0xFFAD, 0x4E}, // Numpad -
{0xFFAB, 0x45}, // Numpad +
// symbol keys
{0x003B, 0x29}, // ; (Semicolon)
{0x0027, 0x27}, // ' (Quote)
{0x007E, 0x32}, // ` (Backtick)
{0x002C, 0x2B}, // , (Comma)
{0x002E, 0x2F}, // . (Period)
{0x002F, 0x2C}, // / (Slash)
{0x005C, 0x2A}, // \ (Backslash)
{0x005B, 0x21}, // [ (Left Bracket)
{0x005D, 0x1E}, // ] (Right Bracket)
{0x002D, 0x1B}, // - (Minus)
{0x003D, 0x18}, // = (Equals)
// modifier keys
{0xFFE5, 0x39}, // Caps Lock
{0xFFE1, 0x38}, // Shift (Left)
{0xFFE2, 0x3C}, // Shift (Right)
{0xFFE3, 0x3B}, // Control (Left)
{0xFFE4, 0x3E}, // Control (Right)
{0xFFE9, 0x3A}, // Alt (Left)
{0xFFEA, 0x3D}, // Alt (Right)
{0xFFEB, 0x37}, // Left Command
{0xFFEC, 0x36}, // Right Command
};
} // namespace crossdesk
#endif
@@ -0,0 +1,195 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
namespace crossdesk::gui_detail {
struct MainWindowState {
float title_bar_width_ = 640;
float title_bar_height_ = 30;
float title_bar_button_width_ = 30;
float title_bar_button_height_ = 30;
bool exit_ = false;
const int sdl_refresh_ms_ = 16;
bool main_window_minimized_ = false;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
float main_window_dpi_scaling_h_ = 1.0f;
float dpi_scale_ = 1.0f;
float main_window_width_default_ = 640;
float main_window_height_default_ = 480;
float main_window_width_ = 640;
float main_window_height_ = 480;
float main_window_width_last_ = 640;
float main_window_height_last_ = 480;
float local_window_width_ = 320;
float local_window_height_ = 235;
float remote_window_width_ = 320;
float remote_window_height_ = 235;
float local_child_window_width_ = 266;
float local_child_window_height_ = 180;
float remote_child_window_width_ = 266;
float remote_child_window_height_ = 180;
float main_window_text_y_padding_ = 10;
float main_child_window_x_padding_ = 27;
float main_child_window_y_padding_ = 45;
float status_bar_height_ = 22;
float connection_status_window_width_ = 200;
float connection_status_window_height_ = 150;
float notification_window_width_ = 200;
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
};
// Application-global interaction flags that coordinate SDL events with media
// and remote-control devices.
struct InteractionState {
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool audio_capture_ = false;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
std::string connect_button_label_ = "Connect";
char input_password_tmp_[7] = "";
char input_password_[7] = "";
std::string random_password_;
char new_password_[7] = "";
char remote_id_display_[12] = "";
unsigned char audio_buffer_[720]{};
int audio_len_ = 0;
bool audio_buffer_fresh_ = false;
bool need_to_rejoin_ = false;
std::chrono::steady_clock::time_point last_rejoin_check_time_ =
std::chrono::steady_clock::now();
bool just_created_ = false;
std::string controlled_remote_id_;
std::string focused_remote_id_;
std::string remote_client_id_;
};
struct UpdateState {
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_;
std::string release_name_;
std::string release_notes_;
std::string release_date_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0;
};
struct StreamWindowState {
bool need_to_create_stream_window_ = false;
bool stream_window_created_ = false;
bool stream_window_inited_ = false;
bool window_maximized_ = false;
bool control_mouse_ = false;
int stream_window_width_default_ = 1280;
int stream_window_height_default_ = 720;
float stream_window_width_ = 1280;
float stream_window_height_ = 720;
int stream_window_width_real_ = 1280;
int stream_window_height_real_ = 720;
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
};
struct ServerWindowState {
// Transport callbacks run outside the UI thread. Keep the two lifecycle
// requests atomic so a disconnect cannot be lost while the Slint timer is
// deciding whether to create or destroy the controlled-side window.
std::atomic<bool> need_to_create_server_window_{false};
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
int server_window_height_default_ = 150;
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
bool server_window_collapsed_ = false;
bool server_window_collapsed_dragging_ = false;
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
int server_window_collapsed_drag_start_win_x_ = 0;
int server_window_collapsed_drag_start_win_y_ = 0;
bool server_window_dragging_ = false;
float server_window_drag_start_mouse_x_ = 0.0f;
float server_window_drag_start_mouse_y_ = 0.0f;
int server_window_drag_start_win_x_ = 0;
int server_window_drag_start_win_y_ = 0;
};
struct UiState {
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
uint32_t password_validating_time_ = 0;
bool show_settings_window_ = false;
bool show_self_hosted_server_config_window_ = false;
bool rejoin_ = false;
bool local_id_copied_ = false;
bool show_password_ = true;
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool show_update_notification_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_edit_connection_alias_window_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_;
std::string edit_connection_alias_remote_id_;
char edit_connection_alias_[128] = "";
std::string offline_warning_text_;
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
};
struct ApplicationState : MainWindowState,
InteractionState,
UpdateState,
StreamWindowState,
ServerWindowState,
UiState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
#include <chrono>
#include <memory>
#include <string>
#include <unordered_map>
#include <remote_action.h>
#include "runtime/cursor_state_provider.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
// Slint application shell. GuiRuntime remains the owner of transport, media,
// device, clipboard, transfer and settings behavior.
class GuiApplication final : private GuiRuntime {
public:
GuiApplication();
~GuiApplication();
int Run();
private:
struct SlintUi;
struct CursorDeliveryState {
CursorState last_sent{};
bool has_sent = false;
bool feedback_pending = false;
std::chrono::steady_clock::time_point last_sent_time{};
};
void InitializeLogger();
void InitializeSettings();
bool InitializeSDL();
void InitializeModules();
void InitializeUi();
void InitializeSystemTray();
bool MinimizeMainWindowToTray();
void BindMainCallbacks();
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
void ShareLocalCursorState();
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
void SyncConnectionDialog();
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncStreamVideoFrame();
void ScheduleNextVideoFrame();
void ConfigureStreamVideoRenderer();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
void ResetSettingsUi();
void SaveSettingsFromUi();
#if _WIN32 && CROSSDESK_PORTABLE
void CheckPortableWindowsService();
void StartPortableWindowsServiceInstall();
void JoinPortableWindowsServiceInstallThread();
#endif
void ConnectFromUi(const std::string &remote_id);
void SelectStreamTab(int index);
void ReorderStreamTab(int from, float drop_x, float tab_width);
void CloseStreamTab(const std::string &remote_id);
void SendPointerInput(int button, int kind, float x, float y);
void SendScrollInput(float delta_x, float delta_y, float x, float y);
void SendKeyInput(const std::string &text, bool pressed, bool control,
bool alt, bool shift, bool meta);
void Cleanup();
std::shared_ptr<RemoteSession> SelectedSession();
std::string OpenFileDialog(const std::string &title);
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
CursorStateProvider cursor_state_provider_;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
uint32_t cursor_state_sequence_ = 0;
std::chrono::steady_clock::time_point next_video_frame_time_{};
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
bool use_x11_custom_titlebar_ = false;
#endif
};
} // namespace crossdesk
#endif
@@ -0,0 +1,323 @@
#include "application/gui_application.h"
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "rd_log.h"
namespace crossdesk {
void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Main context is null");
return;
}
if (stream_window_inited_) {
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Stream context is null");
return;
}
}
if (server_window_inited_) {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Server context is null");
return;
}
}
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
LOG_INFO("Quit program from system tray");
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
#if defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->RemoveTrayIcon();
}
#endif
exit_ = true;
return;
}
if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) {
clipboard_.ApplyPendingRemoteText();
return;
}
switch (event.type) {
case SDL_EVENT_QUIT:
if (stream_window_inited_) {
LOG_INFO("Destroy stream window");
SDL_SetWindowMouseGrab(stream_window_, false);
DestroyStreamWindow();
DestroyStreamWindowContext();
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[host_name, props] : remote_sessions_) {
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_)
? "C-" + std::string(client_id_)
: client_id_;
LOG_INFO("[{}] Leave connection [{}]", client_id, host_name);
LeaveConnection(props->peer_, host_name.c_str());
LOG_INFO("Destroy peer [{}]", client_id);
DestroyPeer(&props->peer_);
}
props->streaming_ = false;
props->remember_password_ = false;
props->connection_established_ = false;
props->audio_capture_button_pressed_ = false;
memset(&props->net_traffic_stats_, 0,
sizeof(props->net_traffic_stats_));
SDL_SetWindowFullscreen(main_window_, false);
SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT);
memset(audio_buffer_, 0, 720);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
rejoin_ = false;
is_client_mode_ = false;
reload_recent_connections_ = true;
fullscreen_button_pressed_ = false;
start_keyboard_capturer_ = false;
just_created_ = false;
recent_connection_image_save_time_ = SDL_GetTicks();
} else {
LOG_INFO("Quit program");
exit_ = true;
}
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (stream_window_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_)) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
break;
}
exit_ = true;
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (stream_window_created_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
UpdateRenderRect();
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
focus_on_stream_window_ = true;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = true;
}
break;
case SDL_EVENT_WINDOW_FOCUS_LOST:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
keyboard_.ForceReleasePressedKeys();
focus_on_stream_window_ = false;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = false;
}
break;
case SDL_EVENT_DROP_FILE:
transfers_.HandleDropEvent(event);
break;
case SDL_EVENT_CLIPBOARD_UPDATE:
clipboard_.HandleLocalUpdate();
break;
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL: {
Uint32 mouse_window_id = 0;
if (event.type == SDL_EVENT_MOUSE_MOTION) {
mouse_window_id = event.motion.windowID;
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
mouse_window_id = event.button.windowID;
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
mouse_window_id = event.wheel.windowID;
}
if (focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == mouse_window_id) {
ProcessMouseEvent(event);
}
break;
}
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
}
break;
default:
if (event.type == STREAM_REFRESH_EVENT) {
auto *props = static_cast<RemoteSession *>(event.user.data1);
if (!props) {
break;
}
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 (video_width <= 0 || video_height <= 0) {
break;
}
if (!frame_snapshot || frame_snapshot->empty()) {
break;
}
if (props->stream_texture_) {
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(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
} else {
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_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
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;
}
}
} // namespace crossdesk
@@ -0,0 +1,424 @@
#include "application/gui_application.h"
#include <remote_action.h>
#include <algorithm>
#include <cmath>
#include <string>
#include "device_controller.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) {
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
switch (event.scancode) {
case SDL_SCANCODE_NUMLOCKCLEAR:
return 0x90;
case SDL_SCANCODE_KP_ENTER:
return 0x0D;
case SDL_SCANCODE_KP_0:
if (!numlock_enabled) {
return 0x2D;
}
return 0x60;
case SDL_SCANCODE_KP_1:
if (!numlock_enabled) {
return 0x23;
}
return 0x61;
case SDL_SCANCODE_KP_2:
if (!numlock_enabled) {
return 0x28;
}
return 0x62;
case SDL_SCANCODE_KP_3:
if (!numlock_enabled) {
return 0x22;
}
return 0x63;
case SDL_SCANCODE_KP_4:
if (!numlock_enabled) {
return 0x25;
}
return 0x64;
case SDL_SCANCODE_KP_5:
return 0x65;
case SDL_SCANCODE_KP_6:
if (!numlock_enabled) {
return 0x27;
}
return 0x66;
case SDL_SCANCODE_KP_7:
if (!numlock_enabled) {
return 0x24;
}
return 0x67;
case SDL_SCANCODE_KP_8:
if (!numlock_enabled) {
return 0x26;
}
return 0x68;
case SDL_SCANCODE_KP_9:
if (!numlock_enabled) {
return 0x21;
}
return 0x69;
case SDL_SCANCODE_KP_PERIOD:
case SDL_SCANCODE_KP_COMMA:
if (!numlock_enabled) {
return 0x2E;
}
return 0x6E;
case SDL_SCANCODE_KP_DIVIDE:
return 0x6F;
case SDL_SCANCODE_KP_MULTIPLY:
return 0x6A;
case SDL_SCANCODE_KP_MINUS:
return 0x6D;
case SDL_SCANCODE_KP_PLUS:
return 0x6B;
case SDL_SCANCODE_KP_EQUALS:
return 0xBB;
default:
return -1;
}
}
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) {
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
if (keypad_key_code >= 0) {
return keypad_key_code;
}
const int key = static_cast<int>(event.key);
if (key >= 'a' && key <= 'z') {
return key - 'a' + 0x41;
}
if (key >= 'A' && key <= 'Z') {
return key;
}
if (key >= '0' && key <= '9') {
return key;
}
switch (key) {
case ';':
return 0xBA;
case '\'':
return 0xDE;
case '`':
return 0xC0;
case ',':
return 0xBC;
case '.':
return 0xBE;
case '/':
return 0xBF;
case '\\':
return 0xDC;
case '[':
return 0xDB;
case ']':
return 0xDD;
case '-':
return 0xBD;
case '=':
return 0xBB;
default:
break;
}
switch (event.scancode) {
case SDL_SCANCODE_ESCAPE:
return 0x1B;
case SDL_SCANCODE_RETURN:
return 0x0D;
case SDL_SCANCODE_SPACE:
return 0x20;
case SDL_SCANCODE_BACKSPACE:
return 0x08;
case SDL_SCANCODE_TAB:
return 0x09;
case SDL_SCANCODE_PRINTSCREEN:
return 0x2C;
case SDL_SCANCODE_SCROLLLOCK:
return 0x91;
case SDL_SCANCODE_PAUSE:
return 0x13;
case SDL_SCANCODE_INSERT:
return 0x2D;
case SDL_SCANCODE_DELETE:
return 0x2E;
case SDL_SCANCODE_HOME:
return 0x24;
case SDL_SCANCODE_END:
return 0x23;
case SDL_SCANCODE_PAGEUP:
return 0x21;
case SDL_SCANCODE_PAGEDOWN:
return 0x22;
case SDL_SCANCODE_LEFT:
return 0x25;
case SDL_SCANCODE_RIGHT:
return 0x27;
case SDL_SCANCODE_UP:
return 0x26;
case SDL_SCANCODE_DOWN:
return 0x28;
case SDL_SCANCODE_F1:
return 0x70;
case SDL_SCANCODE_F2:
return 0x71;
case SDL_SCANCODE_F3:
return 0x72;
case SDL_SCANCODE_F4:
return 0x73;
case SDL_SCANCODE_F5:
return 0x74;
case SDL_SCANCODE_F6:
return 0x75;
case SDL_SCANCODE_F7:
return 0x76;
case SDL_SCANCODE_F8:
return 0x77;
case SDL_SCANCODE_F9:
return 0x78;
case SDL_SCANCODE_F10:
return 0x79;
case SDL_SCANCODE_F11:
return 0x7A;
case SDL_SCANCODE_F12:
return 0x7B;
case SDL_SCANCODE_CAPSLOCK:
return 0x14;
case SDL_SCANCODE_LSHIFT:
return 0xA0;
case SDL_SCANCODE_RSHIFT:
return 0xA1;
case SDL_SCANCODE_LCTRL:
return 0xA2;
case SDL_SCANCODE_RCTRL:
return 0xA3;
case SDL_SCANCODE_LALT:
return 0xA4;
case SDL_SCANCODE_RALT:
return 0xA5;
case SDL_SCANCODE_LGUI:
return 0x5B;
case SDL_SCANCODE_RGUI:
return 0x5C;
default:
return -1;
}
}
} // namespace
int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
return -1;
}
// SDL represents key auto-repeat as additional key-down events. Forward
// them so the fallback path has the same long-press behavior as native hooks.
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
}
return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
}
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
controlled_remote_id_ = "";
RemoteAction remote_action{};
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
event.button.button != SDL_BUTTON_LEFT &&
event.button.button != SDL_BUTTON_RIGHT &&
event.button.button != SDL_BUTTON_MIDDLE) {
return 0;
}
float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y;
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
if (!x || !y || !stream_window_) {
return;
}
int window_width = 0;
int window_height = 0;
int pixel_width = 0;
int pixel_height = 0;
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
pixel_height <= 0) {
return;
}
if ((window_width != pixel_width || window_height != pixel_height) &&
(*x > static_cast<float>(window_width) + 1.0f ||
*y > static_cast<float>(window_height) + 1.0f)) {
const float scale_x =
static_cast<float>(window_width) / static_cast<float>(pixel_width);
const float scale_y =
static_cast<float>(window_height) / static_cast<float>(pixel_height);
*x *= scale_x;
*y *= scale_y;
static bool logged_pixel_to_window_conversion = false;
if (!logged_pixel_to_window_conversion) {
LOG_INFO(
"Mouse coordinate space converted from pixels to window units: "
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
window_width, window_height, pixel_width, pixel_height, scale_x,
scale_y);
logged_pixel_to_window_conversion = true;
}
}
};
if (event.type == SDL_EVENT_MOUSE_MOTION) {
cursor_x = event.motion.x;
cursor_y = event.motion.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
cursor_x = event.button.x;
cursor_y = event.button.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
cursor_x = last_mouse_event.motion.x;
cursor_y = last_mouse_event.motion.y;
}
const bool is_pointer_position_event =
(event.type == SDL_EVENT_MOUSE_MOTION ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (!props->control_mouse_) {
continue;
}
const bool file_transfer_window_hovered =
props->file_transfer_.file_transfer_window_hovered_;
const bool overlay_hovered =
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
const SDL_FRect render_rect = props->stream_render_rect_f_;
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
continue;
}
if (is_pointer_position_event && cursor_x >= render_rect.x &&
cursor_x <= render_rect.x + render_rect.w &&
cursor_y >= render_rect.y &&
cursor_y <= render_rect.y + render_rect.h) {
controlled_remote_id_ = it.first;
last_mouse_event.motion.x = cursor_x;
last_mouse_event.motion.y = cursor_y;
last_mouse_event.button.x = cursor_x;
last_mouse_event.button.y = cursor_y;
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_down;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_down;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_down;
}
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_up;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_up;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_up;
}
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
remote_action.type = ControlType::mouse;
remote_action.m.flag = MouseFlag::move;
}
if (overlay_hovered) {
break;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= render_rect.x &&
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
last_mouse_event.button.y >= render_rect.y &&
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = roundUp(scroll_x);
}
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
(std::max)(render_rect.w, 1.0f);
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
(std::max)(render_rect.h, 1.0f);
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (overlay_hovered) {
continue;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
}
}
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,28 @@
#pragma once
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
namespace crossdesk::server_window_state {
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
std::string* selected_id) {
if (!selected_id) {
return 0;
}
if (ids.empty()) {
selected_id->clear();
return 0;
}
const auto selected = std::find(ids.begin(), ids.end(), *selected_id);
if (selected == ids.end()) {
*selected_id = ids.front();
return 0;
}
return static_cast<int>(std::distance(ids.begin(), selected));
}
} // namespace crossdesk::server_window_state
@@ -0,0 +1,40 @@
#pragma once
#include <algorithm>
namespace crossdesk::window_geometry {
struct PhysicalRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
};
struct PhysicalSize {
int width = 0;
int height = 0;
};
struct PhysicalPosition {
int x = 0;
int y = 0;
};
constexpr PhysicalPosition CenteredPosition(const PhysicalRect& bounds,
const PhysicalSize& size) {
return {
bounds.x + (bounds.width - std::min(bounds.width, size.width)) / 2,
bounds.y + (bounds.height - std::min(bounds.height, size.height)) / 2,
};
}
constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
const PhysicalSize& size) {
return {
bounds.x + std::max(0, bounds.width - size.width),
bounds.y + std::max(0, bounds.height - size.height),
};
}
} // namespace crossdesk::window_geometry
@@ -0,0 +1,687 @@
#include "application/gui_application.h"
#include <libyuv.h>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include "fa_regular_400.h"
#include "fa_solid_900.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "version_checker.h"
#if defined(__APPLE__)
#include "window_util_mac.h"
#endif
namespace crossdesk {
namespace {
const ImWchar *GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) {
ImGuiIO &io = ImGui::GetIO();
ImFontGlyphRangesBuilder builder;
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
ImVector<ImWchar> built_ranges;
builder.BuildRanges(&built_ranges);
glyph_ranges.assign(built_ranges.Data,
built_ranges.Data + built_ranges.Size);
}
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
}
bool CanReadFontFile(const char *font_path) {
if (!font_path) {
return false;
}
std::ifstream font_file(font_path, std::ios::binary);
return font_file.good();
}
#if _WIN32
HICON LoadTrayIcon() {
HMODULE module = GetModuleHandleW(nullptr);
HICON icon = reinterpret_cast<HICON>(
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (icon) {
return icon;
}
return LoadIconW(nullptr, IDI_APPLICATION);
}
#endif
#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
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
const SDL_Point *area,
void *data) {
GuiApplication *application = (GuiApplication *)data;
if (!application) {
return SDL_HITTEST_NORMAL;
}
if (application->fullscreen_button_pressed_) {
return SDL_HITTEST_NORMAL;
}
// Server window: OS-level dragging for the title bar, but keep the left-side
// collapse/expand button clickable.
if (application->server_window_ && window == application->server_window_) {
const float title_h = application->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);
// check if curosor is in tab bar
if (application->stream_window_inited_ && application->stream_window_created_ &&
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
ImGui::SetCurrentContext(application->stream_ctx_);
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
if (tab_bar_window && tab_bar_window->Active) {
ImGuiIO &io = ImGui::GetIO();
float scale_x = io.DisplayFramebufferScale.x;
float scale_y = io.DisplayFramebufferScale.y;
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
float tab_bar_width = tab_bar_window->Size.x * scale_x;
float tab_bar_height = tab_bar_window->Size.y * scale_y;
ImGui::SetCurrentContext(prev_ctx);
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
return SDL_HITTEST_NORMAL;
}
} else {
ImGui::SetCurrentContext(prev_ctx);
}
}
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
if (area->y < application->title_bar_button_width_ &&
area->y > mouse_grab_padding &&
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
area->x > mouse_grab_padding) {
return SDL_HITTEST_DRAGGABLE;
}
// if (!application->streaming_) {
// return SDL_HITTEST_NORMAL;
// }
if (area->y < mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPRIGHT;
} else {
return SDL_HITTEST_RESIZE_TOP;
}
} else if (area->y > window_height - mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
} else {
return SDL_HITTEST_RESIZE_BOTTOM;
}
} else if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_LEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_RIGHT;
}
return SDL_HITTEST_NORMAL;
}
int GuiApplication::CreateMainWindow() {
main_ctx_ = ImGui::CreateContext();
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Main Window", (int)main_window_width_,
(int)main_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
&main_window_, &main_renderer_)) {
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
return -1;
}
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
dpi_scale_ = dpi_scale;
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
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_);
window_rounding_ = window_rounding_default_ * dpi_scale_;
SDL_SetWindowSize(main_window_, (int)main_window_width_,
(int)main_window_height_);
}
SDL_SetWindowResizable(main_window_, false);
// for window region action
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&main_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
#if _WIN32
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#elif defined(__linux__) && !defined(__APPLE__)
tray_ = std::make_unique<LinuxTray>(
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
#endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
ImGui_ImplSDLRenderer3_Init(main_renderer_);
return 0;
}
int GuiApplication::DestroyMainWindow() {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
}
if (main_renderer_) {
SDL_DestroyRenderer(main_renderer_);
}
if (main_window_) {
SDL_DestroyWindow(main_window_);
}
return 0;
}
int GuiApplication::CreateStreamWindow() {
if (stream_window_created_) {
return 0;
}
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
stream_ctx_ = ImGui::CreateContext();
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Stream Window", (int)stream_window_width_,
(int)stream_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&stream_window_, &stream_renderer_)) {
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
SDL_GetError());
return -1;
}
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
SDL_SetWindowResizable(stream_window_, true);
// for window region action
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&stream_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
// change props->stream_render_rect_
SDL_Event event;
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
event.window.windowID = SDL_GetWindowID(stream_window_);
SDL_PushEvent(&event);
stream_window_created_ = true;
just_created_ = true;
stream_window_inited_ = true;
LOG_INFO("Stream window inited");
return 0;
}
int GuiApplication::DestroyStreamWindow() {
stream_window_width_ = (float)stream_window_width_default_;
stream_window_height_ = (float)stream_window_height_default_;
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
}
if (stream_renderer_) {
SDL_DestroyRenderer(stream_renderer_);
stream_renderer_ = nullptr;
}
if (stream_window_) {
SDL_DestroyWindow(stream_window_);
stream_window_ = nullptr;
}
stream_window_created_ = false;
focus_on_stream_window_ = false;
stream_window_grabbed_ = false;
control_mouse_ = false;
return 0;
}
int GuiApplication::CreateServerWindow() {
if (server_window_created_) {
return 0;
}
server_ctx_ = ImGui::CreateContext();
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Server Window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
SDL_GetError());
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_),
&display_bounds)) {
int window_x =
display_bounds.x + display_bounds.w - (int)server_window_width_;
int window_y =
display_bounds.y + display_bounds.h - (int)server_window_height_;
SDL_SetWindowPosition(server_window_, window_x, window_y);
}
SDL_SetWindowResizable(server_window_, false);
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
// for window region action
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
SetupFontAndStyle(&server_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
ImGui_ImplSDLRenderer3_Init(server_renderer_);
server_window_created_ = true;
server_window_inited_ = true;
LOG_INFO("Server window inited");
return 0;
}
int GuiApplication::DestroyServerWindow() {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
}
if (server_renderer_) {
SDL_DestroyRenderer(server_renderer_);
server_renderer_ = nullptr;
}
if (server_window_) {
SDL_DestroyWindow(server_window_);
server_window_ = nullptr;
}
server_window_created_ = false;
server_window_inited_ = false;
return 0;
}
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
float font_size = 32.0f;
// Setup Dear ImGui style
ImGuiIO &io = ImGui::GetIO();
io.IniFilename = NULL; // disable imgui.ini
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = false;
if (system_chinese_font_out) {
*system_chinese_font_out = nullptr;
}
ImFont *ui_font = nullptr;
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
#if defined(_WIN32)
const char *base_font_paths[] = {
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/simsun.ttc", nullptr};
#elif defined(__APPLE__)
const char *base_font_paths[] = {
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/System/Library/Fonts/SFNS.ttf", nullptr};
#else
const char *base_font_paths[] = {
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
nullptr};
#endif
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
if (!CanReadFontFile(base_font_paths[i])) {
continue;
}
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
&config, multilingual_ranges);
if (ui_font != nullptr) {
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
}
}
if (!ui_font) {
ui_font = io.Fonts->AddFontDefault(&config);
}
if (!ui_font) {
LOG_WARN("Failed to initialize base UI font");
ImGui::StyleColorsLight();
return 0;
}
ImFontConfig icon_config = config;
icon_config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &icon_config, icon_ranges);
io.FontDefault = ui_font;
if (system_chinese_font_out) {
*system_chinese_font_out = ui_font;
}
ImGui::StyleColorsLight();
return 0;
}
int GuiApplication::DestroyMainWindowContext() {
if (!main_ctx_) {
return 0;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(main_ctx_);
main_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyStreamWindowContext() {
if (!stream_ctx_) {
stream_window_inited_ = false;
return 0;
}
stream_window_inited_ = false;
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(stream_ctx_);
stream_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyServerWindowContext() {
if (!server_ctx_) {
server_window_inited_ = false;
return 0;
}
server_window_inited_ = false;
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(server_ctx_);
server_ctx_ = nullptr;
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,236 @@
#include "application/gui_application.h"
#include <cmath>
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::DrawMainWindow() {
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
ImGuiCond_Always);
ImGui::Begin("MainRender", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
TitleBar(true);
MainWindow();
UpdateNotificationWindow();
#if _WIN32 && CROSSDESK_PORTABLE
PortableServiceInstallWindow();
#endif
#ifdef __APPLE__
if (show_request_permission_window_) {
RequestPermissionWindow();
}
#endif
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
SDL_RenderClear(main_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
SDL_RenderPresent(main_renderer_);
return 0;
}
int GuiApplication::DrawStreamWindow() {
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
StreamWindow();
ImGuiIO &io = ImGui::GetIO();
float stream_title_window_height =
fullscreen_button_pressed_ ? 0 : title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
// Set minimum window size to 0 to allow exact height control
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
ImGuiCond_Always);
ImGui::Begin("StreamTitleWindow", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!fullscreen_button_pressed_) {
TitleBar(false);
}
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
SDL_RenderClear(stream_renderer_);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (props->tab_selected_) {
SDL_FRect render_rect_f = {
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
&render_rect_f);
}
}
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
SDL_RenderPresent(stream_renderer_);
return 0;
}
int GuiApplication::DrawServerWindow() {
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
server_window_width_ = io.DisplaySize.x;
server_window_height_ = io.DisplaySize.y;
ServerWindow();
ImGui::Render();
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
SDL_RenderClear(server_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
SDL_RenderPresent(server_renderer_);
return 0;
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#elif defined(__linux__) && !defined(__APPLE__)
if (!tray_) {
return false;
}
return tray_->MinimizeToTray();
#else
return false;
#endif
}
void GuiApplication::UpdateRenderRect() {
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[_, props] : remote_sessions_) {
if (!props->reset_control_bar_pos_) {
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
if (!just_created_) {
props->reset_control_bar_pos_ = true;
}
int stream_window_width, stream_window_height;
SDL_GetWindowSize(stream_window_, &stream_window_width,
&stream_window_height);
stream_window_width_ = (float)stream_window_width;
stream_window_height_ = (float)stream_window_height;
float video_ratio =
(float)props->video_width_ / (float)props->video_height_;
float video_ratio_reverse =
(float)props->video_height_ / (float)props->video_width_;
float render_area_width = props->render_window_width_;
float render_area_height = props->render_window_height_;
props->stream_render_rect_last_ = props->stream_render_rect_;
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
render_area_width, render_area_height};
if (render_area_width < render_area_height * video_ratio) {
rect_f.x = props->render_window_x_;
rect_f.y = std::abs(render_area_height -
render_area_width * video_ratio_reverse) /
2.0f +
props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_width * video_ratio_reverse;
} else if (render_area_width > render_area_height * video_ratio) {
rect_f.x =
std::abs(render_area_width - render_area_height * video_ratio) /
2.0f +
props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_height * video_ratio;
rect_f.h = render_area_height;
} else {
rect_f.x = props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_height;
}
props->stream_render_rect_f_ = rect_f;
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
static_cast<int>(std::lround(rect_f.y)),
static_cast<int>(std::lround(rect_f.w)),
static_cast<int>(std::lround(rect_f.h))};
}
}
} // namespace crossdesk
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,85 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
#define LOCAL_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define REMOTE_WINDOW_WIDTH_CN 300 * dpi_scale_
#define REMOTE_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define MENU_WINDOW_WIDTH_EN 190 * dpi_scale_
#define MENU_WINDOW_HEIGHT_EN 245 * dpi_scale_
#define IPUT_WINDOW_WIDTH 160 * dpi_scale_
#define INPUT_WINDOW_PADDING_CN 66 * dpi_scale_
#define INPUT_WINDOW_PADDING_EN 96 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_CN 202 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_EN 248 * dpi_scale_
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 405 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 345 * dpi_scale_
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_EN 218 * dpi_scale_
#define ENABLE_DAEMON_PADDING_CN 171 * dpi_scale_
#define ENABLE_DAEMON_PADDING_EN 218 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SETTINGS_SELECT_WINDOW_WIDTH 73 * dpi_scale_
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_CN 65 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_EN 83 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 * dpi_scale_
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_
#endif
@@ -0,0 +1,89 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define TITLE_BAR_HEIGHT 0.0625f
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
#define STATUS_BAR_HEIGHT 0.05f
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210
#endif
@@ -0,0 +1,194 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
#include "localization_data.h"
#if _WIN32
#include <Windows.h>
#endif
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
namespace detail {
inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
}
using TranslationTable =
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>;
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
{"en-US", reinterpret_cast<const char*>(row.en)},
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
}
inline TranslationTable BuildTranslationTable() {
TranslationTable table;
for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
return table;
}
inline const TranslationTable& GetTranslationTable() {
static const TranslationTable table = BuildTranslationTable();
return table;
}
inline const std::string& GetTranslatedText(const std::string& key,
int language_index) {
static const std::string kEmptyText = "";
const auto& table = GetTranslationTable();
const auto key_it = table.find(key);
if (key_it == table.end()) {
return kEmptyText;
}
const auto& localized_values = key_it->second;
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
const auto exact_it = localized_values.find(language_code);
if (exact_it != localized_values.end()) {
return exact_it->second;
}
const auto english_it = localized_values.find("en-US");
if (english_it != localized_values.end()) {
return english_it->second;
}
const auto chinese_it = localized_values.find("zh-CN");
if (chinese_it != localized_values.end()) {
return chinese_it->second;
}
return kEmptyText;
}
} // namespace detail
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
inline std::string FormatDisplayLabel(size_t display_index,
const std::string& display_name,
int language_index) {
const std::string number = std::to_string(display_index + 1);
std::string label =
detail::GetTranslatedText("display_screen", language_index) + " " +
number;
const std::string fallback_name = "Display" + number;
if (!display_name.empty() && display_name != fallback_name) {
label += " (" + display_name + ")";
}
return label;
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetWindowsLocalizedLabel(
int language_index, const char* key, const wchar_t* fallback,
std::vector<std::wstring>& cache) {
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText(key, normalized_index);
if (utf8_text.empty()) {
cached_text = fallback;
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length));
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
cached_text.data(), wide_length) <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.pop_back();
return cached_text.c_str();
}
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "show_main_window",
L"Show Main Window", cache);
}
inline const wchar_t* GetSettingsLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
cache);
}
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
cache);
}
#endif
} // namespace localization
} // namespace crossdesk
#endif
@@ -0,0 +1,249 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_DATA_H_
#define _LOCALIZATION_DATA_H_
namespace crossdesk {
namespace localization {
namespace detail {
struct TranslationRow {
const char* key;
const void* zh;
const void* en;
const void* ru;
};
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(display_screen, u8"显示屏", "Display", u8"Экран") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
u8"Свернуть панель управления") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
X(remote_service_unavailable, u8"远端Windows服务不可用", \
"Remote Windows service unavailable", \
u8"Служба Windows на удаленной стороне недоступна") \
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
X(windows_service_setup_message, \
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
u8"Service。\n未安装该服务不影响 CrossDesk " \
u8"正常使用,仅无法在锁屏状态下控制本机。", \
"To support remote control of this device while it is locked, CrossDesk " \
"Service must be installed with administrator permission.\nWithout this " \
"service, CrossDesk still works normally; only lock-screen control of " \
"this computer is unavailable.", \
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
u8"блокировки, необходимо установить CrossDesk Service с правами " \
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
u8"нормально; будет недоступно только управление этим компьютером на " \
u8"экране блокировки.") \
X(install_windows_service, u8"安装", "Install", u8"Установить") \
X(windows_service_settings_label, u8"锁屏控制服务:", \
"Lock Screen Service:", u8"Служба блокировки экрана:") \
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
u8"Больше не напоминать") \
X(windows_service_prompt_suppressed_message, \
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
"You will not be reminded again. To enable remote control while locked " \
"later, click Install in Settings.", \
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
u8"экране блокировки, нажмите «Установить» в настройках.") \
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
u8"Установка службы...") \
X(windows_service_install_success, u8"服务已安装并启动", \
"Service installed and started", u8"Служба установлена и запущена") \
X(windows_service_install_failed, \
u8"服务安装失败。请确认 " \
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
"Service installation failed. Make sure CrossDesk.exe, " \
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
"portable folder, then approve the administrator prompt.", \
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
u8"одной папке портативной версии, затем подтвердите запрос прав " \
u8"администратора.") \
X(remote_unlock_requires_secure_desktop, \
u8"当前仍需要安全桌面专用采集/输入", \
"Secure desktop capture/input is still required", \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
u8"стола") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"高", "High", u8"Высокое") \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
X(video_quality_low, u8"低", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
"Certificate verification failed. Reinstall the self-hosted root " \
"certificate.", \
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
u8"Сбор кандидатов...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(connection_alias, u8"修改名称", "Edit Alias", \
u8"Изменить имя подключения") \
X(delete_connection, u8"删除连接", "Delete Connection", \
u8"Удалить подключение") \
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
u8"Подключиться") \
X(input_connection_alias, u8"请输入连接名称:", \
"Please input connection name:", u8"Введите имя подключения:") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件发送", "Select File to Send", \
u8"Выбрать файл для отправки") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(show_main_window, u8"显示主界面", "Show Main Window", \
u8"Показать главное окно") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
};
} // namespace detail
} // namespace localization
} // namespace crossdesk
#endif
@@ -0,0 +1,172 @@
#include "features/clipboard/clipboard_controller.h"
#include <cstring>
#include <shared_mutex>
#include <utility>
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
// Keep a reliable clipboard message within MiniRTC/KCP's single-message
// fragmentation window (MTU is configured to 1200 bytes).
constexpr size_t kMaxClipboardTextBytes = 128 * 1024;
} // namespace
ClipboardController::ClipboardController(GuiRuntime &owner) : owner_(owner) {}
void ClipboardController::SetEventType(uint32_t event_type) {
event_type_ = event_type;
}
uint32_t ClipboardController::event_type() const { return event_type_; }
void ClipboardController::Initialize() {
last_text_.clear();
char *clipboard_text = SDL_GetClipboardText();
if (clipboard_text) {
last_text_.assign(clipboard_text);
SDL_free(clipboard_text);
}
events_enabled_.store(true, std::memory_order_release);
}
void ClipboardController::Shutdown() {
events_enabled_.store(false, std::memory_order_release);
std::lock_guard<std::mutex> lock(pending_mutex_);
pending_remote_text_.reset();
}
void ClipboardController::QueueRemoteText(const char *data, size_t size) {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
if (!data || size == 0) {
return;
}
if (size > kMaxClipboardTextBytes) {
LOG_WARN("Ignore oversized remote clipboard text: {} bytes", size);
return;
}
if (std::memchr(data, '\0', size) != nullptr) {
LOG_WARN("Ignore remote clipboard text containing an embedded NUL byte");
return;
}
{
std::lock_guard<std::mutex> lock(pending_mutex_);
if (!events_enabled_.load(std::memory_order_relaxed)) {
return;
}
// Only the newest clipboard value matters. Replacing it also bounds the
// amount of memory a busy or malicious peer can queue.
pending_remote_text_ = std::string(data, size);
}
SDL_Event event{};
event.type = event_type_;
if (event_type_ != 0 && !SDL_PushEvent(&event)) {
// MainLoop also drains the pending value after its wait timeout.
LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}",
SDL_GetError());
}
}
void ClipboardController::ApplyPendingRemoteText() {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
std::optional<std::string> pending_text;
{
std::lock_guard<std::mutex> lock(pending_mutex_);
pending_text.swap(pending_remote_text_);
}
if (!pending_text || *pending_text == last_text_) {
return;
}
// SDL clipboard functions must run on the thread that initialized SDL.
if (!SDL_SetClipboardText(pending_text->c_str())) {
LOG_ERROR("Failed to set remote clipboard text: {}", SDL_GetError());
return;
}
// SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update
// the baseline first so that event cannot echo the text back to the sender.
last_text_ = std::move(*pending_text);
}
void ClipboardController::HandleLocalUpdate() {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
if (!SDL_HasClipboardText()) {
last_text_.clear();
return;
}
char *clipboard_text = SDL_GetClipboardText();
if (!clipboard_text) {
LOG_WARN("Failed to read local clipboard text: {}", SDL_GetError());
return;
}
std::string text(clipboard_text);
SDL_free(clipboard_text);
if (text == last_text_) {
return;
}
// Record the value before sending. Duplicate SDL notifications and a
// remote write of the same value must not create a clipboard feedback loop.
last_text_ = text;
if (text.empty()) {
return;
}
if (text.size() > kMaxClipboardTextBytes) {
LOG_WARN("Ignore oversized local clipboard text: {} bytes", text.size());
return;
}
SendToPeers(text);
}
int ClipboardController::SendToPeers(const std::string &text) {
std::shared_lock lock(owner_.remote_sessions_mutex_);
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
if (!props || !props->peer_ || !props->connection_established_ ||
!props->enable_mouse_control_) {
continue;
}
const int ret =
SendReliableDataFrame(props->peer_, text.data(), text.size(),
props->clipboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
remote_id.c_str(), ret);
return ret;
}
}
if (owner_.peer_) {
const int ret =
SendReliableDataFrame(owner_.peer_, text.data(), text.size(),
owner_.clipboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
owner_.remote_id_display_, ret);
return ret;
}
}
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,45 @@
#ifndef CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#define CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#include <SDL3/SDL.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
namespace crossdesk {
class GuiRuntime;
// Owns clipboard synchronization state and keeps feedback-loop prevention out
// of the main UI coordinator.
class ClipboardController {
public:
explicit ClipboardController(GuiRuntime &owner);
void SetEventType(uint32_t event_type);
uint32_t event_type() const;
void Initialize();
void Shutdown();
void QueueRemoteText(const char *data, size_t size);
void ApplyPendingRemoteText();
void HandleLocalUpdate();
private:
int SendToPeers(const std::string &text);
GuiRuntime &owner_;
uint32_t event_type_ = 0;
std::atomic<bool> events_enabled_{false};
std::mutex pending_mutex_;
std::optional<std::string> pending_remote_text_;
std::string last_text_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
@@ -0,0 +1,584 @@
#include "features/devices/session_device_manager.h"
#include <remote_action.h>
#include <chrono>
#include <cstdlib>
#include <display_stream_id.h>
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
constexpr size_t kMaxCapturedKeyboardInputs = 512;
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
} // namespace
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
bool SessionDeviceManager::ShouldSendCapturedFrame(
std::chrono::steady_clock::time_point now, int fps) {
const auto interval =
std::chrono::nanoseconds(std::chrono::seconds(1)) / (fps > 0 ? fps : 1);
if (next_frame_deadline_ == std::chrono::steady_clock::time_point{}) {
next_frame_deadline_ = now;
}
if (now + kFrameDeadlineTolerance < next_frame_deadline_) {
return false;
}
next_frame_deadline_ += interval;
if (next_frame_deadline_ <= now) {
next_frame_deadline_ = now + interval;
}
return true;
}
void SessionDeviceManager::Initialize() {
InitializeAudioOutput();
screen_capturer_factory_ = new ScreenCapturerFactory();
speaker_capturer_factory_ = new SpeakerCapturerFactory();
device_controller_factory_ = new DeviceControllerFactory();
keyboard_capturer_ =
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Keyboard));
}
int SessionDeviceManager::InitializeScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) {
screen_capturer_ =
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
}
last_frame_time_ = {};
next_frame_deadline_ = {};
const int fps = owner_.config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
LOG_INFO("Init screen capturer with {} fps", fps);
display_info_list_.clear();
registered_display_stream_count_ = 0;
last_video_frame_stream_id_.clear();
invalid_video_stream_id_logged_ = false;
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
const char *display_name) {
const auto now_time = std::chrono::steady_clock::now();
if (!ShouldSendCapturedFrame(now_time, fps)) {
return;
}
const bool has_previous_frame =
last_frame_time_.time_since_epoch().count() != 0;
const auto duration_ms =
has_previous_frame
? std::chrono::duration_cast<std::chrono::milliseconds>(
now_time - last_frame_time_)
.count()
: 0;
std::vector<std::string> connected_remote_ids;
{
std::shared_lock lock(owner_.connection_status_mutex_);
connected_remote_ids.reserve(owner_.connection_status_.size());
for (const auto &[remote_id, status] : owner_.connection_status_) {
if (status == ConnectionStatus::Connected) {
connected_remote_ids.push_back(remote_id);
}
}
}
// Capture can still deliver frames while ICE is gathering or after
// the final controller disconnects. Do not broadcast those frames to
// MiniRTC: a broadcast also reaches newly joining peers whose ICE
// transport is not ready yet.
if (connected_remote_ids.empty()) {
last_frame_time_ = now_time;
return;
}
const std::string stream_id = ResolveDisplayStreamId(
display_name, registered_display_stream_count_, -1,
last_video_frame_stream_id_);
if (stream_id.empty()) {
if (!invalid_video_stream_id_logged_) {
LOG_ERROR(
"Drop captured frames with an empty or unregistered video "
"stream id, reported='{}', registered_streams={}",
display_name ? display_name : "",
registered_display_stream_count_);
invalid_video_stream_id_logged_ = true;
}
return;
}
invalid_video_stream_id_logged_ = false;
const bool resumed_after_gap =
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
last_video_frame_stream_id_ != stream_id;
if (resumed_after_gap || stream_changed) {
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
LOG_INFO("Request video key frame before sending captured frame, "
"stream='{}', gap_ms={}, stream_changed={}",
stream_id, duration_ms, stream_changed);
}
}
XVideoFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
for (const std::string &remote_id : connected_remote_ids) {
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
remote_id.data(), remote_id.size());
}
last_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time;
});
if (init_ret == 0) {
LOG_INFO("Init screen capturer success");
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
registered_display_stream_count_ = display_info_list_.size();
return 0;
}
LOG_ERROR("Init screen capturer failed");
screen_capturer_->Destroy();
delete screen_capturer_;
screen_capturer_ = nullptr;
return -1;
}
int SessionDeviceManager::StartScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) {
LOG_INFO("Screen capturer instance missing, recreating before start");
if (InitializeScreenCapturer() != 0) {
LOG_ERROR("Recreate screen capturer failed");
return -1;
}
}
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
const int ret = screen_capturer_->Start(owner_.show_cursor_);
if (ret != 0) {
LOG_ERROR("Start screen capturer failed: {}", ret);
}
return ret;
}
int SessionDeviceManager::StopScreenCapturer() {
if (screen_capturer_) {
LOG_INFO("Stop screen capturer");
screen_capturer_->Stop();
}
return 0;
}
int SessionDeviceManager::StartSpeakerCapturer() {
if (!speaker_capturer_) {
speaker_capturer_ =
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
const int init_ret = speaker_capturer_->Init(
[this](unsigned char *data, size_t size, const char *audio_name) {
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
size, owner_.audio_label_.c_str());
});
if (init_ret != 0) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
}
if (!speaker_capturer_) {
return -1;
}
const int ret = speaker_capturer_->Start();
if (ret != 0) {
LOG_ERROR("Start speaker capturer failed: {}", ret);
return ret;
}
owner_.start_speaker_capturer_ = true;
return 0;
}
int SessionDeviceManager::StopSpeakerCapturer() {
if (speaker_capturer_) {
speaker_capturer_->Stop();
owner_.start_speaker_capturer_ = false;
}
return 0;
}
int SessionDeviceManager::StartMouseController() {
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
return -1;
}
#endif
if (!device_controller_factory_) {
LOG_INFO("Device controller factory is nullptr");
return -1;
}
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
if (!screen_capturer_) {
return 1;
}
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (latest_display_info.empty() ||
latest_display_info[0].handle == nullptr) {
return 1;
}
}
if (screen_capturer_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
}
#endif
mouse_controller_ =
static_cast<MouseController *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Mouse));
if (!mouse_controller_) {
LOG_ERROR("Create mouse controller failed");
return -1;
}
const int init_ret = mouse_controller_->Init(display_info_list_);
if (init_ret != 0) {
LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
return init_ret;
}
int SessionDeviceManager::StopMouseController() {
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
return 0;
}
int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_window_events_ = true;
return 0;
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0;
}
#endif
if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0;
}
const int hook_ret = keyboard_capturer_->Hook(
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
void *user_ptr) {
if (user_ptr) {
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
extended);
}
},
this);
if (hook_ret != 0) {
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to Slint keyboard events");
} else {
LOG_INFO("Start keyboard capturer with native input");
}
return 0;
}
int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0;
}
if (keyboard_capturer_) {
keyboard_capturer_->Unhook();
ClearCapturedKeyboardInput();
LOG_INFO("Stop keyboard capturer");
}
return 0;
}
int SessionDeviceManager::InitializeAudioOutput() {
SDL_AudioSpec desired_out{};
desired_out.freq = 48000;
desired_out.format = SDL_AUDIO_S16;
desired_out.channels = 1;
auto open_stream = [&]() {
output_stream_ = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
return output_stream_ != nullptr;
};
if (!open_stream()) {
#if defined(__linux__) && !defined(__APPLE__)
LOG_WARN("Failed to open output stream with driver [{}]: {}",
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
: "(default)",
SDL_GetError());
setenv("SDL_AUDIODRIVER", "dummy", 1);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
SDL_GetError());
return -1;
}
if (!open_stream()) {
LOG_ERROR("Failed to open output stream with dummy driver: {}",
SDL_GetError());
return -1;
}
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
#else
LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
return -1;
#endif
}
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
return 0;
}
int SessionDeviceManager::DestroyAudioOutput() {
if (output_stream_) {
SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
SDL_DestroyAudioStream(output_stream_);
output_stream_ = nullptr;
}
return 0;
}
void SessionDeviceManager::PushAudio(const char *data, size_t size) {
if (!output_stream_) {
return;
}
const int pushed = SDL_PutAudioStreamData(
output_stream_, reinterpret_cast<const Uint8 *>(data),
static_cast<int>(size));
if (pushed < 0) {
LOG_ERROR("Failed to push audio data: {}", SDL_GetError());
}
}
void SessionDeviceManager::UpdateInteractions() {
#if defined(__linux__) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
is_wayland_session && !owner_.start_screen_capturer_ &&
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
owner_.mouse_controller_is_started_;
if (stop_wayland_mouse_before_screen) {
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
"cleanly release the shared portal session");
StopMouseController();
owner_.mouse_controller_is_started_ = false;
}
#endif
if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) {
if (StartScreenCapturer() == 0) {
owner_.screen_capturer_is_started_ = true;
}
} else if (!owner_.start_screen_capturer_ &&
owner_.screen_capturer_is_started_) {
StopScreenCapturer();
owner_.screen_capturer_is_started_ = false;
}
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
if (StartSpeakerCapturer() == 0) {
owner_.speaker_capturer_is_started_ = true;
}
} else if (!owner_.start_speaker_capturer_ &&
owner_.speaker_capturer_is_started_) {
StopSpeakerCapturer();
owner_.speaker_capturer_is_started_ = false;
}
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
if (StartMouseController() == 0) {
owner_.mouse_controller_is_started_ = true;
}
} else if (!owner_.start_mouse_controller_ &&
owner_.mouse_controller_is_started_) {
StopMouseController();
owner_.mouse_controller_is_started_ = false;
}
#if defined(__linux__) || defined(__APPLE__)
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
mouse_controller_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
}
}
#endif
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
owner_.keyboard_capturer_is_started_ = true;
}
if (owner_.keyboard_capturer_is_started_) {
DrainCapturedKeyboardInput();
owner_.keyboard_.SendHeartbeat(false);
}
} else if (owner_.keyboard_capturer_is_started_) {
owner_.keyboard_.ForceReleasePressedKeys();
StopKeyboardCapturer();
owner_.keyboard_capturer_is_started_ = false;
}
owner_.keyboard_.CheckRemoteTimeouts();
}
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
bool is_down,
uint32_t scan_code,
bool extended) {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
captured_keyboard_inputs_.pop_front();
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
}
captured_keyboard_inputs_.push_back(
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
}
void SessionDeviceManager::DrainCapturedKeyboardInput() {
std::deque<CapturedKeyboardInput> inputs;
{
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
inputs.swap(captured_keyboard_inputs_);
}
for (const CapturedKeyboardInput &input : inputs) {
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
input.scan_code, input.extended);
}
}
void SessionDeviceManager::ClearCapturedKeyboardInput() {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
captured_keyboard_inputs_.clear();
}
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code,
bool extended) {
return keyboard_capturer_ && keyboard_capturer_->SendKeyboardCommand(
key_code, is_down, scan_code, extended) == 0;
}
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
int selected_display) {
if (mouse_controller_) {
mouse_controller_->SendMouseCommand(action, selected_display);
}
}
int SessionDeviceManager::SwitchDisplay(int display_id) {
return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1;
}
void SessionDeviceManager::ResetToInitialDisplay() {
if (screen_capturer_) {
screen_capturer_->ResetToInitialMonitor();
}
}
const std::vector<DisplayInfo> &
SessionDeviceManager::display_info_list() const {
return display_info_list_;
}
void SessionDeviceManager::DestroyDevices() {
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
if (screen_capturer_) {
screen_capturer_->Destroy();
delete screen_capturer_;
screen_capturer_ = nullptr;
}
if (speaker_capturer_) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
if (keyboard_capturer_) {
delete keyboard_capturer_;
keyboard_capturer_ = nullptr;
}
}
void SessionDeviceManager::DestroyFactories() {
delete screen_capturer_factory_;
screen_capturer_factory_ = nullptr;
delete speaker_capturer_factory_;
speaker_capturer_factory_ = nullptr;
delete device_controller_factory_;
device_controller_factory_ = nullptr;
}
} // namespace crossdesk
@@ -0,0 +1,96 @@
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#include <SDL3/SDL.h>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <string>
#include <vector>
#include <remote_action.h>
#include "device_controller.h"
#include "device_controller_factory.h"
#include "display_info.h"
#include "screen_capturer_factory.h"
#include "speaker_capturer_factory.h"
namespace crossdesk {
class GuiRuntime;
// Owns media capture/playback and remote input devices for the active GUI
// session. GuiRuntime supplies application intent; this class handles device
// creation, updates and teardown.
class SessionDeviceManager {
public:
explicit SessionDeviceManager(GuiRuntime &owner);
void Initialize();
void UpdateInteractions();
void DestroyDevices();
void DestroyFactories();
int InitializeScreenCapturer();
int StartScreenCapturer();
int StopScreenCapturer();
int StartSpeakerCapturer();
int StopSpeakerCapturer();
int StartMouseController();
int StopMouseController();
int StartKeyboardCapturer();
int StopKeyboardCapturer();
int InitializeAudioOutput();
int DestroyAudioOutput();
void PushAudio(const char *data, size_t size);
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void SendMouseCommand(const RemoteAction &action, int selected_display);
int SwitchDisplay(int display_id);
void ResetToInitialDisplay();
const std::vector<DisplayInfo> &display_info_list() const;
private:
struct CapturedKeyboardInput {
int key_code = 0;
bool is_down = false;
uint32_t scan_code = 0;
bool extended = false;
};
void QueueCapturedKeyboardInput(int key_code, bool is_down,
uint32_t scan_code, bool extended);
void DrainCapturedKeyboardInput();
void ClearCapturedKeyboardInput();
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps);
GuiRuntime &owner_;
SDL_AudioStream *output_stream_ = nullptr;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
ScreenCapturer *screen_capturer_ = nullptr;
SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr;
SpeakerCapturer *speaker_capturer_ = nullptr;
DeviceControllerFactory *device_controller_factory_ = nullptr;
MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_;
std::chrono::steady_clock::time_point last_frame_time_{};
std::chrono::steady_clock::time_point next_frame_deadline_{};
std::string last_video_frame_stream_id_;
bool invalid_video_stream_id_logged_ = false;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
@@ -0,0 +1,340 @@
#include "features/file_transfer/file_transfer_manager.h"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <utility>
#include "file_transfer.h"
#include "filesystem_utf8.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
return global_state_;
}
FileTransferManager::FileTransferState &FileTransferManager::state_for(
const std::shared_ptr<RemoteSession> &props) {
return props ? props->file_transfer_ : global_state_;
}
void FileTransferManager::ProcessSelectedFile(
const std::string &path,
const std::shared_ptr<RemoteSession> &props,
const std::string &file_label, const std::string &remote_id) {
if (path.empty()) {
return;
}
FileTransferState &state = state_for(props);
LOG_INFO("Selected file: {}", path.c_str());
const std::filesystem::path file_path = PathFromUtf8(path);
std::error_code ec;
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("Selected path is not a regular file: {}", path);
return;
}
const uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message());
return;
}
{
std::lock_guard<std::mutex> lock(state.file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
const auto utf8_name = file_path.filename().u8string();
info.file_name.assign(reinterpret_cast<const char *>(utf8_name.data()),
utf8_name.size());
info.file_path = file_path;
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
state.file_transfer_list_.push_back(std::move(info));
}
state.file_transfer_window_visible_ = true;
auto enqueue = [&]() {
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
state.file_send_queue_.push(
FileTransferState::QueuedFile{file_path, file_label, remote_id});
queue_size = state.file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
};
if (state.file_sending_.load()) {
enqueue();
return;
}
Start(props, file_path, file_label, remote_id);
if (!state.file_sending_.load()) {
enqueue();
}
}
void FileTransferManager::Start(
std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path, const std::string &file_label,
const std::string &remote_id) {
const bool is_global = !props;
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
if (!peer) {
LOG_ERROR("StartFileTransfer: invalid peer");
return;
}
FileTransferState &initial_state = state_for(props);
bool expected = false;
if (!initial_state.file_sending_.compare_exchange_strong(expected, true)) {
LOG_WARN("StartFileTransfer called while another file is active: {}",
file_path.filename().string().c_str());
return;
}
const auto props_weak = std::weak_ptr<RemoteSession>(props);
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
is_global]() {
auto props_locked = props_weak.lock();
FileTransferState *state = nullptr;
if (props_locked) {
state = &props_locked->file_transfer_;
} else if (is_global) {
state = &global_state_;
} else {
return;
}
std::error_code ec;
const uint64_t total_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
state->file_sending_ = false;
return;
}
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(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;
}
FileSender sender;
const uint32_t file_id = FileSender::NextFileId();
if (props_locked) {
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
file_id_to_props_[file_id] = props_weak;
} else {
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
file_id_to_state_[file_id] = state;
}
state->current_file_id_ = file_id;
{
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 == FileTransferState::FileTransferStatus::Queued) {
info.status = FileTransferState::FileTransferStatus::Sending;
info.file_id = file_id;
info.file_size = total_size;
info.sent_bytes = 0;
break;
}
}
}
const int ret = sender.SendFile(
file_path, file_path.filename().string(),
[peer, file_label, remote_id](const char *buffer, size_t size) {
if (remote_id.empty()) {
return SendReliableDataFrame(peer, buffer, size,
file_label.c_str());
}
return SendReliableDataFrameToPeer(
peer, buffer, size, file_label.c_str(), remote_id.c_str(),
remote_id.size());
},
64 * 1024, file_id);
if (ret == 0) {
return;
}
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, props_locked != nullptr);
{
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);
ProcessQueue(props_locked);
}).detach();
}
void FileTransferManager::ProcessQueue(
std::shared_ptr<RemoteSession> props) {
FileTransferState &state = state_for(props);
if (state.file_sending_.load()) {
return;
}
FileTransferState::QueuedFile queued_file;
{
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
if (state.file_send_queue_.empty()) {
return;
}
queued_file = state.file_send_queue_.front();
state.file_send_queue_.pop();
}
Start(props, queued_file.file_path, queued_file.file_label,
queued_file.remote_id);
}
void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
if (per_peer) {
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
file_id_to_props_.erase(file_id);
} else {
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
file_id_to_state_.erase(file_id);
}
}
void FileTransferManager::HandleAck(const char *data, size_t size) {
FileTransferAck ack{};
if (!DecodeFileTransferAck(data, size, &ack)) {
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
return;
}
std::shared_ptr<RemoteSession> props;
{
std::shared_lock lock(file_id_to_props_mutex_);
const auto it = file_id_to_props_.find(ack.file_id);
if (it != file_id_to_props_.end()) {
props = it->second.lock();
}
}
FileTransferState *state = props ? &props->file_transfer_ : nullptr;
if (!state) {
std::shared_lock lock(file_id_to_state_mutex_);
const auto it = file_id_to_state_.find(ack.file_id);
if (it != file_id_to_state_.end()) {
state = it->second;
}
}
if (!state) {
LOG_WARN("FileTransferAck: no state found for file_id={}", ack.file_id);
return;
}
state->file_sent_bytes_ = ack.acked_offset;
state->file_total_bytes_ = ack.total_size;
uint32_t rate_bps = 0;
if (props) {
const uint32_t bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
const uint32_t current_rate = state->file_send_rate_bps_.load();
if (current_rate > 0) {
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
rate_bps = state->file_send_rate_bps_.load();
}
} else {
const uint32_t current_rate = state->file_send_rate_bps_.load();
uint32_t estimated_rate = 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 auto delta_bytes = ack.acked_offset - last_bytes;
const double seconds =
std::chrono::duration<double>(now - last_time).count();
if (seconds > 0.0 && delta_bytes > 0) {
const double bits_per_second = delta_bytes * 8.0 / seconds;
estimated_rate = static_cast<uint32_t>((std::min)(
bits_per_second,
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
}
}
rate_bps =
estimated_rate > 0 && current_rate > 0
? static_cast<uint32_t>(current_rate * 0.7 + estimated_rate * 0.3)
: estimated_rate > 0 ? estimated_rate
: current_rate;
}
state->file_send_rate_bps_ = rate_bps;
state->file_send_last_bytes_ = ack.acked_offset;
state->file_send_last_update_time_ = std::chrono::steady_clock::now();
{
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;
info.rate_bps = rate_bps;
if ((ack.flags & 0x01) != 0) {
info.status = FileTransferState::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size;
}
break;
}
}
}
if ((ack.flags & 0x01) == 0) {
return;
}
LOG_INFO("File transfer completed: file_id={}, bytes={}", ack.file_id,
ack.total_size);
state->file_transfer_window_visible_ = true;
state->file_sending_ = false;
Unregister(ack.file_id, props != nullptr);
ProcessQueue(props);
}
} // namespace crossdesk
@@ -0,0 +1,54 @@
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include "runtime/gui_state.h"
namespace crossdesk {
class GuiRuntime;
class FileTransferManager {
public:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
explicit FileTransferManager(GuiRuntime &owner);
FileTransferState &global_state();
FileTransferState &
state_for(const std::shared_ptr<RemoteSession> &props);
void
ProcessSelectedFile(const std::string &path,
const std::shared_ptr<RemoteSession> &props,
const std::string &file_label,
const std::string &remote_id = "");
void HandleAck(const char *data, size_t size);
private:
void Start(std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path,
const std::string &file_label, const std::string &remote_id = "");
void ProcessQueue(std::shared_ptr<RemoteSession> props);
void Unregister(uint32_t file_id, bool per_peer);
GuiRuntime &owner_;
FileTransferState global_state_;
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
file_id_to_props_;
std::shared_mutex file_id_to_props_mutex_;
std::unordered_map<uint32_t, FileTransferState *> file_id_to_state_;
std::shared_mutex file_id_to_state_mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
@@ -0,0 +1,365 @@
#include "features/input/keyboard_controller.h"
#include <remote_action.h>
#include <SDL3/SDL.h>
#include <cstdint>
#include <mutex>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include "minirtc.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "windows_key_metadata.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
constexpr uint32_t kHeartbeatIntervalMs = 500;
constexpr uint32_t kRemoteReleaseTimeoutMs = 2500;
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
bool extended) {
#if _WIN32
if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) {
return key_code;
}
UINT scan_code_with_prefix = static_cast<UINT>(scan_code & 0xFF);
if (extended) {
scan_code_with_prefix |= 0xE000;
}
const UINT normalized_vk =
MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX);
return normalized_vk != 0 ? static_cast<int>(normalized_vk) : key_code;
#else
(void)scan_code;
(void)extended;
return key_code;
#endif
}
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
bool* extended_out) {
if (!scan_code_out || !extended_out) {
return;
}
#if _WIN32
const UINT scan_code =
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
if (scan_code != 0) {
*scan_code_out = static_cast<uint32_t>(scan_code & 0xFF);
*extended_out = (scan_code & 0xFF00) != 0;
return;
}
#endif
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
}
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
}
*last_tick = now;
LOG_WARN(
"local secure-desktop input blocked, stage={}, normal SendInput path "
"cannot drive the Windows password UI",
stage ? stage : "");
}
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
const RemoteAction& action) {
return response.is_object() &&
response.value("error", std::string()) == "send_input_failed" &&
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
action.type == ControlType::keyboard &&
action.k.flag == KeyFlag::key_up;
}
#endif
} // namespace
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
if (is_down) {
pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended};
} else {
pressed_keys_.erase(key_code);
}
}
void KeyboardController::ForceReleasePressedKeys() {
std::vector<PressedKey> pressed_keys;
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
pressed_keys.reserve(pressed_keys_.size());
for (const auto& [_, key] : pressed_keys_) {
pressed_keys.push_back(key);
}
pressed_keys_.clear();
}
for (const PressedKey& key : pressed_keys) {
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
}
SendHeartbeat(true);
}
void KeyboardController::SendHeartbeat(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) {
return;
}
RemoteAction action{};
action.type = ControlType::keyboard_state;
action.ks.seq = ++state_sequence_;
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
size_t index = 0;
for (const auto& [_, key] : pressed_keys_) {
if (index >= kMaxKeyboardStateKeys) {
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
pressed_keys_.size());
break;
}
action.ks.pressed_keys[index].key_value =
static_cast<size_t>(key.key_code);
action.ks.pressed_keys[index].scan_code = key.scan_code;
action.ks.pressed_keys[index].extended = key.extended;
++index;
}
action.ks.pressed_count = index;
}
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
const auto props_it = owner_.remote_sessions_.find(target_id);
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
props_it->second->connection_status_.load() !=
ConnectionStatus::Connected ||
!props_it->second->peer_) {
last_heartbeat_tick_ = now;
return;
}
const std::string message = action.to_json();
const int result = SendReliableDataFrame(
props_it->second->peer_, message.c_str(), message.size(),
props_it->second->keyboard_label_.c_str());
if (result != 0) {
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
result);
}
last_heartbeat_tick_ = now;
}
int KeyboardController::SendKeyCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
if (scan_code == 0) {
PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
}
#if _WIN32
key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended);
#endif
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
action.k.key_value = key_code;
action.k.scan_code = scan_code;
action.k.extended = extended;
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
const auto props_it = owner_.remote_sessions_.find(target_id);
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
props_it->second->connection_status_.load() ==
ConnectionStatus::Connected &&
props_it->second->peer_) {
const std::string message = action.to_json();
const int result = SendReliableDataFrame(
props_it->second->peer_, message.c_str(), message.size(),
props_it->second->keyboard_label_.c_str());
if (result != 0) {
LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", target_id,
result);
}
}
TrackPressedKey(key_code, is_down, scan_code, extended);
return 0;
}
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
#if _WIN32
if (owner_.local_service_status_received_ &&
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
const auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.key_value = static_cast<size_t>(key_code);
action.k.scan_code = scan_code;
action.k.extended = extended;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
LogSecureDesktopInputBlocked(
&owner_.last_local_secure_input_block_log_tick_,
owner_.local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
"response={}",
key_code, is_down, response);
return false;
}
return true;
}
#endif
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
extended);
}
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
const RemoteAction& action) {
const int key_code = static_cast<int>(action.k.key_value);
const bool is_down = action.k.flag == KeyFlag::key_down;
const bool injected =
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto& state = remote_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down && injected) {
state.pressed_keys[key_code] =
PressedKey{key_code, action.k.scan_code, action.k.extended};
} else if (!is_down && injected) {
state.pressed_keys.erase(key_code);
}
}
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
const RemoteAction& action) {
std::vector<PressedKey> keys_to_release;
std::vector<PressedKey> keys_to_press;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto& state = remote_states_[remote_id];
if (action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
return;
}
state.last_seq = action.ks.seq;
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
state.keyboard_state_seen = true;
std::unordered_map<int, PressedKey> desired_keys;
const size_t count =
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
for (size_t index = 0; index < count; ++index) {
const auto& key = action.ks.pressed_keys[index];
const int key_code = static_cast<int>(key.key_value);
desired_keys[key_code] =
PressedKey{key_code, key.scan_code, key.extended};
}
for (const auto& [key_code, key] : state.pressed_keys) {
if (desired_keys.find(key_code) == desired_keys.end()) {
keys_to_release.push_back(key);
}
}
for (const auto& [key_code, key] : desired_keys) {
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
keys_to_press.push_back(key);
}
}
}
for (const PressedKey& key : keys_to_release) {
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
const auto state_it = remote_states_.find(remote_id);
if (state_it != remote_states_.end()) {
state_it->second.pressed_keys.erase(key.key_code);
}
}
}
for (const PressedKey& key : keys_to_press) {
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
remote_states_[remote_id].pressed_keys[key.key_code] = key;
}
}
}
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason) {
std::vector<PressedKey> keys_to_release;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
const auto state_it = remote_states_.find(remote_id);
if (state_it == remote_states_.end()) {
return;
}
for (const auto& [_, key] : state_it->second.pressed_keys) {
keys_to_release.push_back(key);
}
remote_states_.erase(state_it);
}
if (!keys_to_release.empty()) {
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKey& key : keys_to_release) {
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
}
}
void KeyboardController::CheckRemoteTimeouts() {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
for (const auto& [remote_id, state] : remote_states_) {
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
state.last_seen_tick != 0 &&
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
timed_out_remotes.push_back(remote_id);
}
}
}
for (const std::string& remote_id : timed_out_remotes) {
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
}
}
} // namespace crossdesk
@@ -0,0 +1,65 @@
#ifndef CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
#define CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include <remote_action.h>
#include "device_controller.h"
namespace crossdesk {
class GuiRuntime;
// Synchronizes local and remote keyboard state, including heartbeat recovery
// for keys whose key-up event was lost during a connection interruption.
class KeyboardController {
public:
explicit KeyboardController(GuiRuntime &owner);
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
bool extended = false);
void ForceReleasePressedKeys();
void SendHeartbeat(bool force);
void ApplyRemoteEvent(const std::string &remote_id,
const RemoteAction &remote_action);
void ApplyRemoteState(const std::string &remote_id,
const RemoteAction &remote_action);
void ReleaseRemotePressedKeys(const std::string &remote_id,
const char *reason);
void CheckRemoteTimeouts();
private:
struct PressedKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
struct RemoteState {
std::unordered_map<int, PressedKey> pressed_keys;
uint32_t last_seq = 0;
uint32_t last_seen_tick = 0;
bool keyboard_state_seen = false;
};
void TrackPressedKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
GuiRuntime &owner_;
std::unordered_map<int, PressedKey> pressed_keys_;
std::mutex pressed_keys_mutex_;
uint32_t state_sequence_ = 0;
uint32_t last_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteState> remote_states_;
std::mutex remote_states_mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
@@ -0,0 +1,622 @@
#include "features/settings/settings_manager.h"
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <exception>
#include <filesystem>
#include <fstream>
#include <functional>
#include <memory>
#include <string>
#include <thread>
#if defined(_WIN32)
#include <io.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif
#include "localization.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
constexpr uint32_t kCacheV3Version = 3;
template <size_t Size>
void CopyString(char (&destination)[Size], const char *source) {
static_assert(Size > 0);
std::memset(destination, 0, Size);
if (source) {
std::strncpy(destination, source, Size - 1);
}
}
uint32_t CacheChecksum(const void *data, size_t size) {
const auto *bytes = static_cast<const unsigned char *>(data);
uint32_t hash = 2166136261u;
for (size_t i = 0; i < size; ++i) {
hash ^= bytes[i];
hash *= 16777619u;
}
return hash;
}
std::filesystem::path TemporaryPathFor(
const std::filesystem::path &target) {
const auto timestamp = std::chrono::steady_clock::now()
.time_since_epoch()
.count();
const auto thread_id =
std::hash<std::thread::id>{}(std::this_thread::get_id());
std::filesystem::path temporary = target;
temporary += ".tmp-" + std::to_string(timestamp) + "-" +
std::to_string(thread_id);
return temporary;
}
bool FlushFileToDisk(FILE *file) {
if (!file || std::fflush(file) != 0) {
return false;
}
#if defined(_WIN32)
return _commit(_fileno(file)) == 0;
#else
return fsync(fileno(file)) == 0;
#endif
}
bool ReplaceFileAtomically(const std::filesystem::path &temporary,
const std::filesystem::path &target) {
#if defined(_WIN32)
return MoveFileExW(temporary.c_str(), target.c_str(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
#else
if (::rename(temporary.c_str(), target.c_str()) != 0) {
return false;
}
const std::filesystem::path parent = target.parent_path().empty()
? std::filesystem::path(".")
: target.parent_path();
int directory_flags = O_RDONLY;
#if defined(O_DIRECTORY)
directory_flags |= O_DIRECTORY;
#endif
const int directory = open(parent.c_str(), directory_flags);
if (directory >= 0) {
const bool synced = fsync(directory) == 0;
close(directory);
return synced;
}
return false;
#endif
}
bool WriteFileAtomically(const std::filesystem::path &target,
const void *data, size_t size) {
std::error_code ec;
const std::filesystem::path parent = target.parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent, ec);
if (ec) {
return false;
}
}
const std::filesystem::path temporary = TemporaryPathFor(target);
#if defined(_WIN32)
FILE *file = _wfopen(temporary.c_str(), L"wb");
#else
FILE *file = std::fopen(temporary.c_str(), "wb");
#endif
if (!file) {
return false;
}
const bool written = std::fwrite(data, 1, size, file) == size;
const bool flushed = written && FlushFileToDisk(file);
const bool closed = std::fclose(file) == 0;
if (!written || !flushed || !closed ||
!ReplaceFileAtomically(temporary, target)) {
std::filesystem::remove(temporary, ec);
return false;
}
return true;
}
} // namespace
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
int SettingsManager::Save() {
std::lock_guard<std::mutex> lock(cache_mutex_);
return SaveLocked();
}
int SettingsManager::SaveLocked() {
CopyString(cache_v2_.client_id_with_password,
owner_.client_id_with_password_);
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
cache_v3_.magic = kCacheV3Magic;
cache_v3_.version = kCacheV3Version;
cache_v3_.base = cache_v2_;
cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0';
cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] =
'\0';
cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] =
'\0';
cache_v3_.checksum =
CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum));
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc",
&cache_v3_, sizeof(cache_v3_))) {
return -1;
}
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v2.enc",
&cache_v2_, sizeof(cache_v2_))) {
LOG_WARN("Failed to update legacy v2 credential cache");
}
// Keep writing the legacy cache while older installations may still read it.
CopyString(cache_v1_.client_id_with_password,
owner_.client_id_with_password_);
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
&cache_v1_, sizeof(cache_v1_))) {
LOG_WARN("Failed to update legacy v1 credential cache");
}
return 0;
}
bool SettingsManager::ReadV3Locked() {
std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc",
std::ios::binary);
if (!cache_file) {
return false;
}
CacheV3 loaded{};
cache_file.read(reinterpret_cast<char *>(&loaded), sizeof(loaded));
if (cache_file.gcount() != static_cast<std::streamsize>(sizeof(loaded)) ||
loaded.magic != kCacheV3Magic ||
loaded.version != kCacheV3Version ||
loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) {
LOG_WARN("Ignore invalid v3 credential cache");
return false;
}
loaded.base.client_id_with_password
[sizeof(loaded.base.client_id_with_password) - 1] = '\0';
loaded.base.self_hosted_id[sizeof(loaded.base.self_hosted_id) - 1] = '\0';
loaded.pending_identity[sizeof(loaded.pending_identity) - 1] = '\0';
loaded.pending_server_host[sizeof(loaded.pending_server_host) - 1] = '\0';
loaded.pending_request_id[sizeof(loaded.pending_request_id) - 1] = '\0';
cache_v3_ = loaded;
cache_v2_ = loaded.base;
return true;
}
bool SettingsManager::ReadV2Locked() {
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
std::ios::binary);
if (!cache_v2_file) {
return false;
}
cache_v2_file.read(reinterpret_cast<char *>(&cache_v2_), sizeof(cache_v2_));
if (cache_v2_file.gcount() !=
static_cast<std::streamsize>(sizeof(cache_v2_))) {
return false;
}
cache_v2_
.client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] =
'\0';
cache_v2_.self_hosted_id[sizeof(cache_v2_.self_hosted_id) - 1] = '\0';
return true;
}
int SettingsManager::Load() {
std::unique_lock<std::mutex> lock(cache_mutex_);
const bool loaded_v3 = ReadV3Locked();
if (loaded_v3 || ReadV2Locked()) {
CopyString(owner_.client_id_with_password_,
cache_v2_.client_id_with_password);
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key));
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
if (loaded_v3) {
LOG_INFO("Load settings from v3 cache file");
} else {
cache_v3_ = {};
SaveLocked();
LOG_INFO("Migrated settings from v2 to v3 cache file");
}
} else {
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
std::ios::binary);
if (!cache_v1_file) {
lock.unlock();
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
std::memset(owner_.aes128_key_, 0, sizeof(owner_.aes128_key_));
std::memset(owner_.aes128_iv_, 0, sizeof(owner_.aes128_iv_));
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
owner_.thumbnail_ =
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/");
owner_.thumbnail_->GetKeyAndIv(owner_.aes128_key_, owner_.aes128_iv_);
owner_.thumbnail_->DeleteAllFilesInDirectory();
Save();
return -1;
}
cache_v1_file.read(reinterpret_cast<char *>(&cache_v1_), sizeof(cache_v1_));
cache_v1_
.client_id_with_password[sizeof(cache_v1_.client_id_with_password) -
1] = '\0';
CopyString(cache_v2_.client_id_with_password,
cache_v1_.client_id_with_password);
std::memcpy(cache_v2_.key, cache_v1_.key, sizeof(cache_v1_.key));
std::memcpy(cache_v2_.iv, cache_v1_.iv, sizeof(cache_v1_.iv));
std::memset(cache_v2_.self_hosted_id, 0, sizeof(cache_v2_.self_hosted_id));
CopyString(owner_.client_id_with_password_,
cache_v1_.client_id_with_password);
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key));
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
cache_v3_ = {};
SaveLocked();
LOG_INFO("Migrated settings from v1 to v3 cache file");
}
lock.unlock();
ActivateCachedPublicIdentity();
owner_.thumbnail_ =
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
owner_.aes128_key_, owner_.aes128_iv_);
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.video_quality_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoQuality());
owner_.video_frame_rate_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_hardware_video_codec_ =
owner_.config_center_->IsHardwareVideoCodec();
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
#if _WIN32 && CROSSDESK_PORTABLE
owner_.portable_service_prompt_suppressed_ =
owner_.config_center_->IsPortableServicePromptSuppressed();
owner_.portable_service_do_not_remind_ =
owner_.portable_service_prompt_suppressed_;
#endif
const std::string saved_path =
owner_.config_center_->GetFileTransferSavePath();
CopyString(owner_.file_transfer_save_path_buf_, saved_path.c_str());
owner_.file_transfer_save_path_last_ = saved_path;
owner_.language_button_value_last_ = owner_.language_button_value_;
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
owner_.video_encode_format_button_value_last_ =
owner_.video_encode_format_button_value_;
owner_.enable_hardware_video_codec_last_ =
owner_.enable_hardware_video_codec_;
owner_.enable_turn_last_ = owner_.enable_turn_;
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
LOG_INFO("Load settings from cache file");
return 0;
}
bool SettingsManager::LoadCachedSelfHostedIdentity() {
std::lock_guard<std::mutex> lock(cache_mutex_);
if ((!ReadV3Locked() && !ReadV2Locked()) ||
cache_v2_.self_hosted_id[0] == '\0') {
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
return false;
}
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
const char *at_pos = std::strchr(owner_.self_hosted_id_, '@');
if (at_pos == nullptr) {
CopyString(owner_.client_id_, owner_.self_hosted_id_);
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
} else {
const std::string id(owner_.self_hosted_id_,
at_pos - owner_.self_hosted_id_);
CopyString(owner_.client_id_, id.c_str());
CopyString(owner_.password_saved_, at_pos + 1);
}
return true;
}
bool SettingsManager::ActivateCachedPublicIdentity() {
if (owner_.client_id_with_password_[0] == '\0') {
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
return false;
}
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
if (at_pos == nullptr) {
CopyString(owner_.client_id_, owner_.client_id_with_password_);
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
} else {
const std::string id(owner_.client_id_with_password_,
at_pos - owner_.client_id_with_password_);
CopyString(owner_.client_id_, id.c_str());
CopyString(owner_.password_saved_, at_pos + 1);
}
return owner_.client_id_[0] != '\0';
}
void SettingsManager::ActivateIdentity(const char *identity,
bool self_hosted) {
if (!identity) {
return;
}
if (self_hosted) {
CopyString(owner_.self_hosted_id_, identity);
} else {
CopyString(owner_.client_id_with_password_, identity);
}
const char *at_pos = std::strchr(identity, '@');
if (at_pos == nullptr) {
CopyString(owner_.client_id_, identity);
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
return;
}
const std::string id(identity, at_pos - identity);
CopyString(owner_.client_id_, id.c_str());
CopyString(owner_.password_saved_, at_pos + 1);
}
bool SettingsManager::StagePendingPasswordChange(
const std::string &identity, bool self_hosted,
const std::string &server_host, int server_port,
const std::string &request_id) {
if (identity.empty() || identity.size() >= sizeof(cache_v3_.pending_identity) ||
server_host.empty() ||
server_host.size() >= sizeof(cache_v3_.pending_server_host) ||
server_port <= 0 || request_id.empty() ||
request_id.size() >= sizeof(cache_v3_.pending_request_id)) {
return false;
}
std::lock_guard<std::mutex> lock(cache_mutex_);
if (cache_v3_.pending_identity[0] != '\0' &&
(cache_v3_.pending_self_hosted != self_hosted ||
cache_v3_.pending_server_port != server_port ||
server_host != cache_v3_.pending_server_host ||
request_id != cache_v3_.pending_request_id)) {
LOG_WARN("Refuse to overwrite an unresolved password change");
return false;
}
const CacheV3 previous = cache_v3_;
CopyString(cache_v3_.pending_identity, identity.c_str());
CopyString(cache_v3_.pending_server_host, server_host.c_str());
cache_v3_.pending_server_port = server_port;
cache_v3_.pending_self_hosted = self_hosted;
CopyString(cache_v3_.pending_request_id, request_id.c_str());
if (SaveLocked() != 0) {
cache_v3_ = previous;
return false;
}
return true;
}
std::string SettingsManager::PendingPasswordChangeIdentity(
bool self_hosted, const std::string &server_host, int server_port) const {
std::lock_guard<std::mutex> lock(cache_mutex_);
if (cache_v3_.pending_identity[0] == '\0' ||
cache_v3_.pending_self_hosted != self_hosted ||
cache_v3_.pending_server_port != server_port ||
server_host != cache_v3_.pending_server_host) {
return {};
}
return cache_v3_.pending_identity;
}
bool SettingsManager::PromotePendingPasswordChange() {
std::lock_guard<std::mutex> lock(cache_mutex_);
if (cache_v3_.pending_identity[0] == '\0') {
return true;
}
const CacheV3 previous = cache_v3_;
const std::string identity = cache_v3_.pending_identity;
const bool self_hosted = cache_v3_.pending_self_hosted;
ActivateIdentity(identity.c_str(), self_hosted);
std::memset(cache_v3_.pending_identity, 0,
sizeof(cache_v3_.pending_identity));
std::memset(cache_v3_.pending_server_host, 0,
sizeof(cache_v3_.pending_server_host));
cache_v3_.pending_server_port = 0;
cache_v3_.pending_self_hosted = false;
std::memset(cache_v3_.pending_request_id, 0,
sizeof(cache_v3_.pending_request_id));
if (SaveLocked() != 0) {
// The already-durable pending credential remains the recovery source on
// disk. Keep it in memory as well so a reconnect in this process retries
// the credential that the server has accepted.
cache_v3_ = previous;
return false;
}
return true;
}
bool SettingsManager::ClearPendingPasswordChange() {
std::lock_guard<std::mutex> lock(cache_mutex_);
if (cache_v3_.pending_identity[0] == '\0') {
return true;
}
const CacheV3 previous = cache_v3_;
std::memset(cache_v3_.pending_identity, 0,
sizeof(cache_v3_.pending_identity));
std::memset(cache_v3_.pending_server_host, 0,
sizeof(cache_v3_.pending_server_host));
cache_v3_.pending_server_port = 0;
cache_v3_.pending_self_hosted = false;
std::memset(cache_v3_.pending_request_id, 0,
sizeof(cache_v3_.pending_request_id));
if (SaveLocked() != 0) {
cache_v3_ = previous;
return false;
}
return true;
}
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
if (!client_id) {
return;
}
std::lock_guard<std::mutex> lock(cache_mutex_);
CopyString(owner_.self_hosted_id_, client_id);
if (SaveLocked() != 0) {
LOG_ERROR("Failed to persist self-hosted identity atomically");
}
}
int SettingsManager::LoadRecentConnectionAliases() {
recent_connection_aliases_.clear();
std::ifstream alias_file(owner_.cache_path_ +
"/recent_connection_aliases.json");
if (!alias_file.good()) {
return 0;
}
try {
nlohmann::json alias_json;
alias_file >> alias_json;
const nlohmann::json *aliases = &alias_json;
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
aliases = &alias_json["aliases"];
}
if (!aliases->is_object()) {
LOG_WARN("Invalid recent connection alias file");
return -1;
}
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
if (it.value().is_string()) {
const std::string remote_id = it.key();
const std::string alias = it.value().get<std::string>();
if (!remote_id.empty() && !alias.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
}
} catch (const std::exception &e) {
LOG_WARN("Load recent connection aliases failed: {}", e.what());
return -1;
}
return 0;
}
int SettingsManager::SaveRecentConnectionAliases() const {
std::error_code ec;
std::filesystem::create_directories(owner_.cache_path_, ec);
if (ec) {
LOG_WARN("Create cache directory failed while saving aliases: {}",
ec.message());
return -1;
}
nlohmann::json alias_json;
alias_json["aliases"] = nlohmann::json::object();
for (const auto &[remote_id, alias] : recent_connection_aliases_) {
if (!remote_id.empty() && !alias.empty()) {
alias_json["aliases"][remote_id] = alias;
}
}
std::ofstream alias_file(
owner_.cache_path_ + "/recent_connection_aliases.json", std::ios::trunc);
if (!alias_file.good()) {
LOG_WARN("Open recent connection alias file failed");
return -1;
}
alias_file << alias_json.dump(2);
return 0;
}
std::string SettingsManager::RecentConnectionDisplayName(
const Thumbnail::RecentConnection &connection) const {
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
if (alias_it != recent_connection_aliases_.end() &&
!alias_it->second.empty()) {
return alias_it->second;
}
if (!connection.remote_host_name.empty() &&
connection.remote_host_name != "unknown") {
return connection.remote_host_name;
}
return connection.remote_id;
}
void SettingsManager::BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection &connection) {
owner_.edit_connection_alias_remote_id_ = connection.remote_id;
std::memset(owner_.edit_connection_alias_, 0,
sizeof(owner_.edit_connection_alias_));
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
const std::string alias = alias_it != recent_connection_aliases_.end()
? alias_it->second
: RecentConnectionDisplayName(connection);
CopyString(owner_.edit_connection_alias_, alias.c_str());
owner_.focus_on_input_widget_ = true;
owner_.show_edit_connection_alias_window_ = true;
}
void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias) {
if (!remote_id.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
void SettingsManager::EraseRecentConnectionAlias(const std::string &remote_id) {
recent_connection_aliases_.erase(remote_id);
}
} // namespace crossdesk
@@ -0,0 +1,114 @@
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include "thumbnail.h"
namespace crossdesk {
class GuiRuntime;
// Owns persistent GUI settings and recent-connection aliases. GuiRuntime keeps
// only the runtime/UI state that consumes these values.
class SettingsManager {
public:
explicit SettingsManager(GuiRuntime &owner);
int Save();
int Load();
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
std::string RecentConnectionDisplayName(
const Thumbnail::RecentConnection &connection) const;
void
BeginEditRecentConnectionAlias(const Thumbnail::RecentConnection &connection);
void SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias);
void EraseRecentConnectionAlias(const std::string &remote_id);
// Loads the cached self-hosted identity into the owner's active connection
// fields. Returns true only when a non-empty identity was restored.
bool LoadCachedSelfHostedIdentity();
// Restores the public-server identity kept in the in-memory cache fields.
// This must run when switching away from a self-hosted server so signal
// callbacks are matched against the identity used by the replacement peer.
bool ActivateCachedPublicIdentity();
void PersistSelfHostedIdentity(const char *client_id);
// Password rotation is deliberately persisted in two phases. The active
// credential remains usable while the pending credential records the value
// that may already have been committed by the server. On the next launch the
// caller can try the pending credential first and safely fall back to the
// active credential when the request never reached the server.
bool StagePendingPasswordChange(const std::string &identity,
bool self_hosted,
const std::string &server_host,
int server_port,
const std::string &request_id);
std::string PendingPasswordChangeIdentity(
bool self_hosted, const std::string &server_host,
int server_port) const;
bool PromotePendingPasswordChange();
bool ClearPendingPasswordChange();
private:
struct CacheV1 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
};
struct CacheV2 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
char self_hosted_id[17];
};
struct CacheV3 {
uint32_t magic;
uint32_t version;
CacheV2 base;
char pending_identity[17];
char pending_server_host[256];
int pending_server_port;
bool pending_self_hosted;
char pending_request_id[64];
uint32_t checksum;
};
int SaveLocked();
bool ReadV3Locked();
bool ReadV2Locked();
void ActivateIdentity(const char *identity, bool self_hosted);
GuiRuntime &owner_;
CacheV1 cache_v1_{};
CacheV2 cache_v2_{};
CacheV3 cache_v3_{};
mutable std::mutex cache_mutex_;
std::unordered_map<std::string, std::string> recent_connection_aliases_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_SETTINGS_MANAGER_H_
+15
View File
@@ -0,0 +1,15 @@
#include "render.h"
#include <memory>
#include "application/gui_application.h"
namespace crossdesk {
Render::Render() : application_(std::make_unique<GuiApplication>()) {}
Render::~Render() = default;
int Render::Run() { return application_->Run(); }
} // namespace crossdesk
+28
View File
@@ -0,0 +1,28 @@
#ifndef CROSSDESK_GUI_RENDER_H_
#define CROSSDESK_GUI_RENDER_H_
#include <memory>
namespace crossdesk {
class GuiApplication;
// Stable application-facing facade. The SDL application and feature
// controllers remain private implementation details.
class Render {
public:
Render();
~Render();
Render(const Render &) = delete;
Render &operator=(const Render &) = delete;
int Run();
private:
std::unique_ptr<GuiApplication> application_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDER_H_
@@ -0,0 +1,502 @@
#include "rendering/slint_video_presenter.h"
#include <libyuv.h>
#include <slint.h>
#include <algorithm>
#include <cmath>
#include <mutex>
#include <unordered_map>
#include <utility>
#include "nv12_scaler.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#if defined(__APPLE__)
#include "platform/macos/gui/metal_video_renderer.h"
#else
#if defined(_WIN32)
#include <windows.h>
#endif
#include <GL/gl.h>
#include "platform/common/gui/opengl_video_renderer.h"
#endif
namespace crossdesk {
namespace {
#if !defined(__APPLE__)
constexpr GLint kGlClampToEdge = 0x812F;
constexpr float kStreamWindowCornerRadius = 12.0f;
#else
constexpr int kMetalAttachmentAttemptLimit = 30;
#endif
struct VideoRenderSize {
int width = 0;
int height = 0;
bool operator==(const VideoRenderSize&) const = default;
};
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
int area_width, int area_height) {
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
area_height <= 0) {
return {};
}
const double scale =
std::min({1.0, static_cast<double>(area_width) / source_width,
static_cast<double>(area_height) / source_height});
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
width &= ~1;
height &= ~1;
return {std::min(width, source_width & ~1),
std::min(height, source_height & ~1)};
}
} // namespace
struct SlintVideoPresenter::Impl {
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
VideoRenderer& renderer;
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
StateProvider state_provider;
DirtyCallback dirty_callback;
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
std::vector<uint8_t> scaled_video_frame;
std::vector<uint8_t> scaled_video_frame_scratch;
#if defined(__APPLE__)
int attachment_attempts = 0;
bool skip_attachment_once = false;
#else
std::mutex gl_mutex;
std::vector<uint8_t> gl_conversion_frame;
std::vector<uint8_t> gl_pending_frame;
uint32_t gl_texture = 0;
VideoRenderSize gl_texture_size;
VideoRenderSize gl_image_size;
VideoRenderSize gl_pending_size;
bool gl_pending_dirty = false;
#endif
SurfaceState CurrentState() const {
return state_provider ? state_provider() : SurfaceState{};
}
void MarkDirty() const {
if (dirty_callback) {
dirty_callback();
}
}
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
if (remote_id.empty() || sequence == 0 ||
displayed_frame_sequence[remote_id] == sequence) {
return false;
}
displayed_frame_sequence[remote_id] = sequence;
return true;
}
#if !defined(__APPLE__)
void ConfigureOpenGlNotifier() {
if (!stream) {
return;
}
const auto error = (*stream)->window().set_rendering_notifier(
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
return;
}
std::lock_guard lock(gl_mutex);
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
if (state == slint::RenderingState::RenderingSetup) {
GLuint texture = 0;
glGenTextures(1, &texture);
if (texture == 0) {
return;
}
GLint previous_texture = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_texture = texture;
if (opengl_renderer && opengl_renderer->Setup()) {
const std::string selected_stream =
CurrentState().selected_stream;
opengl_renderer->SetSelectedStream(selected_stream);
seeded_frame_sequence.erase(selected_stream);
MarkDirty();
}
return;
}
if (state == slint::RenderingState::BeforeRendering && stream &&
opengl_renderer && opengl_renderer->IsReady() &&
(*stream)->get_native_video_enabled()) {
const SurfaceState surface = CurrentState();
const auto window_size = (*stream)->window().size();
const float scale_factor =
std::max(1.0f, (*stream)->window().scale_factor());
const int top_inset =
!surface.fullscreen && surface.tab_count > 1
? static_cast<int>(std::lround(30.0f * scale_factor))
: 0;
const bool rounded_window = (*stream)->get_custom_titlebar() &&
!surface.fullscreen &&
!(*stream)->window().is_maximized();
const int corner_radius =
rounded_window ? static_cast<int>(std::lround(
kStreamWindowCornerRadius * scale_factor))
: 0;
opengl_renderer->RenderLatest(
surface.selected_stream, static_cast<int>(window_size.width),
static_cast<int>(window_size.height), top_inset, corner_radius);
return;
}
if (state == slint::RenderingState::BeforeRendering &&
gl_texture != 0 && gl_pending_dirty &&
!gl_pending_frame.empty()) {
GLint previous_texture = 0;
GLint previous_unpack_alignment = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, gl_texture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
if (gl_texture_size != gl_pending_size) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
gl_texture_size = gl_pending_size;
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
}
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_pending_dirty = false;
return;
}
if (state == slint::RenderingState::RenderingTeardown) {
if (opengl_renderer) {
opengl_renderer->Teardown();
}
if (gl_texture != 0) {
const GLuint texture = gl_texture;
glDeleteTextures(1, &texture);
}
gl_texture = 0;
gl_texture_size = {};
gl_image_size = {};
gl_pending_size = {};
gl_pending_dirty = false;
gl_pending_frame.clear();
}
});
if (error.has_value()) {
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
} else {
renderer.SetSelectedStream(CurrentState().selected_stream);
}
}
#endif
};
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
: impl_(std::make_unique<Impl>(renderer)) {}
SlintVideoPresenter::~SlintVideoPresenter() = default;
void SlintVideoPresenter::PrepareWindow(
slint::ComponentHandle<ui::StreamWindow>& stream,
StateProvider state_provider, DirtyCallback dirty_callback) {
impl_->stream = &stream;
impl_->state_provider = std::move(state_provider);
impl_->dirty_callback = std::move(dirty_callback);
#if defined(__APPLE__)
stream->set_native_video_enabled(false);
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
impl_->skip_attachment_once = true;
#else
impl_->ConfigureOpenGlNotifier();
#endif
}
bool SlintVideoPresenter::EnsureAttached() {
#if defined(__APPLE__)
if (!impl_->stream || impl_->attachment_attempts <= 0) {
return false;
}
if (impl_->skip_attachment_once) {
impl_->skip_attachment_once = false;
return false;
}
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (!metal_renderer) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (!metal_renderer->IsReady()) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (metal_renderer->IsAttached()) {
impl_->attachment_attempts = 0;
return false;
}
if (void* view = (*impl_->stream)->window().appkit_view()) {
(*impl_->stream)->set_native_video_enabled(true);
if (metal_renderer->Attach(view)) {
impl_->attachment_attempts = 0;
const SurfaceState surface = impl_->CurrentState();
impl_->displayed_frame_sequence.erase(surface.selected_stream);
impl_->seeded_frame_sequence.erase(surface.selected_stream);
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
impl_->MarkDirty();
}
return true;
}
(*impl_->stream)->set_native_video_enabled(false);
}
if (--impl_->attachment_attempts == 0) {
(*impl_->stream)->set_native_video_enabled(false);
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
}
#endif
return false;
}
bool SlintVideoPresenter::NeedsRedraw() const {
#if defined(__APPLE__)
const auto* metal_renderer =
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
return metal_renderer && metal_renderer->IsActive() &&
metal_renderer->NeedsSurfaceRedraw();
#else
return false;
#endif
}
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
if (changed && !remote_id.empty()) {
impl_->seeded_frame_sequence.erase(remote_id);
}
return changed;
}
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
const Frame& frame) {
PresentResult result;
if (!impl_->stream || frame.remote_id.empty()) {
return result;
}
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
frame.height > 0 && frame.sequence > 0 &&
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
const auto submit_result = impl_->renderer.SubmitCachedNv12(
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
frame.height);
if (submit_result == VideoRenderer::SubmitResult::submitted ||
submit_result == VideoRenderer::SubmitResult::dropped) {
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
}
}
#if defined(__APPLE__)
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (metal_renderer && metal_renderer->IsActive()) {
const SurfaceState surface = impl_->CurrentState();
const double top_inset =
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
const auto outcome = metal_renderer->RenderLatest(
frame.remote_id, top_inset, !surface.fullscreen);
if (outcome.result == VideoRenderer::RenderResult::rendered) {
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = outcome.width;
result.height = outcome.height;
result.new_frame =
impl_->MarkPresented(frame.remote_id, outcome.sequence);
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
(*impl_->stream)->set_has_frame(false);
}
return result;
}
#else
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
if (opengl_renderer && opengl_renderer->IsReady()) {
opengl_renderer->SetSelectedStream(frame.remote_id);
if (!(*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(true);
}
const bool has_frame =
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
(*impl_->stream)->set_has_frame(has_frame);
if (has_frame) {
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
}
(*impl_->stream)->window().request_redraw();
return result;
}
if ((*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(false);
}
#endif
const size_t nv12_size =
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
frame.nv12->size() < nv12_size) {
(*impl_->stream)->set_has_frame(false);
return result;
}
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
return result;
}
const auto window_size = (*impl_->stream)->window().size();
const VideoRenderSize render_size = FitVideoToRenderArea(
frame.width, frame.height, static_cast<int>(window_size.width),
static_cast<int>(window_size.height));
const uint8_t* y_plane = frame.nv12->data();
const uint8_t* uv_plane =
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
int output_width = frame.width;
int output_height = frame.height;
if (render_size.width > 0 && render_size.height > 0 &&
(render_size.width != frame.width ||
render_size.height != frame.height)) {
const size_t scaled_nv12_size =
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
impl_->scaled_video_frame.resize(scaled_nv12_size);
uint8_t* scaled_y = impl_->scaled_video_frame.data();
uint8_t* scaled_uv =
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
frame.width, frame.height, scaled_y, render_size.width,
scaled_uv, render_size.width, render_size.width,
render_size.height, libyuv::kFilterBox,
&impl_->scaled_video_frame_scratch) == 0) {
y_plane = scaled_y;
uv_plane = scaled_uv;
output_width = render_size.width;
output_height = render_size.height;
}
}
#if !defined(__APPLE__)
uint32_t texture_id = 0;
{
std::lock_guard lock(impl_->gl_mutex);
texture_id = impl_->gl_texture;
}
if (texture_id != 0) {
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
output_height * 4);
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
impl_->gl_conversion_frame.data(), output_width * 4,
output_width, output_height) != 0) {
return result;
}
{
std::lock_guard lock(impl_->gl_mutex);
if (impl_->gl_texture == 0) {
return result;
}
texture_id = impl_->gl_texture;
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
impl_->gl_pending_size = {output_width, output_height};
impl_->gl_pending_dirty = true;
}
const VideoRenderSize output_size{output_width, output_height};
if (impl_->gl_image_size != output_size) {
(*impl_->stream)
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
texture_id,
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
static_cast<uint32_t>(output_height)}));
impl_->gl_image_size = output_size;
}
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
(*impl_->stream)->window().request_redraw();
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
#endif
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
output_height);
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
reinterpret_cast<uint8_t*>(pixels.begin()),
output_width * 3, output_width, output_height) != 0) {
return result;
}
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
impl_->displayed_frame_sequence.erase(remote_id);
impl_->seeded_frame_sequence.erase(remote_id);
}
void SlintVideoPresenter::Detach() {
impl_->renderer.SetSelectedStream({});
#if defined(__APPLE__)
if (auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
metal_renderer->Detach();
}
impl_->attachment_attempts = 0;
impl_->skip_attachment_once = false;
#endif
impl_->stream = nullptr;
impl_->state_provider = {};
impl_->dirty_callback = {};
}
} // namespace crossdesk
@@ -0,0 +1,75 @@
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "crossdesk_ui.h"
namespace crossdesk {
class VideoRenderer;
// Owns the bridge between decoded NV12 frames, the selected native graphics
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
// details are intentionally contained in the implementation file.
class SlintVideoPresenter {
public:
struct SurfaceState {
std::string selected_stream;
bool fullscreen = false;
size_t tab_count = 0;
};
struct Frame {
std::string remote_id;
std::shared_ptr<std::vector<unsigned char>> nv12;
int width = 0;
int height = 0;
uint64_t sequence = 0;
};
struct PresentResult {
int width = 0;
int height = 0;
bool new_frame = false;
};
using StateProvider = std::function<SurfaceState()>;
using DirtyCallback = std::function<void()>;
explicit SlintVideoPresenter(VideoRenderer& renderer);
~SlintVideoPresenter();
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
// Called before the window is shown. OpenGL installs Slint's rendering
// notifier here; macOS deliberately defers native view attachment until a
// later UI pass, after AppKit has committed the ordinary window style.
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
StateProvider state_provider,
DirtyCallback dirty_callback);
// Returns true only when a deferred native surface became attached during
// this call, allowing the caller to seed it from a cached frame once.
bool EnsureAttached();
bool NeedsRedraw() const;
bool SelectStream(std::string remote_id);
PresentResult Present(const Frame& frame);
void ForgetStream(const std::string& remote_id);
void Detach();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
@@ -0,0 +1,477 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include <display_stream_id.h>
#include "localization.h"
#include "platform.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
} // namespace
void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
if (!recent_connection_ids_.empty()) {
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto& id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
size_t pos = std::string::npos;
if (pos_y != std::string::npos &&
(pos_n == std::string::npos || pos_y < pos_n)) {
pos = pos_y;
} else if (pos_n != std::string::npos) {
pos = pos_n;
}
if (pos != std::string::npos) {
pure_id = pure_id.substr(0, pos);
}
j["devices"].push_back(pure_id);
}
auto s = j.dump();
SendSignalMessage(peer_, s.data(), s.size());
}
}
need_to_send_recent_connections_ = false;
}
void GuiRuntime::HandlePendingPresenceProbe() {
bool has_action = false;
bool should_connect = false;
bool remember_password = false;
std::string remote_id;
std::string password;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (!pending_presence_probe_ || !pending_presence_result_ready_) {
return;
}
has_action = true;
should_connect = pending_presence_online_;
remote_id = pending_presence_remote_id_;
password = pending_presence_password_;
remember_password = pending_presence_remember_password_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
if (!has_action) {
return;
}
if (should_connect) {
ConnectTo(remote_id, password.c_str(), remember_password, true);
return;
}
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
}
void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false;
std::string presence_remote_id;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (pending_presence_probe_ && !pending_presence_result_ready_ &&
now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) {
presence_probe_timed_out = true;
presence_remote_id = pending_presence_remote_id_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
}
if (presence_probe_timed_out) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
}
}
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
{
std::lock_guard lock(remote_pointer_input_mutex_);
last_remote_pointer_input_time_.erase(remote_id);
}
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
connection_host_names_.erase(remote_id);
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) {
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
}
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
start_screen_capturer_ = true;
LOG_INFO(
"Keeping Wayland screen capturer running after disconnect to "
"preserve reconnect stability");
} else {
start_screen_capturer_ = false;
}
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
devices_.ResetToInitialDisplay();
}
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) {
if (!signal_connected_ || !peer_) {
return -1;
}
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = true;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_probe_started_at_ = std::chrono::steady_clock::now();
pending_presence_remote_id_ = remote_id;
pending_presence_password_ = password ? password : "";
pending_presence_remember_password_ = remember_password;
}
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array({remote_id});
auto s = j.dump();
int ret = SendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
return ret;
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
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()) &&
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
frame_snapshot = props->thumbnail_frame_;
video_width = props->thumbnail_width_;
video_height = props->thumbnail_height_;
}
}
auto* native_renderer = video_renderer_.get();
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer->CopyLatestNv12(props->remote_id_,
native_snapshot.get(), &video_width,
&video_height)) {
frame_snapshot = std::move(native_snapshot);
}
}
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 =
props->remember_password_ ? props->remote_password_ : "";
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
thumbnail_save_threads_.emplace_back(std::move(save_thread));
}
}
if (native_renderer) {
native_renderer->DiscardStream(props->remote_id_);
video_frame_dirty_.store(true, std::memory_order_release);
}
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
LeaveConnection(props->peer_, props->remote_id_.c_str());
LOG_INFO("Destroy peer [{}]", props->local_id_);
DestroyPeer(&props->peer_);
}
}
void GuiRuntime::CloseAllRemoteSessions() {
if (peer_) {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto& it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
}
void GuiRuntime::WaitForThumbnailSaveTasks() {
std::vector<std::thread> threads_to_join;
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
threads_to_join.swap(thumbnail_save_threads_);
}
if (threads_to_join.empty()) {
return;
}
for (auto& thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
}
}
}
void GuiRuntime::ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props) {
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
{
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
}
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
const std::string& remote_id) {
if (remote_id.empty()) {
return nullptr;
}
std::shared_lock lock(remote_sessions_mutex_);
auto it = remote_sessions_.find(remote_id);
if (it == remote_sessions_.end()) {
return nullptr;
}
return it->second;
}
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
PeerPtr* peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 10.0f;
props->control_window_height_ = title_bar_height_ * 1.3f;
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
return -1;
}
const auto& displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index);
AddVideoStream(props->peer_, stream_id.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_.store(ConnectionStatus::Connecting);
peer_to_init = props->peer_;
local_id = props->local_id_;
}
}
if (peer_to_init) {
LOG_INFO("[{}] Create peer instance successful", local_id);
Init(peer_to_init);
LOG_INFO("[{}] Peer init finish", local_id);
}
}
int ret = -1;
// std::shared_lock read_lock(remote_sessions_mutex_);
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
props->remember_password_ = remember_password;
if (strcmp(password, "") != 0 &&
strcmp(password, props->remote_password_) != 0) {
strncpy(props->remote_password_, password,
sizeof(props->remote_password_) - 1);
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
}
std::string remote_id_with_pwd = remote_id + "@" + password;
if (props->peer_) {
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
}
}
}
// read_lock.unlock();
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,67 @@
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
#include <algorithm>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
inline void ResetCursorPosition(CursorState* state) {
if (!state) return;
state->position_update = true;
state->position_valid = false;
state->x = 0.5f;
state->y = 0.5f;
state->visual_offset_x = 0.0f;
state->visual_offset_y = 0.0f;
state->display_id = -1;
}
inline bool NormalizeCursorPosition(
double screen_x, double screen_y,
const std::vector<DisplayInfo>& displays, int preferred_display,
CursorState* state) {
if (!state) return false;
ResetCursorPosition(state);
auto contains = [&](int index) {
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
const auto& display = displays[index];
return display.width > 0 && display.height > 0 &&
screen_x >= display.left && screen_x < display.right &&
screen_y >= display.top && screen_y < display.bottom;
};
int display_id = contains(preferred_display) ? preferred_display : -1;
if (display_id < 0) {
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
if (contains(index)) {
display_id = index;
break;
}
}
}
if (display_id < 0) return false;
const auto& display = displays[display_id];
// Screen coordinates describe a continuous rectangle with an exclusive
// right/bottom edge. Use its full extent so feedback is the exact inverse of
// normalized input instead of accumulating a one-pixel edge convention.
const double horizontal_extent = std::max(display.width, 1);
const double vertical_extent = std::max(display.height, 1);
state->position_valid = true;
state->x = static_cast<float>(std::clamp(
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
state->y = static_cast<float>(std::clamp(
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
state->display_id = display_id;
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,136 @@
#include "runtime/cursor_state_provider.h"
#include <remote_action.h>
#if defined(_WIN32)
#include <windows.h>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
preferred_display, state);
return true;
}
} // namespace crossdesk
#elif defined(__linux__) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <string>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
for (size_t index = 0; index < pixel_count; ++index) {
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
}
return false;
}
} // namespace
struct CursorStateProvider::Impl {
Display* display = XOpenDisplay(nullptr);
~Impl() {
if (display) XCloseDisplay(display);
}
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state || !impl_) return false;
ResetCursorPosition(state);
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
}
}
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
if (!image) return false;
const std::string name = image->name ? image->name : "";
state->seq = 0;
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
state);
XFree(image);
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,33 @@
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
#include <memory>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
// Samples the cursor that is currently displayed by the controlled desktop
// and converts platform-specific cursor handles to protocol cursor shapes.
class CursorStateProvider {
public:
CursorStateProvider();
~CursorStateProvider();
CursorStateProvider(const CursorStateProvider&) = delete;
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
bool Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif
@@ -0,0 +1,41 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#include <mutex>
#include <string>
#include <unordered_map>
namespace crossdesk {
class DevicePresenceCache {
public:
void SetOnline(const std::string &device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string &device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = cache_.find(device_id);
return it != cache_.end() && it->second;
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
@@ -0,0 +1,300 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include <display_stream_id.h>
#include "localization.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
namespace crossdesk {
GuiRuntime::GuiRuntime()
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
keyboard_(*this), peer_events_(*this) {}
GuiRuntime::~GuiRuntime() = default;
int GuiRuntime::CreateConnectionPeer() {
params_.use_cfg_file = false;
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity();
if (!use_cached_id) {
LOG_INFO(
"secure_cache_v2.enc not found, will use empty id to get new id from "
"server");
}
if (use_cached_id && strlen(self_hosted_id_) > 0) {
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
strncpy(self_hosted_user_id_, self_hosted_id_,
sizeof(self_hosted_user_id_) - 1);
self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0';
params_.user_id = self_hosted_user_id_;
} else {
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
params_.user_id = self_hosted_user_id_;
LOG_INFO(
"Using empty id for self-hosted server, server will assign new id");
}
} else {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
settings_.ActivateCachedPublicIdentity();
params_.user_id = client_id_with_password_;
}
const bool self_hosted = config_center_->IsSelfHosted();
const std::string pending_identity =
settings_.PendingPasswordChangeIdentity(
self_hosted, signal_server_ip, signal_server_port);
bool try_pending_identity = false;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
if (pending_identity.empty()) {
credential_recovery_in_progress_ = false;
credential_recovery_attempt_pending_ = false;
credential_recovery_retry_active_ = false;
credential_recovery_promote_pending_ = false;
credential_recovery_clear_pending_ = false;
} else if (!credential_recovery_in_progress_) {
credential_recovery_in_progress_ = true;
credential_recovery_attempt_pending_ = true;
LOG_INFO("Recovering an interrupted password change for [{}]",
client_id_);
}
try_pending_identity = credential_recovery_in_progress_ &&
credential_recovery_attempt_pending_ &&
!pending_identity.empty();
}
const char *active_identity =
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
const std::string login_identity =
try_pending_identity ? pending_identity : std::string(active_identity);
std::memset(connection_login_identity_, 0,
sizeof(connection_login_identity_));
std::strncpy(connection_login_identity_, login_identity.c_str(),
sizeof(connection_login_identity_) - 1);
params_.user_id = connection_login_identity_;
// self hosted server config
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
int signal_port = config_center_->GetSignalServerPort();
if (signal_port > 0) {
strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(),
sizeof(signal_server_port_self_) - 1);
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
} else {
signal_server_port_self_[0] = '\0';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_server_port_self_[0] = '\0';
}
// peer config
strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(),
sizeof(params_.signal_server_ip) - 1);
params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0';
params_.signal_server_port = signal_server_port;
strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(),
sizeof(params_.stun_server_ip) - 1);
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
params_.stun_server_port = coturn_server_port;
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
sizeof(params_.turn_server_ip) - 1);
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
params_.turn_server_port = coturn_server_port;
// TURN credentials are issued by the signaling server after login. Keep the
// initial values empty so a reusable static password is never embedded in
// the client binary.
params_.turn_server_username[0] = '\0';
params_.turn_server_password[0] = '\0';
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
// The Slint desktop renderer currently consumes packed CPU frames. Native
// renderers can opt into platform-native frame output independently.
params_.native_video_output = false;
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
? true
: false;
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_content_type = VideoContentType::ScreenContent;
params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality());
params_.video_frame_rate =
config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
params_.video_degradation_preference =
VideoDegradationPreference::MaintainFrameRate;
params_.on_receive_video_buffer = nullptr;
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
params_.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer;
params_.on_signal_status = PeerEventHandler::OnSignalStatus;
params_.on_signal_message = PeerEventHandler::OnSignalMessage;
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
params_.user_data = &peer_events_;
// The previous peer may have left a terminal status behind. Reset it before
// Init() starts emitting callbacks for the newly selected server.
signal_connected_ = false;
signal_status_ = SignalStatus::SignalConnecting;
peer_ = CreatePeer(&params_);
if (peer_) {
LOG_INFO("Create peer instance [{}] successful", client_id_);
Init(peer_);
LOG_INFO("Peer [{}] init finish", client_id_);
} else {
LOG_INFO("Create peer [{}] instance failed", client_id_);
}
if (0 == devices_.InitializeScreenCapturer()) {
const auto &displays = devices_.display_info_list();
for (size_t index = 0; index < displays.size(); ++index) {
const std::string stream_id = MakeDisplayStreamId(index);
AddVideoStream(peer_, stream_id.c_str());
}
AddAudioStream(peer_, audio_label_.c_str());
AddDataStream(peer_, data_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), false);
AddDataStream(peer_, keyboard_label_.c_str(), true);
AddDataStream(peer_, control_data_label_.c_str(), true);
AddDataStream(peer_, file_label_.c_str(), true);
AddDataStream(peer_, file_feedback_label_.c_str(), true);
AddDataStream(peer_, clipboard_label_.c_str(), true);
return 0;
} else {
return -1;
}
}
void GuiRuntime::UpdateLabels() {
if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) {
connect_button_label_ =
connect_button_pressed_
? localization::disconnect[localization_language_index_]
: localization::connect[localization_language_index_];
label_inited_ = true;
localization_language_index_last_ = localization_language_index_;
}
}
void GuiRuntime::HandleRecentConnections() {
if (reload_recent_connections_ && thumbnail_) {
uint32_t now_time = SDL_GetTicks();
if (now_time - recent_connection_image_save_time_ >= 50) {
int ret = thumbnail_->LoadThumbnail(recent_connections_,
&recent_connection_image_width_,
&recent_connection_image_height_);
if (!ret) {
LOG_INFO("Load recent connection thumbnails");
}
reload_recent_connections_ = false;
recent_connection_ids_.clear();
for (const auto &conn : recent_connections_) {
recent_connection_ids_.push_back(conn.first);
}
need_to_send_recent_connections_ = true;
}
}
}
void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
GuiRuntime *runtime = static_cast<GuiRuntime *>(userdata);
if (!runtime) {
return;
}
if (1) {
std::shared_lock lock(runtime->remote_sessions_mutex_);
for (const auto &it : runtime->remote_sessions_) {
auto props = it.second;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
SendAudioFrame(props->peer_, (const char *)stream, len,
runtime->audio_label_.c_str());
}
}
}
} else {
memcpy(runtime->audio_buffer_, stream, len);
runtime->audio_len_ = len;
SDL_Delay(10);
runtime->audio_buffer_fresh_ = true;
}
}
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
[[maybe_unused]] Uint8 *stream,
[[maybe_unused]] int len) {
// GuiApplication *runtime = (GuiApplication *)userdata;
// for (auto it : runtime->remote_sessions_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!runtime->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (runtime->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// runtime->audio_buffer_fresh_ = false;
}
} // namespace crossdesk
+101
View File
@@ -0,0 +1,101 @@
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
#include <atomic>
#include <memory>
#include <string>
#include "features/clipboard/clipboard_controller.h"
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
namespace crossdesk {
class VideoRenderer;
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState {
protected:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
enum class RemoteUnlockState {
none,
service_unavailable,
lock_screen,
credential_ui,
secure_desktop,
};
GuiRuntime();
~GuiRuntime();
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
int CreateConnectionPeer();
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
void UpdateLabels();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status);
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
#ifdef __APPLE__
bool CheckScreenRecordingPermission();
bool CheckAccessibilityPermission();
void OpenScreenRecordingPreferences();
void OpenAccessibilityPreferences();
void RefreshMacPermissionStatus(bool force);
bool EnsureMacScreenRecordingPermission();
bool EnsureMacAccessibilityPermission();
#endif
ClipboardController clipboard_;
SessionDeviceManager devices_;
FileTransferManager transfers_;
SettingsManager settings_;
KeyboardController keyboard_;
PeerEventHandler peer_events_;
std::atomic<bool> video_frame_dirty_{false};
std::unique_ptr<VideoRenderer> video_renderer_;
private:
friend class ClipboardController;
friend class SessionDeviceManager;
friend class FileTransferManager;
friend class SettingsManager;
friend class KeyboardController;
friend class PeerEventHandler;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
+21
View File
@@ -0,0 +1,21 @@
/*
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
*/
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
namespace crossdesk::gui_detail {
struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_STATE_H_
@@ -0,0 +1,319 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "filesystem_utf8.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo> &displays,
int display_index, float normalized_x,
float normalized_y, int *absolute_x_out,
int *absolute_y_out) {
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
return false;
}
const DisplayInfo &display = displays[display_index];
if (display.width <= 0 || display.height <= 0) {
return false;
}
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
return true;
}
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
const char *stage) {
if (last_tick == nullptr) {
return;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
}
*last_tick = now;
LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path "
"cannot drive the Windows password UI",
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
}
#endif
} // namespace
void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string source_id = std::string(src_id, src_id_size);
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
// Update output directory from config
std::string configured_path =
runtime->config_center_->GetFileTransferSavePath();
if (!configured_path.empty()) {
receiver.SetOutputDir(PathFromUtf8(configured_path));
} else if (receiver.OutputDir().empty()) {
receiver = FileReceiver(); // re-init with default desktop path
}
receiver.SetOnSendAck([runtime,
remote_user_id](const FileTransferAck &ack) -> int {
const auto encoded_ack = EncodeFileTransferAck(ack);
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props) {
PeerPtr *peer = props->peer_;
return SendReliableDataFrame(
peer, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
});
receiver.OnData(data, size);
return;
} else if (source_id == runtime->clipboard_label_) {
if (size > 0) {
std::string remote_user_id(user_id, user_id_size);
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props && !props->enable_mouse_control_) {
return;
}
runtime->clipboard_.QueueRemoteText(data, size);
}
return;
} else if (source_id == runtime->file_feedback_label_) {
runtime->transfers_.HandleAck(data, size);
return;
}
std::string json_str(data, size);
RemoteAction remote_action{};
if (!remote_action.from_json(json_str)) {
LOG_ERROR("Failed to parse RemoteAction JSON payload");
return;
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::service_status) {
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
}
return;
}
if (remote_action.type == ControlType::service_command) {
#if _WIN32
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
runtime->pending_windows_service_sas_.store(true,
std::memory_order_relaxed);
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
if (!LockWorkStation()) {
LOG_WARN("Remote lock workstation request failed, error={}",
GetLastError());
}
}
#endif
return;
}
if (remote_action.type == ControlType::cursor_state) {
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
props = props_it->second;
}
}
if (!props) {
return;
}
std::lock_guard lock(props->remote_cursor_state_mutex_);
const uint32_t previous_seq = props->remote_cursor_state_.seq;
const bool is_newer =
!props->remote_cursor_state_received_ ||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
if (is_newer) {
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
CursorState merged = remote_action.cs;
if (!remote_action.cs.position_update &&
props->remote_cursor_state_received_) {
merged.position_valid = props->remote_cursor_state_.position_valid;
merged.x = props->remote_cursor_state_.x;
merged.y = props->remote_cursor_state_.y;
merged.display_id = props->remote_cursor_state_.display_id;
}
props->remote_cursor_state_ = merged;
props->remote_cursor_state_received_ = true;
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
}
}
return;
}
if (remote_action.type == ControlType::host_infomation) {
bool is_client_mode = false;
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
is_client_mode = true;
props = props_it->second;
}
}
if (is_client_mode) {
// client mode
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]));
}
}
remote_action_codec::Free(remote_action);
} else {
// server mode
std::string host_name(remote_action.i.host_name,
remote_action.i.host_name_size);
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
remote_action_codec::Free(remote_action);
}
} else {
// remote
if (runtime->is_server_mode_ &&
remote_action.type == ControlType::mouse) {
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
runtime->last_remote_pointer_input_time_[remote_id] =
std::chrono::steady_clock::now();
}
#if _WIN32
if (runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) {
int absolute_x = 0;
int absolute_y = 0;
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
runtime->selected_display_,
remote_action.m.x, remote_action.m.y,
&absolute_x, &absolute_y)) {
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
remote_action.m.y);
return;
}
const std::string response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
runtime->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
"flag={}, response={}",
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), response);
}
return;
}
}
#endif
if (remote_action.type == ControlType::mouse) {
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !runtime->start_speaker_capturer_)
runtime->devices_.StartSpeakerCapturer();
else if (!remote_action.a && runtime->start_speaker_capturer_)
runtime->devices_.StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard) {
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
} else if (remote_action.type == ControlType::keyboard_state) {
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
} else if (remote_action.type == ControlType::display_id) {
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
if (ret == 0) {
runtime->selected_display_ = remote_action.d;
} else {
LOG_WARN("Display switch skipped, invalid display_id={}",
remote_action.d);
}
}
}
}
} // namespace crossdesk
@@ -0,0 +1,579 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
const std::string& client_id) {
if (!peer) {
return;
}
#if defined(_WIN32)
constexpr const char* kClientPlatform = "windows";
#elif defined(__APPLE__)
constexpr const char* kClientPlatform = "macos";
#elif defined(__linux__)
constexpr const char* kClientPlatform = "linux";
#else
constexpr const char* kClientPlatform = "unknown";
#endif
const nlohmann::json message = {{"type", "client_info"},
{"version", CROSSDESK_VERSION},
{"platform", kClientPlatform}};
const std::string payload = message.dump();
if (SendSignalMessage(peer, payload.data(), payload.size()) != 0) {
LOG_WARN("[{}] failed to report client information", client_id);
}
}
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
}
std::string s(message, size);
auto j = nlohmann::json::parse(s, nullptr, false);
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
return;
}
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto& dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
if (!dev.contains("id") || !dev["id"].is_string()) {
continue;
}
if (!dev.contains("online") || !dev["online"].is_boolean()) {
continue;
}
std::string id = dev["id"].get<std::string>();
bool online = dev["online"].get<bool>();
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
} else if (type == "presence_update") {
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
j["online"].is_boolean()) {
std::string id = j["id"].get<std::string>();
bool online = j["online"].get<bool>();
if (!id.empty()) {
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
} else if (type == "change_password") {
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (!runtime->password_change_pending_) {
LOG_WARN("Ignore unexpected password change response");
return;
}
if (!j.contains("request_id") || !j["request_id"].is_string() ||
j["request_id"].get<std::string>() !=
runtime->pending_password_change_request_id_) {
LOG_WARN("Ignore password change response with unexpected request id");
return;
}
if (j.contains("user_id") && j["user_id"].is_string()) {
const std::string response_user_id = j["user_id"].get<std::string>();
if (!response_user_id.empty() &&
response_user_id != runtime->client_id_) {
LOG_WARN("Ignore password change response for unexpected id [{}]",
response_user_id);
return;
}
}
runtime->password_change_succeeded_ =
j.contains("status") && j["status"].is_string() &&
j["status"].get<std::string>() == "success";
runtime->password_change_error_ =
j.contains("reason") && j["reason"].is_string()
? j["reason"].get<std::string>()
: "Password change failed";
runtime->password_change_result_uncertain_ = false;
runtime->password_change_result_ready_ = true;
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string client_id(user_id, user_id_size);
if (client_id == runtime->client_id_) {
runtime->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
runtime->signal_connected_ = true;
runtime->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id);
SendClientInfo(runtime->peer_, client_id);
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (runtime->credential_recovery_in_progress_) {
if (runtime->credential_recovery_attempt_pending_) {
runtime->credential_recovery_promote_pending_ = true;
} else {
runtime->credential_recovery_clear_pending_ = true;
}
}
} else if (SignalStatus::SignalFailed == status) {
runtime->signal_connected_ = false;
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
if (runtime->credential_recovery_in_progress_ &&
runtime->credential_recovery_attempt_pending_) {
runtime->credential_recovery_retry_active_ = true;
}
} else if (SignalStatus::SignalClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
runtime->signal_connected_ = false;
}
} else {
if (client_id.rfind("C-", 0) != 0) {
return;
}
std::string remote_id(client_id.begin() + 2, client_id.end());
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
props->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", remote_id);
SendClientInfo(props->peer_, client_id);
} else if (SignalStatus::SignalFailed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
props->signal_connected_ = false;
}
}
}
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char* user_id,
const size_t user_id_size,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto it = runtime->remote_sessions_.find(remote_id);
if (it != runtime->remote_sessions_.end()) {
props = it->second;
}
}
if (props) {
runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true;
props->connection_status_.store(status);
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.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,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.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(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
runtime->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
int native_snapshot_width = 0;
int native_snapshot_height = 0;
bool needs_native_snapshot = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
needs_native_snapshot =
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
}
if (needs_native_snapshot) {
auto* native_renderer = runtime->video_renderer_.get();
auto snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer &&
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
&native_snapshot_width,
&native_snapshot_height)) {
native_snapshot = std::move(snapshot);
}
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
if (native_snapshot &&
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
props->thumbnail_frame_ = std::move(native_snapshot);
props->thumbnail_width_ = native_snapshot_width;
props->thumbnail_height_ = native_snapshot_height;
}
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer) {
native_renderer->DiscardStream(remote_id);
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
runtime->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
break;
}
} else {
runtime->is_client_mode_ = false;
runtime->show_connection_status_window_ = true;
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_[remote_id] = status;
}
switch (status) {
case ConnectionStatus::Connected: {
#if _WIN32
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.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,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char*)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.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(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed");
break;
}
default:
break;
}
}
}
void PeerEventHandler::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) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password;
const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) {
id = client_id;
password.clear();
} else {
id.assign(client_id, at_pos - client_id);
password = at_pos + 1;
}
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
if (is_self_hosted) {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_));
strncpy(runtime->self_hosted_id_, client_id,
sizeof(runtime->self_hosted_id_) - 1);
runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0';
LOG_INFO("Use self-hosted client id [{}] and save to cache file", id);
runtime->settings_.PersistSelfHostedIdentity(client_id);
} else {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->client_id_with_password_, 0,
sizeof(runtime->client_id_with_password_));
strncpy(runtime->client_id_with_password_, client_id,
sizeof(runtime->client_id_with_password_) - 1);
runtime
->client_id_with_password_[sizeof(runtime->client_id_with_password_) -
1] = '\0';
LOG_INFO("Use client id [{}] and save id into cache file", id);
runtime->settings_.Save();
}
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
if (props->traversal_mode_ != mode) {
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
if (!net_traffic_stats) {
return;
}
// only display client side net status if connected to itself
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
@@ -0,0 +1,50 @@
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#include <cstddef>
#include <string>
#include "minirtc.h"
namespace crossdesk {
class GuiRuntime;
// Adapts MiniRTC C callbacks to the GUI runtime.
class PeerEventHandler {
public:
explicit PeerEventHandler(GuiRuntime &owner);
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveAudioBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveDataBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data);
static void OnSignalMessage(const char *message, size_t size,
void *user_data);
static void OnConnectionStatus(ConnectionStatus status, const char *user_id,
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, size_t user_id_size,
void *user_data);
private:
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
GuiRuntime &owner_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
@@ -0,0 +1,145 @@
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "platform/video_renderer.h"
#include "runtime/gui_runtime.h"
#include "runtime/peer_event_handler.h"
namespace crossdesk {
namespace {
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
} // namespace
void PeerEventHandler::OnReceiveVideoBuffer(
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
GuiRuntime::RemoteSession* props =
runtime->remote_sessions_.find(remote_id)->second.get();
if (props->connection_established_) {
bool background_snapshot_only = false;
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer && native_renderer->IsActive()) {
const auto submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height);
if (submit_result == VideoRenderer::SubmitResult::submitted) {
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
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;
// Once the native renderer owns the current stream, do not leave an
// older CPU frame ahead of the renderer's close snapshot.
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
++props->video_frame_sequence_;
props->streaming_ = true;
runtime->video_frame_dirty_.store(true, std::memory_order_release);
return;
}
if (submit_result == VideoRenderer::SubmitResult::dropped ||
submit_result == VideoRenderer::SubmitResult::failed) {
// Keep presenting the last native frame. A later decoded frame can
// reuse the renderer without changing ownership mid-window.
props->streaming_ = true;
return;
}
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
background_snapshot_only = true;
}
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const auto now = std::chrono::steady_clock::now();
if (background_snapshot_only && props->thumbnail_frame_ &&
!props->thumbnail_frame_->empty() &&
props->background_snapshot_time_ !=
std::chrono::steady_clock::time_point{} &&
now - props->background_snapshot_time_ <
kBackgroundSnapshotInterval) {
props->streaming_ = true;
return;
}
// Allocate a third buffer only while the UI still owns the old snapshot.
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
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);
}
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
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_);
props->thumbnail_frame_ = props->front_frame_;
props->thumbnail_width_ = video_frame->width;
props->thumbnail_height_ = video_frame->height;
if (background_snapshot_only) {
props->background_snapshot_time_ = now;
}
++props->video_frame_sequence_;
}
props->streaming_ = true;
if (background_snapshot_only) {
return;
}
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
}
void PeerEventHandler::OnReceiveAudioBuffer(
const char* data, size_t size, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
runtime->audio_buffer_fresh_ = true;
runtime->devices_.PushAudio(data, size);
}
} // namespace crossdesk
@@ -0,0 +1,119 @@
#include "runtime/remote_action_codec.h"
#include <remote_action.h>
#include <cstdlib>
#include <cstring>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action) {
std::vector<char> buffer;
buffer.push_back(static_cast<char>(action.type));
auto insert_bytes = [&](const void *ptr, size_t len) {
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
static_cast<const char *>(ptr) + len);
};
if (action.type == ControlType::host_infomation) {
insert_bytes(&action.i.host_name_size, sizeof(size_t));
insert_bytes(action.i.host_name, action.i.host_name_size);
size_t num = action.i.display_num;
insert_bytes(&num, sizeof(size_t));
for (size_t i = 0; i < num; ++i) {
const size_t len = std::strlen(action.i.display_list[i]);
insert_bytes(&len, sizeof(size_t));
insert_bytes(action.i.display_list[i], len);
}
insert_bytes(action.i.left, sizeof(int) * num);
insert_bytes(action.i.top, sizeof(int) * num);
insert_bytes(action.i.right, sizeof(int) * num);
insert_bytes(action.i.bottom, sizeof(int) * num);
}
return buffer;
}
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
size_t offset = 0;
auto read = [&](void *dst, size_t len) -> bool {
if (offset + len > size) {
return false;
}
std::memcpy(dst, data + offset, len);
offset += len;
return true;
};
if (size < 1) {
return false;
}
out.type = static_cast<ControlType>(data[offset++]);
if (out.type == ControlType::host_infomation) {
size_t name_len;
if (!read(&name_len, sizeof(size_t)) ||
name_len >= sizeof(out.i.host_name)) {
return false;
}
if (!read(out.i.host_name, name_len)) {
return false;
}
out.i.host_name[name_len] = '\0';
out.i.host_name_size = name_len;
size_t num;
if (!read(&num, sizeof(size_t))) {
return false;
}
out.i.display_num = num;
out.i.display_list =
static_cast<char **>(std::malloc(num * sizeof(char *)));
for (size_t i = 0; i < num; ++i) {
size_t len;
if (!read(&len, sizeof(size_t)) || offset + len > size) {
return false;
}
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
std::memcpy(out.i.display_list[i], data + offset, len);
out.i.display_list[i][len] = '\0';
offset += len;
}
auto alloc_int_array = [&](int *&array) {
array = static_cast<int *>(std::malloc(num * sizeof(int)));
return read(array, num * sizeof(int));
};
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
}
return true;
}
void Free(RemoteAction &action) {
if (action.type != ControlType::host_infomation) {
return;
}
for (size_t i = 0; i < action.i.display_num; ++i) {
std::free(action.i.display_list[i]);
}
std::free(action.i.display_list);
std::free(action.i.left);
std::free(action.i.top);
std::free(action.i.right);
std::free(action.i.bottom);
action.i.display_list = nullptr;
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
action.i.display_num = 0;
}
} // namespace crossdesk::remote_action_codec
@@ -0,0 +1,17 @@
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
#include <remote_action.h>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action);
bool Deserialize(const char *data, size_t size, RemoteAction &out);
void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
@@ -0,0 +1,177 @@
#ifndef _REMOTE_SESSION_H_
#define _REMOTE_SESSION_H_
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include <remote_action.h>
#include <stream_names.h>
#include "display_info.h"
#include "minirtc.h"
namespace crossdesk::gui_detail {
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;
bool file_transfer_window_hovered_ = 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_;
};
// Runtime state for one connected or connecting remote endpoint. It groups the
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = kAudioStream;
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
std::string keyboard_label_ = kKeyboardStream;
std::string file_label_ = kFileStream;
std::string control_data_label_ = kControlStream;
std::string file_feedback_label_ = kFileFeedbackStream;
std::string clipboard_label_ = kClipboardStream;
std::string local_id_;
std::string remote_id_;
bool exit_ = false;
bool signal_connected_ = false;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
bool display_selectable_hovered_ = false;
bool shortcut_selectable_hovered_ = false;
bool control_bar_expand_ = true;
bool reset_control_bar_pos_ = false;
bool control_window_width_is_changing_ = false;
bool control_window_height_is_changing_ = false;
bool p2p_mode_ = true;
bool remember_password_ = false;
char remote_password_[7] = "";
float sub_stream_window_width_ = 1280;
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 300;
float control_window_min_height_ = 38;
float control_window_max_height_ = 180;
float control_window_width_ = 300;
float control_window_height_ = 38;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
// Written by the decode callback thread and consumed by the UI thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
// Retains the most recent background CPU frame across disconnect cleanup
// so closing an unselected tab can still update its recent-item thumbnail.
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
int thumbnail_width_ = 0;
int thumbnail_height_ = 0;
std::chrono::steady_clock::time_point background_snapshot_time_;
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;
float mouse_pos_y_last_ = 0;
int texture_width_ = 1280;
int texture_height_ = 720;
int video_width_ = 0;
int video_height_ = 0;
int video_width_last_ = 0;
int video_height_last_ = 0;
int selected_display_ = 0;
size_t video_size_ = 0;
uint64_t video_frame_sequence_ = 0;
bool tab_selected_ = false;
bool tab_opened_ = true;
std::optional<float> pos_x_before_docked_;
std::optional<float> pos_y_before_docked_;
float render_window_x_ = 0;
float render_window_y_ = 0;
float render_window_width_ = 0;
float render_window_height_ = 0;
std::string fullscreen_button_label_ = "Fullscreen";
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
std::string mouse_control_button_label_ = "Mouse Control";
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_;
bool remote_service_status_received_ = false;
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
// Cursor snapshots arrive on the transport callback thread and are applied
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
// and shape cannot briefly come from different protocol messages.
std::mutex remote_cursor_state_mutex_;
CursorState remote_cursor_state_{};
bool remote_cursor_state_received_ = false;
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_{};
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
};
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif
@@ -0,0 +1,213 @@
#ifndef _RUNTIME_STATE_H_
#define _RUNTIME_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include <stream_names.h>
#include "config_center.h"
#include "path_manager.h"
#include "runtime/device_presence_cache.h"
#include "runtime/remote_session.h"
#include "thumbnail.h"
namespace crossdesk::gui_detail {
struct InfrastructureState {
std::unique_ptr<ConfigCenter> config_center_;
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
int localization_language_index_ = -1;
int localization_language_index_last_ = -1;
bool modules_inited_ = false;
unsigned char aes128_key_[16]{};
unsigned char aes128_iv_[16]{};
};
struct RecentConnectionsState {
std::shared_ptr<Thumbnail> thumbnail_;
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresenceCache device_presence_cache_;
bool need_to_send_recent_connections_ = true;
};
struct PeerState {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
std::string signal_status_str_;
bool signal_connected_ = false;
PeerPtr *peer_ = nullptr;
PeerPtr *peer_reserved_ = nullptr;
std::string video_primary_label_ = "primary_display";
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = kDataStream;
std::string mouse_label_ = kMouseStream;
std::string keyboard_label_ = kKeyboardStream;
std::string info_label_ = "info";
std::string control_data_label_ = kControlStream;
std::string file_label_ = kFileStream;
std::string file_feedback_label_ = kFileFeedbackStream;
std::string clipboard_label_ = kClipboardStream;
Params params_;
};
// OS-specific service and permission state is kept separate from transport
// state so platform code does not appear to be part of the wire contract.
struct PlatformIntegrationState {
#if _WIN32
std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false;
bool local_service_available_ = false;
std::string local_interactive_stage_;
uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_tick_ = 0;
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
#if CROSSDESK_PORTABLE
enum class PortableServiceInstallState {
idle,
installing,
succeeded,
failed,
};
bool portable_service_prompt_checked_ = false;
bool show_portable_service_install_window_ = false;
bool show_portable_service_prompt_suppressed_window_ = false;
bool portable_service_do_not_remind_ = false;
bool portable_service_prompt_suppressed_ = false;
bool portable_service_installed_ = false;
std::atomic<PortableServiceInstallState> portable_service_install_state_{
PortableServiceInstallState::idle};
std::thread portable_service_install_thread_;
#endif
#endif
#ifdef __APPLE__
bool show_request_permission_window_ = true;
bool mac_permission_status_initialized_ = false;
uint32_t mac_permission_last_check_tick_ = 0;
bool mac_screen_recording_permission_granted_ = false;
bool mac_accessibility_permission_granted_ = false;
bool mac_screen_recording_permission_requested_ = false;
bool mac_accessibility_permission_requested_ = false;
#endif
};
struct UserSettingsState {
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
char connection_login_identity_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_;
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_;
bool show_file_browser_ = true;
};
struct PasswordChangeState {
std::mutex password_change_mutex_;
uint64_t next_password_change_request_id_ = 0;
bool password_change_pending_ = false;
bool password_change_result_ready_ = false;
bool password_change_succeeded_ = false;
bool password_change_result_uncertain_ = false;
std::chrono::steady_clock::time_point password_change_requested_at_;
std::string pending_password_change_request_id_;
std::string pending_local_password_;
std::string password_change_error_;
bool credential_recovery_in_progress_ = false;
bool credential_recovery_attempt_pending_ = false;
bool credential_recovery_retry_active_ = false;
bool credential_recovery_promote_pending_ = false;
bool credential_recovery_clear_pending_ = false;
};
struct ConnectionState {
using RemoteSessionMap =
std::unordered_map<std::string, RemoteSessionPtr>;
RemoteSessionMap remote_sessions_;
std::shared_mutex remote_sessions_mutex_;
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
// Cursor position is sampled asynchronously after remote mouse input has
// been injected. Remember the input source so the sampled position is not
// immediately echoed back to that same controller as stale feedback.
std::mutex remote_pointer_input_mutex_;
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
last_remote_pointer_input_time_;
std::string selected_server_remote_id_;
std::string selected_server_remote_hostname_;
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::chrono::steady_clock::time_point pending_presence_probe_started_at_;
std::string pending_presence_remote_id_;
std::string pending_presence_password_;
bool pending_presence_remember_password_ = false;
};
struct RuntimeState : InfrastructureState,
RecentConnectionsState,
PeerState,
PlatformIntegrationState,
UserSettingsState,
PasswordChangeState,
ConnectionState {};
} // namespace crossdesk::gui_detail
#endif
+458
View File
@@ -0,0 +1,458 @@
// The legacy client uses ImGui::StyleColorsLight(). Keep the structural line
// colors in one place so the Slint renderer produces the same visual weight.
export global ImGuiLineStyle {
out property <color> border: #0000004d;
out property <color> separator: #6363639e;
out property <color> divider: #0000007a;
out property <color> control-separator: #b2b2b2;
}
// ImGui loads a 32px font atlas and applies per-window font scales. These
// logical sizes reproduce those visual tiers on Slint's HiDPI renderer.
export global ImGuiFontStyle {
out property <length> base: 12px;
out property <length> section-title: 18px;
out property <length> panel-label: 17px;
out property <length> field-value: 22px;
out property <length> prominent: 16px;
out property <length> body: 11px;
out property <length> small: 10px;
}
// Font Awesome 6 Free Solid glyphs used by the legacy ImGui client. The font
// data is registered for every Slint window by GuiApplication before show().
export global FontAwesomeIcons {
out property <string> angle-down: "\u{f107}";
out property <string> angle-left: "\u{f104}";
out property <string> angle-right: "\u{f105}";
out property <string> angle-up: "\u{f106}";
out property <string> arrow-right-long: "\u{f178}";
out property <string> bars: "\u{f0c9}";
out property <string> check: "\u{f00c}";
out property <string> circle-arrow-up: "\u{f0aa}";
out property <string> compress: "\u{f066}";
out property <string> computer-mouse: "\u{f8cc}";
out property <string> copy: "\u{f0c5}";
out property <string> display: "\u{e163}";
out property <string> expand: "\u{f065}";
out property <string> eye: "\u{f06e}";
out property <string> eye-slash: "\u{f070}";
out property <string> folder-open: "\u{f07c}";
out property <string> keyboard: "\u{f11c}";
out property <string> minus: "\u{f068}";
out property <string> pen: "\u{f304}";
out property <string> signal: "\u{f012}";
out property <string> shield-halved: "\u{f3ed}";
out property <string> square-full: "\u{f45c}";
out property <string> trash-can: "\u{f2ed}";
out property <string> volume-high: "\u{f028}";
out property <string> volume-xmark: "\u{f6a9}";
out property <string> xmark: "\u{f00d}";
}
export component IconButton inherits Rectangle {
in property <string> icon;
in property <string> tooltip;
in property <bool> danger: false;
in property <length> icon-size: 16px;
in property <color> icon-color: #24272d;
in property <color> normal-background: transparent;
in property <color> hover-background: #edf0f5;
in property <color> pressed-background: #d9dde5;
in property <bool> slashed: false;
callback clicked;
width: 30px;
height: 30px;
background: touch.pressed ? (danger ? #d92d20 : root.pressed-background)
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
: root.normal-background;
border-radius: 4px;
Text {
text: root.icon;
color: touch.has-hover && root.danger ? white : root.icon-color;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: root.icon-size;
horizontal-alignment: center;
vertical-alignment: center;
}
if root.slashed: Text {
text: "";
color: touch.has-hover && root.danger ? white : root.icon-color;
font-size: root.icon-size + 7px;
font-weight: 700;
horizontal-alignment: center;
vertical-alignment: center;
}
touch := TouchArea {
clicked => { root.clicked(); }
}
if touch.has-hover && root.tooltip != "": Rectangle {
x: min(root.width - self.width, 0px);
y: root.height + 3px;
width: tip.preferred-width + 12px;
height: 24px;
background: #22252be8;
border-radius: 4px;
z: 20;
tip := Text {
text: root.tooltip;
color: white;
font-size: ImGuiFontStyle.small;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
// Retained for non-font artwork. UI action icons use IconButton so the Slint
// and ImGui clients share Font Awesome's glyph geometry.
export component ImageButton inherits Rectangle {
in property <image> icon;
in property <string> tooltip;
in property <bool> danger: false;
in property <length> icon-width: 16px;
in property <length> icon-height: 16px;
callback clicked;
width: 30px;
height: 30px;
background: touch.pressed ? (danger ? #d92d20 : #d9dde5)
: touch.has-hover ? (danger ? #f04438 : #edf0f5)
: transparent;
border-radius: 4px;
Image {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: root.icon-width;
height: root.icon-height;
source: root.icon;
image-fit: contain;
}
touch := TouchArea {
clicked => { root.clicked(); }
}
if touch.has-hover && root.tooltip != "": Rectangle {
x: min(root.width - self.width, 0px);
y: root.height + 3px;
width: tip.preferred-width + 12px;
height: 24px;
background: #22252be8;
border-radius: 4px;
z: 20;
tip := Text {
text: root.tooltip;
color: white;
font-size: ImGuiFontStyle.small;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
export component CompactComboBox inherits Rectangle {
in property <[string]> model;
in-out property <int> current-index: 0;
in property <bool> enabled: true;
property <bool> popup-animation-enabled: false;
property <float> popup-progress: 0.0;
callback selected(int);
animate popup-progress {
duration: 160ms;
easing: ease-out;
enabled: root.popup-animation-enabled;
}
width: 73px;
height: 24px;
background: root.enabled ? white : #f1f1f1;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
Text {
x: 5px;
width: parent.width - 24px;
text: root.model[root.current-index];
color: root.enabled ? #202124 : #8b8f95;
font-size: ImGuiFontStyle.body;
overflow: elide;
vertical-alignment: center;
}
Rectangle {
x: parent.width - 21px;
width: 20px;
height: parent.height - 2px;
y: 1px;
background: root.enabled ? #9bc9f8 : #d7d9dc;
Text {
text: FontAwesomeIcons.angle-down;
color: #202124;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 10px;
horizontal-alignment: center;
vertical-alignment: center;
transform-rotation: combo-popup.is-open ? 180deg : 0deg;
animate transform-rotation {
duration: 140ms;
easing: ease-out;
enabled: combo-popup.is-open;
}
}
}
TouchArea {
enabled: root.enabled;
clicked => {
root.popup-animation-enabled = false;
root.popup-progress = 0.0;
combo-popup.show();
popup-open-timer.running = true;
}
}
// Start on the next frame so the native popup first renders at zero
// height, making the downward expansion visible on every backend.
popup-open-timer := Timer {
interval: 16ms;
running: false;
triggered => {
self.running = false;
if combo-popup.is-open {
root.popup-animation-enabled = true;
root.popup-progress = 1.0;
}
}
}
combo-popup := PopupWindow {
x: 0px;
y: root.height;
width: root.width;
height: root.model.length * 23px;
close-policy: close-on-click-outside;
Rectangle {
// Keep the reveal anchored to the popup's top edge while the
// content settles downward into its final position.
y: (1.0 - root.popup-progress) * -8px;
width: parent.width;
height: parent.height * root.popup-progress;
opacity: root.popup-progress;
background: white;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
clip: true;
for choice[index] in root.model: Rectangle {
y: index * 23px;
width: parent.width;
height: 23px;
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
option-touch := TouchArea {
clicked => {
popup-open-timer.running = false;
root.popup-animation-enabled = false;
root.popup-progress = 0.0;
root.current-index = index;
root.selected(index);
combo-popup.close();
}
}
}
Rectangle {
width: parent.width;
height: parent.height;
background: transparent;
border-width: 1px;
border-color: #bfc2c7;
border-radius: 3px;
z: 2;
}
}
}
}
export component ImGuiCheckBox inherits Rectangle {
in-out property <bool> checked;
in property <bool> enabled: true;
width: 21px;
height: 21px;
background: root.enabled ? white : #f1f1f1;
border-width: 1px;
border-color: check-touch.has-hover && root.enabled ? #8cbef2 : #b9bdc2;
border-radius: 4px;
if root.checked: Text {
text: FontAwesomeIcons.check;
color: root.enabled ? #2e86de : #8b8f95;
font-family: "Font Awesome 6 Free";
font-weight: 900;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
}
check-touch := TouchArea {
enabled: root.enabled;
clicked => { root.checked = !root.checked; }
}
}
export component MiniToggleSwitch inherits Rectangle {
in-out property <bool> checked;
in property <bool> enabled: true;
callback toggled(bool);
width: 29px;
height: 16px;
background: !root.enabled ? (root.checked ? #6e9ddacc : #9a9a9acc)
: root.checked ? #2463c7 : #9a9a9a;
border-radius: 8px;
Rectangle {
x: root.checked ? parent.width - self.width - 2px : 2px;
y: 2px;
width: 12px;
height: 12px;
border-radius: 6px;
background: white;
}
TouchArea {
enabled: root.enabled;
clicked => { root.checked = !root.checked; root.toggled(root.checked); }
}
}
export component CompactButton inherits Rectangle {
in property <string> text;
in property <bool> primary: false;
in property <bool> enabled: true;
callback clicked;
width: 34px;
height: 24px;
background: !root.enabled ? #eceef0
: button-touch.pressed ? (root.primary ? #82b8f2 : #d8dadd)
: button-touch.has-hover ? (root.primary ? #b8d9fb : #f0f1f2)
: root.primary ? #a8d0fa : #e2e4e7;
border-width: 1px;
border-color: root.primary ? #9bc3ed : #c8cbd0;
border-radius: 3px;
Text {
text: root.text;
color: root.enabled ? #24272d : #8e9298;
font-size: ImGuiFontStyle.body;
overflow: elide;
horizontal-alignment: center;
vertical-alignment: center;
}
button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } }
}
export component PrimaryButton inherits Rectangle {
in property <string> text;
in property <bool> enabled: true;
callback clicked;
min-width: 72px;
height: 30px;
background: !root.enabled ? #b8c1d1
: touch.pressed ? #1849a9
: touch.has-hover ? #2970d6
: #2463c7;
border-radius: 5px;
Text {
text: root.text;
color: white;
font-size: ImGuiFontStyle.body;
horizontal-alignment: center;
vertical-alignment: center;
}
touch := TouchArea {
enabled: root.enabled;
clicked => { root.clicked(); }
}
}
export component SecondaryButton inherits Rectangle {
in property <string> text;
in property <bool> enabled: true;
callback clicked;
min-width: 72px;
height: 30px;
background: !root.enabled ? #f1f2f4
: touch.pressed ? #dfe3e9
: touch.has-hover ? #f3f4f6 : white;
border-width: 1px;
border-color: #c9ced7;
border-radius: 5px;
clip: true;
Text {
text: root.text;
color: root.enabled ? #24272d : #949aa5;
font-size: ImGuiFontStyle.body;
horizontal-alignment: center;
vertical-alignment: center;
}
touch := TouchArea {
enabled: root.enabled;
clicked => { root.clicked(); }
}
}
export component ToggleSwitch inherits Rectangle {
in-out property <bool> checked;
in property <bool> enabled: true;
callback toggled(bool);
width: 36px;
height: 20px;
background: !root.enabled ? #d0d5dd : root.checked ? #2463c7 : #98a2b3;
border-radius: self.height / 2;
Rectangle {
x: root.checked ? parent.width - self.width - 3px : 3px;
y: 3px;
width: 14px;
height: 14px;
border-radius: 7px;
background: white;
animate x { duration: 100ms; easing: ease-in-out; }
}
TouchArea {
enabled: root.enabled;
clicked => {
root.checked = !root.checked;
root.toggled(root.checked);
}
}
}
export component SectionTitle inherits Text {
color: #00000080;
font-size: ImGuiFontStyle.section-title;
font-weight: 500;
vertical-alignment: center;
}
export component DialogSurface inherits Rectangle {
background: white;
border-width: 1px;
border-color: ImGuiLineStyle.border;
border-radius: 8px;
}

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