Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d381cbaa9b | ||
|
|
5aedb09c55 | ||
|
|
fe1178e26f | ||
|
|
e0cd761bdc | ||
|
|
2000654792 | ||
|
|
19800ef350 | ||
|
|
d10b65cc19 | ||
|
|
3a0179968c | ||
|
|
55f389b834 | ||
|
|
8a58166c59 | ||
|
|
aa2ee8ce7f | ||
|
|
b150d46501 | ||
|
|
010064d5b7 | ||
|
|
db3b26efe7 | ||
|
|
cd1b53ee65 | ||
|
|
b031a3953e | ||
|
|
3f537ae63c | ||
|
|
e716346b26 | ||
|
|
b7e1adbec6 | ||
|
|
58f2daa9c1 | ||
|
|
8752e51921 | ||
|
|
a8cb0e21ce | ||
|
|
90184f5251 | ||
|
|
fb0ab4f14e | ||
|
|
0e3f3cec00 | ||
|
|
72af49eb80 | ||
|
|
36f05834b5 | ||
|
|
1a190fbaf5 | ||
|
|
1fe236da76 | ||
|
|
3b989fb70d | ||
|
|
7595ab8bd8 |
@@ -8,13 +8,15 @@
|
||||
# Dependency manifests and local xmake package recipes are the only project
|
||||
# files needed while pre-warming the build image.
|
||||
!xmake.lua
|
||||
!xmake/
|
||||
!xmake/**
|
||||
!thirdparty/
|
||||
!thirdparty/**
|
||||
!submodules/
|
||||
!submodules/xmake.lua
|
||||
!submodules/minirtc/
|
||||
!submodules/minirtc/xmake.lua
|
||||
!submodules/minirtc/thirdparty/
|
||||
!submodules/minirtc/thirdparty/**
|
||||
!apps/
|
||||
!apps/desktop/
|
||||
!apps/desktop/xmake/
|
||||
!apps/desktop/xmake/**
|
||||
!deps/thirdparty/
|
||||
!deps/thirdparty/**
|
||||
!deps/submodules/
|
||||
!deps/submodules/xmake.lua
|
||||
!deps/submodules/minirtc/
|
||||
!deps/submodules/minirtc/xmake.lua
|
||||
!deps/submodules/minirtc/thirdparty/
|
||||
!deps/submodules/minirtc/thirdparty/**
|
||||
|
||||
@@ -11,11 +11,11 @@ on:
|
||||
- docker/linux-build/**
|
||||
- xmake.lua
|
||||
- xmake/**
|
||||
- thirdparty/**
|
||||
- submodules/xmake.lua
|
||||
- submodules/minirtc
|
||||
- submodules/minirtc/xmake.lua
|
||||
- submodules/minirtc/thirdparty/**
|
||||
- deps/thirdparty/**
|
||||
- deps/submodules/xmake.lua
|
||||
- deps/submodules/minirtc
|
||||
- deps/submodules/minirtc/xmake.lua
|
||||
- deps/submodules/minirtc/thirdparty/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_amd64.sh
|
||||
package_script: ./apps/desktop/scripts/linux/pkg_amd64.sh
|
||||
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_arm64.sh
|
||||
package_script: ./apps/desktop/scripts/linux/pkg_arm64.sh
|
||||
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
@@ -138,6 +138,17 @@ jobs:
|
||||
--cc=gcc-10 --cxx=g++-10 --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
- name: Verify repository and wire boundaries
|
||||
env:
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
xmake b -vy --root repository_structure_test
|
||||
xmake b -vy --root crossdesk_wire_test
|
||||
xmake b -vy --root keyboard_state_wire_test
|
||||
xmake run --root repository_structure_test
|
||||
xmake run --root crossdesk_wire_test
|
||||
xmake run --root keyboard_state_wire_test
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
chmod +x ${{ matrix.package_script }}
|
||||
@@ -178,13 +189,13 @@ jobs:
|
||||
runner: macos-26-intel
|
||||
cache-key: intel-macos26-xcode26.6-min14
|
||||
out-dir: ./build/macosx/x86_64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_x64.sh
|
||||
package_script: ./apps/desktop/scripts/macos/pkg_x64.sh
|
||||
- arch: arm64
|
||||
binary_arch: arm64
|
||||
runner: macos-26
|
||||
cache-key: arm-macos26-xcode26.6-min14
|
||||
out-dir: ./build/macosx/arm64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_arm64.sh
|
||||
package_script: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
@@ -282,7 +293,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
|
||||
./apps/desktop/scripts/macos/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -295,6 +306,173 @@ jobs:
|
||||
mkdir -p release
|
||||
cp crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg release/
|
||||
|
||||
# iOS
|
||||
build-ios:
|
||||
name: Build on iOS (arm64)
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
IOS_DEPLOYMENT_TARGET: "16.0"
|
||||
EXPECTED_IOS_SDK: "26.5"
|
||||
XMAKE_VERSION: "3.1.1"
|
||||
XMAKE_REPO_REVISION: "121109352da272cd1a3d4ed52f2dcda34f839955"
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
|
||||
- name: Select Xcode 26.6
|
||||
shell: bash
|
||||
run: |
|
||||
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
|
||||
xcodebuild -version
|
||||
SDK_VERSION="$(xcrun --sdk iphoneos --show-sdk-version)"
|
||||
if [[ "${SDK_VERSION}" != "${EXPECTED_IOS_SDK}" ]]; then
|
||||
echo "Unexpected iOS SDK: expected ${EXPECTED_IOS_SDK}, got ${SDK_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: apps/ios/.xmake/packages
|
||||
key: "${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install xmake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: "${{ env.XMAKE_VERSION }}"
|
||||
|
||||
- name: Verify xmake version
|
||||
shell: bash
|
||||
run: |
|
||||
XMAKE_VERSION_OUTPUT="$(xmake --version)"
|
||||
echo "${XMAKE_VERSION_OUTPUT}"
|
||||
if [[ "${XMAKE_VERSION_OUTPUT}" != *"xmake v${XMAKE_VERSION}"* ]]; then
|
||||
echo "Unexpected xmake version: expected ${XMAKE_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pin xmake package repository
|
||||
shell: bash
|
||||
run: |
|
||||
xmake repo -u
|
||||
XMAKE_REPO_DIR="${HOME}/.xmake/repositories/xmake-repo"
|
||||
git -C "${XMAKE_REPO_DIR}" fetch --depth=1 origin "${XMAKE_REPO_REVISION}"
|
||||
git -C "${XMAKE_REPO_DIR}" checkout --detach "${XMAKE_REPO_REVISION}"
|
||||
test "$(git -C "${XMAKE_REPO_DIR}" rev-parse HEAD)" = "${XMAKE_REPO_REVISION}"
|
||||
|
||||
- name: Build CrossDesk Mobile
|
||||
shell: bash
|
||||
run: |
|
||||
DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData"
|
||||
echo "DERIVED_DATA=${DERIVED_DATA}" >> "${GITHUB_ENV}"
|
||||
xcodebuild \
|
||||
-project apps/ios/CrossDeskMobile.xcodeproj \
|
||||
-scheme CrossDeskMobile \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-derivedDataPath "${DERIVED_DATA}" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||
IPHONEOS_DEPLOYMENT_TARGET="${IOS_DEPLOYMENT_TARGET}" \
|
||||
build
|
||||
|
||||
- name: Verify CrossDesk Mobile
|
||||
shell: bash
|
||||
run: |
|
||||
APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app"
|
||||
EXECUTABLE_PATH="${APP_PATH}/CrossDeskMobile"
|
||||
INFO_PLIST="${APP_PATH}/Info.plist"
|
||||
|
||||
if [[ ! -x "${EXECUTABLE_PATH}" ]]; then
|
||||
echo "CrossDeskMobile executable is missing from ${APP_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcrun lipo "${EXECUTABLE_PATH}" -verify_arch arm64
|
||||
MINIMUM_OS=$(/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "${INFO_PLIST}")
|
||||
if [[ "${MINIMUM_OS}" != "${IOS_DEPLOYMENT_TARGET}" ]]; then
|
||||
echo "Unexpected minimum iOS version: expected ${IOS_DEPLOYMENT_TARGET}, got ${MINIMUM_OS}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
xcrun lipo -info "${EXECUTABLE_PATH}"
|
||||
echo "Minimum iOS version: ${MINIMUM_OS}"
|
||||
|
||||
- name: Package CrossDesk Mobile
|
||||
shell: bash
|
||||
run: |
|
||||
APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app"
|
||||
PACKAGE_DIR="${RUNNER_TEMP}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}"
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}.zip"
|
||||
|
||||
mkdir -p "${PACKAGE_DIR}"
|
||||
cp -R "${APP_PATH}" "${PACKAGE_DIR}/"
|
||||
|
||||
DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app.dSYM"
|
||||
if [[ -d "${DSYM_PATH}" ]]; then
|
||||
cp -R "${DSYM_PATH}" "${PACKAGE_DIR}/"
|
||||
fi
|
||||
|
||||
ditto -c -k --sequesterRsrc --keepParent "${PACKAGE_DIR}" "${PACKAGE_FILE}"
|
||||
echo "PACKAGE_FILE=${PACKAGE_FILE}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-ios-arm64-unsigned-${{ env.VERSION_NUM }}
|
||||
path: ${{ env.PACKAGE_FILE }}
|
||||
if-no-files-found: error
|
||||
|
||||
# Windows
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
@@ -404,7 +582,7 @@ jobs:
|
||||
- name: Copy nsProcess plugin to NSIS folder
|
||||
run: |
|
||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||
copy "${{ github.workspace }}\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||
copy "${{ github.workspace }}\apps\desktop\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
@@ -414,14 +592,14 @@ jobs:
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
& "${{ github.workspace }}\apps\desktop\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Upload unsigned Windows installer for test signing
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
id: upload-unsigned-windows-installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -447,7 +625,7 @@ jobs:
|
||||
run: |
|
||||
$installerName = "crossdesk-win-x64-${{ env.VERSION_NUM }}.exe"
|
||||
$signedInstaller = Join-Path "${{ github.workspace }}\signpath\signed-installer" $installerName
|
||||
$destination = Join-Path "${{ github.workspace }}\scripts\windows" $installerName
|
||||
$destination = Join-Path "${{ github.workspace }}\apps\desktop\scripts\windows" $installerName
|
||||
|
||||
if (!(Test-Path $signedInstaller)) {
|
||||
throw "SignPath did not return the signed installer: $signedInstaller"
|
||||
@@ -504,7 +682,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
|
||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
|
||||
- name: Upload portable artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -515,7 +693,7 @@ jobs:
|
||||
release:
|
||||
name: Publish Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-linux, build-macos, build-windows-x64]
|
||||
needs: [build-linux, build-macos, build-ios, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -578,6 +756,7 @@ jobs:
|
||||
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||
cp artifacts/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Xmake cache
|
||||
.xmake/
|
||||
build/
|
||||
apps/ios/Vendor/
|
||||
apps/ios/DerivedData*/
|
||||
apps/ios/.xmake/
|
||||
xcuserdata/
|
||||
*.xcuserstate
|
||||
|
||||
# MacOS Cache
|
||||
.DS_Store
|
||||
|
||||
# VSCode cache
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[submodule "submodules/minirtc"]
|
||||
path = submodules/minirtc
|
||||
[submodule "deps/submodules/minirtc"]
|
||||
path = deps/submodules/minirtc
|
||||
url = https://github.com/kunkundi/minirtc.git
|
||||
|
||||
@@ -171,10 +171,10 @@ xmake b -vy crossdesk
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
||||
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
||||
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
|
||||
```
|
||||
|
||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||
|
||||
@@ -179,10 +179,10 @@ Build a Debian package after compiling a release binary on the matching architec
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
||||
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
||||
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
|
||||
```
|
||||
|
||||
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@@ -1,5 +1,5 @@
|
||||
// Application icon resource; load by the resource name IDI_ICON1.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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 "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
@@ -1,5 +1,5 @@
|
||||
// Session helper icon and manifest resources.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
@@ -20,7 +20,7 @@ PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desk
|
||||
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
@@ -142,7 +142,7 @@ 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/icons/linux/crossdesk_${size}x${size}.png" \
|
||||
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
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
#!/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="./build/macosx/arm64/release/crossdesk"
|
||||
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/arm64/release/crossdesk"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
BINARY_ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
@@ -103,7 +106,7 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
@@ -1,16 +1,19 @@
|
||||
#!/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="build/macosx/x86_64/release/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="icons/macos/crossdesk.icns"
|
||||
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
@@ -103,7 +106,7 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
@@ -12,7 +12,7 @@
|
||||
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
||||
|
||||
; Installer icon path
|
||||
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
|
||||
!define MUI_ICON "${__FILEDIR__}\..\..\resources\windows\crossdesk.ico"
|
||||
|
||||
; Compression settings
|
||||
SetCompressor /FINAL lzma
|
||||
@@ -84,12 +84,12 @@ installApp:
|
||||
SetOverwrite ifnewer
|
||||
|
||||
; Main application executable path
|
||||
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
|
||||
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"
|
||||
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"
|
||||
File "..\..\..\..\build\windows\x64\release\*.dll"
|
||||
|
||||
Call RegisterInstalledService
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
#define _DAEMON_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <csignal>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
@@ -28,9 +27,6 @@ class Daemon {
|
||||
std::string name_;
|
||||
bool runWithRestart(MainLoopFunc loop);
|
||||
|
||||
#ifndef _WIN32
|
||||
static volatile std::sig_atomic_t stop_requested_;
|
||||
#endif
|
||||
std::atomic<bool> running_;
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
@@ -175,6 +176,10 @@ int HandleServiceCliCommand(const std::string& command) {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 &&
|
||||
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
|
||||
return crossdesk::RunSlintRendererProbe();
|
||||
}
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
@@ -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
|
||||
@@ -7,7 +7,7 @@
|
||||
#ifndef _PLATFORM_H_
|
||||
#define _PLATFORM_H_
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "shared_cursor_state.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "remote_cursor_shape.h"
|
||||
#include <remote_cursor_shape.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "task_queue_lock_free.h"
|
||||
|
||||
#include <exception>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
TaskQueueLockFree::TaskQueueLockFree()
|
||||
: producer_(task_queue_), worker_([this] { WorkerThread(); }) {}
|
||||
|
||||
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
|
||||
|
||||
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
|
||||
TaskItem item;
|
||||
item.run = std::move(task);
|
||||
auto result = item.completion.get_future();
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_) {
|
||||
throw std::runtime_error("Cannot post to a stopped task queue");
|
||||
}
|
||||
|
||||
// Count before publishing: the worker may dequeue as soon as enqueue returns.
|
||||
++pending_tasks_;
|
||||
if (!task_queue_.enqueue(producer_, std::move(item))) {
|
||||
--pending_tasks_;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
wake_.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::Stop() {
|
||||
// Concurrent Stop callers all wait for the same worker shutdown to finish.
|
||||
std::call_once(stop_once_, [this] {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_all();
|
||||
worker_.join();
|
||||
});
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::WorkerThread() {
|
||||
while (true) {
|
||||
TaskItem item;
|
||||
while (task_queue_.try_dequeue(item)) {
|
||||
--pending_tasks_;
|
||||
|
||||
std::exception_ptr error;
|
||||
try {
|
||||
item.run();
|
||||
} catch (...) {
|
||||
error = std::current_exception();
|
||||
}
|
||||
|
||||
// Release captured resources before marking the task complete.
|
||||
item.run = nullptr;
|
||||
if (error) {
|
||||
item.completion.set_exception(error);
|
||||
} else {
|
||||
item.completion.set_value();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock lock(mutex_);
|
||||
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
|
||||
// A stop request must not discard tasks submitted while the worker was idle.
|
||||
if (stopping_ && pending_tasks_ == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _TASK_QUEUE_LOCK_FREE_H_
|
||||
#define _TASK_QUEUE_LOCK_FREE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <concurrentqueue.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// A single worker executes tasks in submission order. The concurrent queue
|
||||
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
|
||||
class TaskQueueLockFree {
|
||||
public:
|
||||
TaskQueueLockFree();
|
||||
~TaskQueueLockFree();
|
||||
|
||||
// Returns completion or the task's exception. Throws if the queue is stopped.
|
||||
std::future<void> PostTask(std::function<void()> task);
|
||||
|
||||
// Rejects new tasks, finishes accepted tasks and joins the worker.
|
||||
// May be called repeatedly, but never from the queue's own tasks.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
struct TaskItem {
|
||||
std::function<void()> run;
|
||||
std::promise<void> completion;
|
||||
};
|
||||
|
||||
void WorkerThread();
|
||||
|
||||
moodycamel::ConcurrentQueue<TaskItem> task_queue_;
|
||||
// All submissions share one producer so different caller threads keep FIFO.
|
||||
moodycamel::ProducerToken producer_;
|
||||
std::atomic<std::size_t> pending_tasks_{0};
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopping_ = false;
|
||||
std::once_flag stop_once_;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -12,6 +12,12 @@ bool IsValidTurnModeValue(long value) {
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
bool IsValidVideoAdaptationPolicyValue(long value) {
|
||||
using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY;
|
||||
return value >= static_cast<long>(Policy::FRAME_RATE_PRIORITY) &&
|
||||
value <= static_cast<long>(Policy::BALANCED);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
@@ -52,6 +58,18 @@ int ConfigCenter::Load() {
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
const long video_adaptation_policy_value = ini_.GetLongValue(
|
||||
section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) {
|
||||
video_adaptation_policy_ =
|
||||
static_cast<VIDEO_ADAPTATION_POLICY>(video_adaptation_policy_value);
|
||||
} else {
|
||||
LOG_WARN("Invalid video adaptation policy [{}], using quality priority",
|
||||
video_adaptation_policy_value);
|
||||
video_adaptation_policy_ = VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
}
|
||||
|
||||
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
|
||||
ini_.GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
@@ -137,6 +155,8 @@ int ConfigCenter::Save() {
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
@@ -205,6 +225,18 @@ int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoAdaptationPolicy(
|
||||
VIDEO_ADAPTATION_POLICY policy) {
|
||||
video_adaptation_policy_ = policy;
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
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;
|
||||
@@ -395,6 +427,11 @@ ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ADAPTATION_POLICY
|
||||
ConfigCenter::GetVideoAdaptationPolicy() const {
|
||||
return video_adaptation_policy_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
@@ -18,6 +18,11 @@ class ConfigCenter {
|
||||
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_ADAPTATION_POLICY {
|
||||
FRAME_RATE_PRIORITY = 0,
|
||||
QUALITY_PRIORITY = 1,
|
||||
BALANCED = 2
|
||||
};
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
@@ -34,6 +39,7 @@ class ConfigCenter {
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
@@ -53,6 +59,7 @@ class ConfigCenter {
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
@@ -81,6 +88,8 @@ class ConfigCenter {
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
|
||||
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
@@ -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
|
||||
@@ -8,8 +8,6 @@
|
||||
#define _DEVICE_CONTROLLER_FACTORY_H_
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "keyboard_capturer.h"
|
||||
#include "mouse_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -21,16 +19,7 @@ class DeviceControllerFactory {
|
||||
virtual ~DeviceControllerFactory() {}
|
||||
|
||||
public:
|
||||
DeviceController* Create(Device device) {
|
||||
switch (device) {
|
||||
case Mouse:
|
||||
return new MouseController();
|
||||
case Keyboard:
|
||||
return new KeyboardCapturer();
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
DeviceController* Create(Device device);
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _APPLICATION_STATE_H_
|
||||
#define _APPLICATION_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -192,4 +198,4 @@ struct ApplicationState : MainWindowState,
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#endif
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <slint.h>
|
||||
#include <tinyfiledialogs.h>
|
||||
@@ -22,6 +24,7 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -34,9 +37,10 @@
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/tray/win_tray.h"
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "platform/windows/gui/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/tray/mac_tray.h"
|
||||
#include "platform/macos/gui/tray/mac_tray.h"
|
||||
#include "platform/window_drag.h"
|
||||
#elif defined(__linux__)
|
||||
#include <GL/gl.h>
|
||||
@@ -44,7 +48,8 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform/tray/linux_tray.h"
|
||||
#include "platform/linux/gui/cuda_graphics.h"
|
||||
#include "platform/linux/gui/tray/linux_tray.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
#include "server_window_state.h"
|
||||
@@ -989,6 +994,15 @@ int GuiApplication::Run() {
|
||||
if (!InitializeSDL()) {
|
||||
return -1;
|
||||
}
|
||||
#if _WIN32
|
||||
const auto backend = ConfigureWindowsSlintBackend();
|
||||
if (!backend.success) {
|
||||
LOG_ERROR("{}", backend.diagnostic);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
|
||||
#endif
|
||||
InitializeModules();
|
||||
InitializeUi();
|
||||
|
||||
@@ -1017,6 +1031,7 @@ void GuiApplication::InitializeSettings() {
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
|
||||
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||
// position. Use the session's XWayland compatibility server for the GUI so
|
||||
// auxiliary windows such as the controlled-side status window can be
|
||||
@@ -1203,6 +1218,8 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_language_index(language_button_value_);
|
||||
ui_->main->set_video_quality_index(video_quality_button_value_);
|
||||
ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
|
||||
ui_->main->set_adaptation_policy_index(
|
||||
video_adaptation_policy_button_value_);
|
||||
ui_->main->set_codec_index(video_encode_format_button_value_);
|
||||
ui_->main->set_hardware_codec_enabled(enable_hardware_video_codec_);
|
||||
ui_->main->set_turn_enabled(enable_turn_);
|
||||
@@ -1723,7 +1740,7 @@ void GuiApplication::BindServerCallbacks() {
|
||||
});
|
||||
server->on_disconnect_controller([this] {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
CloseServerController(selected_server_remote_id_);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1733,6 +1750,7 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
if (!peer_) {
|
||||
@@ -1807,60 +1825,142 @@ void GuiApplication::Tick() {
|
||||
}
|
||||
|
||||
void GuiApplication::ShareLocalCursorState() {
|
||||
constexpr auto kCursorEchoSuppressionInterval = 300ms;
|
||||
constexpr auto kCursorStateHeartbeatInterval = 500ms;
|
||||
constexpr auto kCursorPositionShareInterval = 16ms;
|
||||
constexpr float kCursorPositionEpsilon = 0.00005f;
|
||||
|
||||
bool has_connected_controller = false;
|
||||
std::vector<std::string> connected_controllers;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
has_connected_controller =
|
||||
std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||
[](const auto& entry) {
|
||||
return entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
connected_controllers.reserve(connection_status_.size());
|
||||
for (const auto& [remote_id, status] : connection_status_) {
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
connected_controllers.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_server_mode_ || !peer_ || !has_connected_controller) {
|
||||
has_shared_cursor_state_ = false;
|
||||
last_cursor_state_share_time_ = {};
|
||||
if (!is_server_mode_ || !peer_ || connected_controllers.empty()) {
|
||||
cursor_delivery_states_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
CursorState sampled{};
|
||||
if (!cursor_state_provider_.Sample(&sampled)) {
|
||||
if (!cursor_state_provider_.Sample(devices_.display_info_list(),
|
||||
selected_display_, &sampled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool changed =
|
||||
!has_shared_cursor_state_ ||
|
||||
sampled.visible != last_shared_cursor_state_.visible ||
|
||||
sampled.shape != last_shared_cursor_state_.shape;
|
||||
const bool heartbeat_due =
|
||||
last_cursor_state_share_time_.time_since_epoch().count() == 0 ||
|
||||
now - last_cursor_state_share_time_ >= kCursorStateHeartbeatInterval;
|
||||
if (!changed && !heartbeat_due) {
|
||||
return;
|
||||
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
|
||||
last_input_times;
|
||||
{
|
||||
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||
for (const auto& remote_id : connected_controllers) {
|
||||
const auto input_it = last_remote_pointer_input_time_.find(remote_id);
|
||||
if (input_it != last_remote_pointer_input_time_.end()) {
|
||||
last_input_times.emplace(remote_id, input_it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> connected_ids(connected_controllers.begin(),
|
||||
connected_controllers.end());
|
||||
std::erase_if(cursor_delivery_states_, [&](const auto& entry) {
|
||||
return connected_ids.find(entry.first) == connected_ids.end();
|
||||
});
|
||||
|
||||
struct CursorRecipient {
|
||||
std::string remote_id;
|
||||
bool include_position = true;
|
||||
};
|
||||
std::vector<CursorRecipient> recipients;
|
||||
for (const auto& remote_id : connected_controllers) {
|
||||
auto& delivery = cursor_delivery_states_[remote_id];
|
||||
const auto input_it = last_input_times.find(remote_id);
|
||||
const bool suppress_position =
|
||||
input_it != last_input_times.end() &&
|
||||
now - input_it->second < kCursorEchoSuppressionInterval;
|
||||
delivery.feedback_pending =
|
||||
delivery.feedback_pending || suppress_position;
|
||||
|
||||
const CursorState& previous = delivery.last_sent;
|
||||
const bool shape_changed =
|
||||
!delivery.has_sent || sampled.visible != previous.visible ||
|
||||
sampled.shape != previous.shape ||
|
||||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
|
||||
kCursorPositionEpsilon ||
|
||||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
|
||||
kCursorPositionEpsilon;
|
||||
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
|
||||
// one-logical-pixel threshold became several visible phone points.
|
||||
const bool position_changed =
|
||||
!delivery.has_sent ||
|
||||
sampled.position_valid != previous.position_valid ||
|
||||
sampled.display_id != previous.display_id ||
|
||||
(sampled.position_valid && previous.position_valid &&
|
||||
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
|
||||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon));
|
||||
const bool position_update_due =
|
||||
position_changed &&
|
||||
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
||||
now - delivery.last_sent_time >= kCursorPositionShareInterval);
|
||||
const bool heartbeat_due =
|
||||
delivery.last_sent_time.time_since_epoch().count() == 0 ||
|
||||
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
|
||||
if (suppress_position) {
|
||||
// Cursor appearance is independent of cursor position. Continue sending
|
||||
// shape changes to every controller while withholding only the sampled
|
||||
// position from the connection that originated recent input.
|
||||
if (shape_changed || heartbeat_due) {
|
||||
recipients.push_back({remote_id, false});
|
||||
}
|
||||
} else if (delivery.feedback_pending || shape_changed ||
|
||||
position_update_due || heartbeat_due) {
|
||||
recipients.push_back({remote_id, true});
|
||||
}
|
||||
}
|
||||
if (recipients.empty()) return;
|
||||
|
||||
sampled.seq = ++cursor_state_sequence_;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::cursor_state;
|
||||
action.cs = sampled;
|
||||
const std::string message = action.to_json();
|
||||
const int result = SendDataFrame(peer_, message.c_str(), message.size(),
|
||||
mouse_label_.c_str());
|
||||
if (result != 0) {
|
||||
LOG_WARN("Send cursor state failed, ret={}", result);
|
||||
return;
|
||||
}
|
||||
for (const auto& recipient : recipients) {
|
||||
auto& delivery = cursor_delivery_states_[recipient.remote_id];
|
||||
CursorState outgoing = sampled;
|
||||
outgoing.position_update = recipient.include_position;
|
||||
if (!recipient.include_position) {
|
||||
// Keep legacy receivers at their last acknowledged position as well.
|
||||
// New receivers honor position_update=false and leave their optimistic
|
||||
// local position untouched.
|
||||
outgoing.position_valid =
|
||||
delivery.has_sent && delivery.last_sent.position_valid;
|
||||
outgoing.x = delivery.has_sent ? delivery.last_sent.x : 0.5f;
|
||||
outgoing.y = delivery.has_sent ? delivery.last_sent.y : 0.5f;
|
||||
outgoing.display_id = delivery.has_sent
|
||||
? delivery.last_sent.display_id
|
||||
: -1;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LOG_INFO("Sent cursor state: seq={}, visible={}, shape={}", sampled.seq,
|
||||
sampled.visible, static_cast<int>(sampled.shape));
|
||||
}
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::cursor_state;
|
||||
action.cs = outgoing;
|
||||
const std::string message = action.to_json();
|
||||
const int result = SendDataFrameToPeer(
|
||||
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
|
||||
recipient.remote_id.c_str(), recipient.remote_id.size());
|
||||
if (result != 0) {
|
||||
LOG_WARN("Send cursor state to [{}] failed, ret={}",
|
||||
recipient.remote_id, result);
|
||||
continue;
|
||||
}
|
||||
|
||||
last_shared_cursor_state_ = sampled;
|
||||
has_shared_cursor_state_ = true;
|
||||
last_cursor_state_share_time_ = now;
|
||||
delivery.last_sent = outgoing;
|
||||
delivery.has_sent = true;
|
||||
if (recipient.include_position) {
|
||||
delivery.feedback_pending = false;
|
||||
}
|
||||
delivery.last_sent_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::HandlePasswordChangeResult() {
|
||||
@@ -1911,8 +2011,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
}
|
||||
show_offline_warning_window_ = true;
|
||||
if (uncertain && peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1925,8 +2024,7 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
|
||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
||||
if (peer_) {
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1950,7 +2048,7 @@ void GuiApplication::HandleCredentialRecovery() {
|
||||
if (retry_active) {
|
||||
LOG_INFO("Pending credential was not accepted; retrying active credential");
|
||||
if (peer_) {
|
||||
DestroyPeer(&peer_);
|
||||
CloseConnectionPeer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2819,6 +2917,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
std::clamp(main->get_video_quality_index(), 0, 2);
|
||||
video_frame_rate_button_value_ =
|
||||
std::clamp(main->get_frame_rate_index(), 0, 1);
|
||||
video_adaptation_policy_button_value_ =
|
||||
std::clamp(main->get_adaptation_policy_index(), 0, 2);
|
||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||
enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
|
||||
enable_turn_ = main->get_turn_enabled();
|
||||
@@ -2835,6 +2935,9 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
||||
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
||||
video_frame_rate_button_value_));
|
||||
config_center_->SetVideoAdaptationPolicy(
|
||||
static_cast<ConfigCenter::VIDEO_ADAPTATION_POLICY>(
|
||||
video_adaptation_policy_button_value_));
|
||||
config_center_->SetVideoEncodeFormat(
|
||||
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
||||
video_encode_format_button_value_));
|
||||
@@ -2866,6 +2969,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
language_button_value_last_ = language_button_value_;
|
||||
video_quality_button_value_last_ = video_quality_button_value_;
|
||||
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
|
||||
video_adaptation_policy_button_value_last_ =
|
||||
video_adaptation_policy_button_value_;
|
||||
video_encode_format_button_value_last_ = video_encode_format_button_value_;
|
||||
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
|
||||
enable_turn_last_ = enable_turn_;
|
||||
@@ -2979,9 +3084,6 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
||||
}
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
|
||||
CloseRemoteSession(props);
|
||||
WaitForThumbnailSaveTasks();
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
ResetRemoteSessionResources(props);
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
@@ -3186,7 +3288,7 @@ void GuiApplication::Cleanup() {
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForThumbnailSaveTasks();
|
||||
WaitForSessionCleanup();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#if defined(__APPLE__)
|
||||
@@ -1,9 +1,18 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#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"
|
||||
@@ -21,6 +30,12 @@ public:
|
||||
|
||||
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();
|
||||
@@ -73,10 +88,9 @@ private:
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
CursorState last_shared_cursor_state_{};
|
||||
bool has_shared_cursor_state_ = false;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
uint32_t cursor_state_sequence_ = 0;
|
||||
std::chrono::steady_clock::time_point last_cursor_state_share_time_{};
|
||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
@@ -86,4 +100,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_H_
|
||||
#endif
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
@@ -233,6 +235,15 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
#pragma once
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SERVER_WINDOW_STATE_H_
|
||||
#define _SERVER_WINDOW_STATE_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
@@ -24,5 +31,6 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
}
|
||||
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||
}
|
||||
} // namespace crossdesk::server_window_state
|
||||
|
||||
} // namespace crossdesk::server_window_state
|
||||
#endif
|
||||
@@ -1,4 +1,11 @@
|
||||
#pragma once
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_GEOMETRY_H_
|
||||
#define _WINDOW_GEOMETRY_H_
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -38,3 +45,5 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||
}
|
||||
|
||||
} // namespace crossdesk::window_geometry
|
||||
|
||||
#endif
|
||||
@@ -120,9 +120,15 @@ struct TranslationRow {
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_adaptation_policy, u8"画面偏好:", "Video Preference:", \
|
||||
u8"Режим видео:") \
|
||||
X(video_priority_frame_rate, u8"帧率优先", "Frame Rate", \
|
||||
u8"Частота кадров") \
|
||||
X(video_priority_quality, u8"画质优先", "Quality", u8"Качество") \
|
||||
X(video_priority_balanced, u8"平衡", "Balanced", u8"Баланс") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "features/devices/session_device_manager.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include <display_stream_id.h>
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -72,7 +74,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
|
||||
const int init_ret = screen_capturer_->Init(
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
const char *display_name) {
|
||||
const char *display_name,
|
||||
const MiniRtcNativeVideoFrame *native_frame) {
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
@@ -134,12 +137,13 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
}
|
||||
}
|
||||
|
||||
XVideoFrame frame{};
|
||||
MiniRtcVideoFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
frame.native_frame = native_frame;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
@@ -202,8 +206,11 @@ int SessionDeviceManager::StartSpeakerCapturer() {
|
||||
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());
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
SendAudioFrame(owner_.peer_, &frame, owner_.audio_label_.c_str());
|
||||
});
|
||||
|
||||
if (init_ret != 0) {
|
||||
@@ -461,7 +468,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.mouse_controller_is_started_ = false;
|
||||
}
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#if defined(__linux__) || defined(__APPLE__)
|
||||
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
|
||||
mouse_controller_) {
|
||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "device_controller_factory.h"
|
||||
#include "display_info.h"
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
@@ -21,6 +20,16 @@ namespace crossdesk {
|
||||
|
||||
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
FileTransferManager::~FileTransferManager() {
|
||||
std::vector<std::future<void>> workers;
|
||||
{
|
||||
std::lock_guard lock(workers_mutex_);
|
||||
stopping_ = true;
|
||||
workers.swap(workers_);
|
||||
}
|
||||
for (auto& worker : workers) worker.wait();
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
|
||||
return global_state_;
|
||||
}
|
||||
@@ -93,8 +102,16 @@ void FileTransferManager::Start(
|
||||
std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path, const std::string &file_label,
|
||||
const std::string &remote_id) {
|
||||
if (props && props->closing_) return;
|
||||
const bool is_global = !props;
|
||||
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
|
||||
PeerPtr* peer = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events;
|
||||
{
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
if (props && props->closing_) return;
|
||||
peer = is_global ? owner_.peer_ : props->peer_;
|
||||
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
|
||||
}
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
@@ -109,7 +126,14 @@ void FileTransferManager::Start(
|
||||
}
|
||||
|
||||
const auto props_weak = std::weak_ptr<RemoteSession>(props);
|
||||
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
|
||||
std::lock_guard workers_lock(workers_mutex_);
|
||||
if (stopping_) return;
|
||||
// Reap finished senders without blocking the UI.
|
||||
std::erase_if(workers_, [](auto& worker) {
|
||||
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
});
|
||||
workers_.push_back(std::async(std::launch::async,
|
||||
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
FileTransferState *state = nullptr;
|
||||
@@ -167,7 +191,11 @@ void FileTransferManager::Start(
|
||||
|
||||
const int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label, remote_id](const char *buffer, size_t size) {
|
||||
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
|
||||
// Close drains each in-flight send before detaching the peer.
|
||||
if (props_locked && props_locked->closing_) return -1;
|
||||
auto callback = peer_events->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buffer, size,
|
||||
file_label.c_str());
|
||||
@@ -201,12 +229,13 @@ void FileTransferManager::Start(
|
||||
}
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
ProcessQueue(props_locked);
|
||||
}).detach();
|
||||
if (peer_events->IsActive()) ProcessQueue(props_locked);
|
||||
}));
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props && props->closing_) return;
|
||||
FileTransferState &state = state_for(props);
|
||||
if (state.file_sending_.load()) {
|
||||
return;
|
||||
@@ -236,15 +265,9 @@ void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||
if (size < sizeof(FileTransferAck)) {
|
||||
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferAck ack{};
|
||||
std::memcpy(&ack, data, sizeof(ack));
|
||||
if (ack.magic != kFileAckMagic) {
|
||||
LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic);
|
||||
if (!DecodeFileTransferAck(data, size, &ack)) {
|
||||
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -21,6 +24,7 @@ public:
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
explicit FileTransferManager(GuiRuntime &owner);
|
||||
~FileTransferManager();
|
||||
|
||||
FileTransferState &global_state();
|
||||
FileTransferState &
|
||||
@@ -40,6 +44,9 @@ private:
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void Unregister(uint32_t file_id, bool per_peer);
|
||||
|
||||
std::mutex workers_mutex_;
|
||||
std::vector<std::future<void>> workers_;
|
||||
bool stopping_ = false;
|
||||
GuiRuntime &owner_;
|
||||
FileTransferState global_state_;
|
||||
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
@@ -148,6 +150,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
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() !=
|
||||
@@ -187,6 +190,7 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
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() ==
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -301,6 +301,8 @@ int SettingsManager::Load() {
|
||||
static_cast<int>(owner_.config_center_->GetVideoQuality());
|
||||
owner_.video_frame_rate_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
|
||||
owner_.video_adaptation_policy_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoAdaptationPolicy());
|
||||
owner_.video_encode_format_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
|
||||
owner_.enable_hardware_video_codec_ =
|
||||
@@ -324,6 +326,10 @@ int SettingsManager::Load() {
|
||||
|
||||
owner_.language_button_value_last_ = owner_.language_button_value_;
|
||||
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
|
||||
owner_.video_frame_rate_button_value_last_ =
|
||||
owner_.video_frame_rate_button_value_;
|
||||
owner_.video_adaptation_policy_button_value_last_ =
|
||||
owner_.video_adaptation_policy_button_value_;
|
||||
owner_.video_encode_format_button_value_last_ =
|
||||
owner_.video_encode_format_button_value_;
|
||||
owner_.enable_hardware_video_codec_last_ =
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RENDER_H_
|
||||
#define CROSSDESK_GUI_RENDER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _RENDER_H_
|
||||
#define _RENDER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -25,4 +31,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDER_H_
|
||||
#endif
|
||||
@@ -14,14 +14,14 @@
|
||||
#include "rd_log.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "platform/metal_video_renderer.h"
|
||||
#include "platform/macos/gui/metal_video_renderer.h"
|
||||
#else
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <GL/gl.h>
|
||||
|
||||
#include "platform/opengl_video_renderer.h"
|
||||
#include "platform/common/gui/opengl_video_renderer.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_VIDEO_PRESENTER_H_
|
||||
#define _SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -72,4 +78,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#endif
|
||||
@@ -6,9 +6,10 @@
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include <display_stream_id.h>
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
@@ -123,6 +124,10 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
|
||||
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;
|
||||
@@ -216,6 +221,11 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
if (!props || props->closing_.exchange(true)) return;
|
||||
if (props->peer_events_) props->peer_events_->Deactivate();
|
||||
props->connection_established_ = false;
|
||||
props->streaming_ = false;
|
||||
props->connection_status_.store(ConnectionStatus::Closed);
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
@@ -241,85 +251,160 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
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_);
|
||||
PeerPtr* peer = nullptr;
|
||||
{
|
||||
// Audio/clipboard senders hold a shared map lock while using this pointer.
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(props->peer_, nullptr);
|
||||
}
|
||||
const auto queued_at = std::chrono::steady_clock::now();
|
||||
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
|
||||
[props, peer, frame_snapshot, video_width, video_height,
|
||||
thumbnail = thumbnail_, queued_at]() mutable {
|
||||
if (peer) {
|
||||
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
|
||||
props->remote_id_);
|
||||
LeaveConnection(peer, props->remote_id_.c_str());
|
||||
DestroyPeer(&peer);
|
||||
}
|
||||
if (thumbnail && frame_snapshot && !frame_snapshot->empty() &&
|
||||
video_width > 0 && video_height > 0) {
|
||||
thumbnail->SaveToThumbnail(
|
||||
reinterpret_cast<char*>(frame_snapshot->data()), video_width,
|
||||
video_height, props->remote_id_, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - queued_at).count();
|
||||
LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_,
|
||||
elapsed);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseServerController(const std::string& remote_id) {
|
||||
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
|
||||
return;
|
||||
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
|
||||
[peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); });
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseConnectionPeer() {
|
||||
if (!peer_) return;
|
||||
peer_events_->Deactivate();
|
||||
std::vector<std::string> controllers;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
|
||||
}
|
||||
for (const auto& id : controllers)
|
||||
HandleServerControllerDisconnected(id, "peer_closed");
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
signal_connected_ = false;
|
||||
signal_status_ = SignalStatus::SignalClosed;
|
||||
PeerPtr* peer;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(peer_, nullptr);
|
||||
}
|
||||
// The same serial queue also owns outstanding controller disconnects, so
|
||||
// these finish before the shared peer is destroyed.
|
||||
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
|
||||
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
|
||||
LOG_INFO("[{}] Background connection peer cleanup", id);
|
||||
LeaveConnection(peer, id.c_str());
|
||||
DestroyPeer(&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_);
|
||||
}
|
||||
|
||||
CloseConnectionPeer();
|
||||
is_client_mode_ = false;
|
||||
pending_reconnects_.clear();
|
||||
std::vector<std::shared_ptr<RemoteSession>> sessions;
|
||||
{
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto& it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
CloseRemoteSession(props);
|
||||
}
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (const auto& [id, props] : remote_sessions_) sessions.push_back(props);
|
||||
}
|
||||
|
||||
for (const auto& props : sessions) CloseRemoteSession(props);
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
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_);
|
||||
void GuiRuntime::HandleSessionCleanup() {
|
||||
auto completed = [](std::future<void>& task) {
|
||||
if (!task.valid() || task.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) return false;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
completed(connection_peer_cleanup_);
|
||||
for (auto it = controller_cleanup_tasks_.begin();
|
||||
it != controller_cleanup_tasks_.end();) {
|
||||
if (completed(it->second)) it = controller_cleanup_tasks_.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
|
||||
if (threads_to_join.empty()) {
|
||||
return;
|
||||
std::vector<std::pair<std::string, PendingReconnect>> reconnects;
|
||||
for (auto it = session_cleanup_tasks_.begin();
|
||||
it != session_cleanup_tasks_.end();) {
|
||||
if (it->second.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
it->second.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what());
|
||||
}
|
||||
if (auto pending = pending_reconnects_.find(it->first);
|
||||
pending != pending_reconnects_.end()) {
|
||||
reconnects.emplace_back(pending->first, std::move(pending->second));
|
||||
pending_reconnects_.erase(pending);
|
||||
}
|
||||
it = session_cleanup_tasks_.erase(it);
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
}
|
||||
for (const auto& [id, request] : reconnects) {
|
||||
ConnectTo(id, request.password.c_str(), request.remember_password);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& thread : threads_to_join) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
void GuiRuntime::WaitForSessionCleanup() {
|
||||
pending_reconnects_.clear();
|
||||
auto drain = [](std::future<void>& task) {
|
||||
if (!task.valid()) return;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
};
|
||||
drain(connection_peer_cleanup_);
|
||||
for (auto& [id, task] : controller_cleanup_tasks_) drain(task);
|
||||
controller_cleanup_tasks_.clear();
|
||||
for (auto& [id, task] : session_cleanup_tasks_) {
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what());
|
||||
}
|
||||
}
|
||||
session_cleanup_tasks_.clear();
|
||||
}
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
@@ -361,6 +446,11 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||
|
||||
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (session_cleanup_tasks_.count(remote_id)) {
|
||||
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
|
||||
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
|
||||
return 0;
|
||||
}
|
||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
@@ -378,16 +468,14 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
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();
|
||||
bool exists = FindRemoteSession(remote_id) != nullptr;
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr* peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
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];
|
||||
@@ -395,6 +483,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
props->params_.user_data = props->peer_events_.get();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
@@ -441,8 +531,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
|
||||
int ret = -1;
|
||||
// std::shared_lock read_lock(remote_sessions_mutex_);
|
||||
auto props = remote_sessions_[remote_id];
|
||||
auto props = FindRemoteSession(remote_id);
|
||||
if (!props || props->closing_) return -1;
|
||||
if (!props->connection_established_) {
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
@@ -466,7 +556,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
}
|
||||
}
|
||||
// read_lock.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#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
|
||||
@@ -1,9 +1,13 @@
|
||||
#include "runtime/cursor_state_provider.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "runtime/cursor_position.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
@@ -39,7 +43,8 @@ struct CursorStateProvider::Impl {};
|
||||
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||
CursorStateProvider::~CursorStateProvider() = default;
|
||||
|
||||
bool CursorStateProvider::Sample(CursorState* state) {
|
||||
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state) {
|
||||
if (!state) return false;
|
||||
|
||||
CURSORINFO info{};
|
||||
@@ -50,6 +55,8 @@ bool CursorStateProvider::Sample(CursorState* state) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -65,6 +72,7 @@ bool CursorStateProvider::Sample(CursorState* state) {
|
||||
#include "linux_cursor_shape.h"
|
||||
#include "platform.h"
|
||||
#include "shared_cursor_state.h"
|
||||
#include "runtime/cursor_position.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -91,8 +99,10 @@ struct CursorStateProvider::Impl {
|
||||
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||
CursorStateProvider::~CursorStateProvider() = default;
|
||||
|
||||
bool CursorStateProvider::Sample(CursorState* state) {
|
||||
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state) {
|
||||
if (!state || !impl_) return false;
|
||||
ResetCursorPosition(state);
|
||||
|
||||
if (IsWaylandSession()) {
|
||||
SharedCursorState shared{};
|
||||
@@ -115,6 +125,8 @@ bool CursorStateProvider::Sample(CursorState* state) {
|
||||
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;
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
#ifndef CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||
#define CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CURSOR_STATE_PROVIDER_H_
|
||||
#define _CURSOR_STATE_PROVIDER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "display_info.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -17,7 +26,8 @@ class CursorStateProvider {
|
||||
CursorStateProvider(const CursorStateProvider&) = delete;
|
||||
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
|
||||
|
||||
bool Sample(CursorState* state);
|
||||
bool Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
@@ -26,4 +36,4 @@ class CursorStateProvider {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_
|
||||
#endif
|
||||
@@ -1,11 +1,11 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-28
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#ifndef _DEVICE_PRESENCE_CACHE_H_
|
||||
#define _DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -38,4 +38,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#endif
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "display_stream_id.h"
|
||||
#include <display_stream_id.h>
|
||||
#include "localization.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
@@ -19,11 +19,17 @@ namespace crossdesk {
|
||||
|
||||
GuiRuntime::GuiRuntime()
|
||||
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
||||
keyboard_(*this), peer_events_(*this) {}
|
||||
keyboard_(*this) {}
|
||||
|
||||
GuiRuntime::~GuiRuntime() = default;
|
||||
GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); }
|
||||
|
||||
int GuiRuntime::CreateConnectionPeer() {
|
||||
// Tick retries after the old peer's leave notification and socket shutdown.
|
||||
if (connection_peer_cleanup_.valid()) return 0;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
}
|
||||
params_.use_cfg_file = false;
|
||||
|
||||
std::string signal_server_ip;
|
||||
@@ -141,6 +147,14 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
|
||||
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
|
||||
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
|
||||
// platforms fall back to a packed CPU copy when native upload is unavailable.
|
||||
params_.native_video_output = true;
|
||||
#else
|
||||
params_.native_video_output = false;
|
||||
#endif
|
||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||
? true
|
||||
@@ -155,8 +169,21 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
switch (config_center_->GetVideoAdaptationPolicy()) {
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::FRAME_RATE_PRIORITY:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::BALANCED:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::Balanced;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY:
|
||||
default:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
break;
|
||||
}
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
@@ -168,14 +195,17 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||
|
||||
params_.user_data = &peer_events_;
|
||||
params_.user_data = peer_events_.get();
|
||||
|
||||
// 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(¶ms_);
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
}
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
Init(peer_);
|
||||
@@ -219,7 +249,7 @@ void GuiRuntime::UpdateLabels() {
|
||||
|
||||
|
||||
void GuiRuntime::HandleRecentConnections() {
|
||||
if (reload_recent_connections_ && thumbnail_) {
|
||||
if (session_cleanup_tasks_.empty() && 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_,
|
||||
@@ -248,11 +278,22 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
|
||||
if (1) {
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
uint64_t captured_timestamp = 0;
|
||||
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,
|
||||
if (captured_timestamp == 0) {
|
||||
const int64_t timestamp_us = GetSystemTimeMicros(props->peer_);
|
||||
if (timestamp_us > 0) {
|
||||
captured_timestamp = static_cast<uint64_t>(timestamp_us);
|
||||
}
|
||||
}
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(stream);
|
||||
frame.size = static_cast<size_t>(len);
|
||||
frame.captured_timestamp = captured_timestamp;
|
||||
SendAudioFrame(props->peer_, &frame,
|
||||
runtime->audio_label_.c_str());
|
||||
}
|
||||
}
|
||||
@@ -269,29 +310,6 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
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
|
||||
@@ -1,5 +1,11 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_RUNTIME_H_
|
||||
#define _GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
@@ -55,8 +61,11 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
void CloseConnectionPeer();
|
||||
void CloseServerController(const std::string& remote_id);
|
||||
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
|
||||
void WaitForThumbnailSaveTasks();
|
||||
void HandleSessionCleanup();
|
||||
void WaitForSessionCleanup();
|
||||
std::shared_ptr<RemoteSession> FindRemoteSession(
|
||||
const std::string& remote_id);
|
||||
|
||||
@@ -83,7 +92,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
FileTransferManager transfers_;
|
||||
SettingsManager settings_;
|
||||
KeyboardController keyboard_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||
|
||||
@@ -98,4 +107,4 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_STATE_H_
|
||||
#define _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
|
||||