Compare commits

...
Author SHA1 Message Date
dijunkun 16348444f0 [feat] add Windows native video rendering pipeline 2026-09-03 01:53:34 +08:00
dijunkun e716346b26 [fix] stabilize frame rate priority adaptation 2026-09-02 11:05:13 +08:00
dijunkun b7e1adbec6 [feat] add video adaptation preference setting 2026-09-02 01:05:30 +08:00
dijunkun 58f2daa9c1 [fix] prioritize remote desktop resolution quality 2026-09-01 20:21:03 +08:00
dijunkun 8752e51921 [fix] build CI test targets separately 2026-09-01 08:33:59 +08:00
dijunkun a8cb0e21ce [fix] load desktop options on first configuration 2026-09-01 01:43:09 +08:00
dijunkun 90184f5251 [refactor] reorganize platform and wire sources 2026-09-01 01:29:30 +08:00
dijunkun fb0ab4f14e [fix] isolate iOS native build environment 2026-08-31 10:49:38 +08:00
dijunkun 0e3f3cec00 [fix] pin iOS CI xmake version 2026-08-31 10:39:36 +08:00
dijunkun 72af49eb80 [fix] use isolated iOS host build tools 2026-08-31 10:31:49 +08:00
dijunkun 36f05834b5 [fix] stabilize clean iOS dependency builds 2026-08-31 10:19:35 +08:00
dijunkun 1a190fbaf5 [fix] integrate iOS build workflow 2026-08-31 10:03:07 +08:00
dijunkun 1fe236da76 [fix] pin iOS CI package repository 2026-08-31 09:49:59 +08:00
dijunkun 3b989fb70d [fix] set up Python for iOS CI 2026-08-31 02:20:46 +08:00
dijunkun 7595ab8bd8 [feat] add native iOS client support 2026-08-31 02:14:27 +08:00
dijunkun 126f455681 [fix] synchronize Linux Wayland cursor shapes 2026-08-27 09:41:36 +08:00
dijunkun d6d0a548b2 [fix] rename remote cursor shape to avoid X11 macro conflict 2026-08-26 00:01:25 +08:00
dijunkun 74ac849efe Merge branch 'slint-client' of https://github.com/kunkundi/crossdesk into slint-client 2026-08-25 23:25:27 +08:00
dijunkun e2cd18dba4 [fix] detect macOS remote cursor shapes 2026-08-25 23:24:20 +08:00
dijunkun d2b1329eca [fix] restore Linux cursor state build compatibility 2026-08-25 22:59:45 +08:00
dijunkun 0653a95250 [fix] update minirtc NVDEC reconfigure handling 2026-08-25 22:52:41 +08:00
dijunkun bd73ef55f1 [feat] synchronize remote cursor state 2026-08-25 22:25:18 +08:00
dijunkun b854512c24 [feat] integrate Linux tray with desktop environment 2026-08-25 18:17:53 +08:00
dijunkun 4528caf0f4 [feat] refine tray and main window behavior 2026-08-25 17:14:45 +08:00
dijunkun f6e89725aa [fix] update minirtc NVENC fallback 2026-08-25 16:39:13 +08:00
dijunkun 62f79a8e44 [fix] update minirtc video adaptation 2026-08-25 16:18:55 +08:00
dijunkun 096bc68d35 [fix] prevent remote mouse input from popup menus 2026-08-25 12:43:30 +08:00
dijunkun 8ea4668305 [refactor] modularize Slint video rendering backends 2026-08-25 11:45:38 +08:00
dijunkun 21eec76008 [feat] add OpenGL NV12 underlay rendering for Slint on Linux 2026-08-25 10:26:05 +08:00
dijunkun 3e63675787 [fix] update minirtc NVDEC resolution handling 2026-08-25 05:28:39 +08:00
dijunkun 439f137764 [fix] update minirtc signal join sequencing 2026-08-25 04:40:37 +08:00
dijunkun f537147f54 [fix] update minirtc ICE state handling 2026-08-25 04:28:34 +08:00
361 changed files with 15289 additions and 4693 deletions
+12 -10
View File
@@ -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/**
+5 -5
View File
@@ -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:
+190 -11
View File
@@ -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
+6 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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`。
+2 -2
View File
@@ -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
@@ -17,9 +17,10 @@ MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
normalize_app_version() {
local input="$1"
@@ -141,11 +142,11 @@ 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
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
fi
@@ -157,7 +158,7 @@ Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
@@ -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
+76
View File
@@ -0,0 +1,76 @@
#include "daemon.h"
#include <chrono>
#include <iostream>
#include <thread>
#include <utility>
#include "platform/daemon_backend.h"
namespace {
constexpr int kRestartDelayMs = 1000;
} // namespace
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
void Daemon::stop() {
running_.store(false);
crossdesk::platform::RequestDaemonStop();
}
bool Daemon::isRunning() const {
return running_.load() && !crossdesk::platform::DaemonStopRequested();
}
bool Daemon::start(MainLoopFunc loop) {
crossdesk::platform::ResetDaemonStop();
if (!crossdesk::platform::PrepareDaemon()) return false;
running_.store(true);
return runWithRestart(std::move(loop));
}
bool Daemon::runWithRestart(MainLoopFunc loop) {
int restart_count = 0;
const std::string executable_path =
crossdesk::platform::GetDaemonExecutablePath();
if (executable_path.empty()) {
std::cerr
<< "Failed to get executable path, falling back to direct execution"
<< std::endl;
while (isRunning()) {
try {
loop();
break;
} catch (...) {
++restart_count;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(kRestartDelayMs));
}
}
return true;
}
while (isRunning()) {
const crossdesk::platform::DaemonChildResult result =
crossdesk::platform::RunDaemonChild(
executable_path, [this] { return isRunning(); });
if (!isRunning() || result.normal_exit) break;
++restart_count;
if (!result.started) {
std::cerr << "Failed to create child process";
} else if (result.terminated_by_signal) {
std::cerr << "Child process crashed with signal " << result.exit_code;
} else {
std::cerr << "Child process exited with code " << result.exit_code;
}
std::cerr << ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
}
return true;
}
@@ -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_;
};
+21
View File
@@ -0,0 +1,21 @@
#include "autostart.h"
#include "platform/autostart_backend.h"
namespace crossdesk {
bool EnableAutostart(const std::string& app_name) {
const std::string executable_path = platform::GetAutostartExecutablePath();
return !executable_path.empty() &&
platform::EnableAutostart(app_name, executable_path);
}
bool DisableAutostart(const std::string& app_name) {
return platform::DisableAutostart(app_name);
}
bool IsAutostartEnabled(const std::string& app_name) {
return platform::IsAutostartEnabled(app_name);
}
} // namespace crossdesk
@@ -7,7 +7,7 @@
#ifndef _PLATFORM_H_
#define _PLATFORM_H_
#include <iostream>
#include <string>
namespace crossdesk {
@@ -0,0 +1,44 @@
#include "shared_cursor_state.h"
#include <remote_action.h>
#include <mutex>
namespace crossdesk {
namespace {
std::mutex g_cursor_state_mutex;
SharedCursorState g_cursor_state;
bool g_cursor_state_available = false;
} // namespace
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = visible;
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
g_cursor_state_available = true;
}
bool GetSharedCursorState(SharedCursorState* state) {
if (!state) {
return false;
}
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
if (!g_cursor_state_available) {
return false;
}
*state = g_cursor_state;
return true;
}
void ClearSharedCursorState() {
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
++g_cursor_state.generation;
g_cursor_state.visible = false;
g_cursor_state.shape = RemoteCursorShape::none;
g_cursor_state_available = false;
}
} // namespace crossdesk
@@ -0,0 +1,22 @@
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
#include <cstdint>
#include <remote_cursor_shape.h>
namespace crossdesk {
struct SharedCursorState {
uint64_t generation = 0;
bool visible = false;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
bool GetSharedCursorState(SharedCursorState* state);
void ClearSharedCursorState();
} // namespace crossdesk
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
@@ -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)
@@ -34,6 +40,8 @@ int ConfigCenter::Load() {
persist_config_migration |= ini_.Delete(section_, "screen_content");
persist_config_migration |=
ini_.Delete(section_, "enable_desktop_quality_optimization");
persist_config_migration |=
ini_.Delete(section_, "enable_minimize_to_tray");
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
@@ -50,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_)));
@@ -109,8 +129,6 @@ int ConfigCenter::Load() {
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
@@ -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_);
@@ -157,8 +177,6 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
@@ -207,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;
@@ -338,17 +368,6 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
return 0;
}
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
enable_minimize_to_tray_ = enable_minimize_to_tray;
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
@@ -408,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_;
}
@@ -448,8 +472,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
@@ -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);
@@ -43,7 +49,6 @@ class ConfigCenter {
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
@@ -54,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;
@@ -66,7 +72,6 @@ class ConfigCenter {
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
@@ -83,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;
@@ -94,7 +101,6 @@ class ConfigCenter {
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
@@ -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,10 +1,14 @@
#ifndef CROSSDESK_GUI_APPLICATION_H_
#define CROSSDESK_GUI_APPLICATION_H_
#ifndef _GUI_APPLICATION_H_
#define _GUI_APPLICATION_H_
#include <chrono>
#include <memory>
#include <string>
#include <unordered_map>
#include <remote_action.h>
#include "runtime/cursor_state_provider.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
@@ -20,6 +24,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();
@@ -32,6 +42,7 @@ private:
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
void ShareLocalCursorState();
void HandlePasswordChangeResult();
void HandleCredentialRecovery();
void SyncMainWindow();
@@ -41,14 +52,6 @@ private:
void SyncStreamVideoFrame();
void ScheduleNextVideoFrame();
void ConfigureStreamVideoRenderer();
#if defined(_WIN32)
void SubmitCachedFrameToOpenGl(
const std::shared_ptr<RemoteSession>& session);
#endif
#if defined(__APPLE__)
void SubmitCachedFrameToMetal(
const std::shared_ptr<RemoteSession>& session);
#endif
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
@@ -78,6 +81,10 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
CursorStateProvider cursor_state_provider_;
std::unordered_map<std::string, CursorDeliveryState>
cursor_delivery_states_;
uint32_t cursor_state_sequence_ = 0;
std::chrono::steady_clock::time_point next_video_frame_time_{};
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
@@ -87,4 +94,4 @@ private:
} // namespace crossdesk
#endif // CROSSDESK_GUI_APPLICATION_H_
#endif
@@ -134,8 +134,10 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
event.window.windowID == SDL_GetWindowID(main_window_)) {
if (!MinimizeMainWindowToTray()) {
SDL_HideWindow(main_window_);
}
break;
}
@@ -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;
@@ -150,7 +150,7 @@ int GuiApplication::DrawServerWindow() {
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
if (!main_window_) {
return false;
}
@@ -135,35 +135,58 @@ inline std::string FormatDisplayLabel(size_t display_index,
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
#if _WIN32
inline const wchar_t* GetWindowsLocalizedLabel(
int language_index, const char* key, const wchar_t* fallback,
std::vector<std::wstring>& cache) {
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText("exit_program", normalized_index);
if (utf8_text.empty()) {
cached_text = L"Exit";
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = L"Exit";
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length - 1));
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
wide_length);
return cached_text.c_str();
}
#endif
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText(key, normalized_index);
if (utf8_text.empty()) {
cached_text = fallback;
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length));
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
cached_text.data(), wide_length) <= 0) {
cached_text = fallback;
return cached_text.c_str();
}
cached_text.pop_back();
return cached_text.c_str();
}
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "show_main_window",
L"Show Main Window", cache);
}
inline const wchar_t* GetSettingsLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
cache);
}
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
cache);
}
#endif
} // namespace localization
} // namespace crossdesk
@@ -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"Низкое") \
@@ -215,8 +221,6 @@ struct TranslationRow {
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
@@ -233,6 +237,8 @@ struct TranslationRow {
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(show_main_window, u8"显示主界面", "Show Main Window", \
u8"Показать главное окно") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
@@ -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"
@@ -461,7 +463,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>
@@ -236,15 +235,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;
}
@@ -1,5 +1,7 @@
#include "features/input/keyboard_controller.h"
#include <remote_action.h>
#include <SDL3/SDL.h>
#include <cstdint>
@@ -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_ =
@@ -310,7 +312,6 @@ int SettingsManager::Load() {
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray();
#if _WIN32 && CROSSDESK_PORTABLE
owner_.portable_service_prompt_suppressed_ =
owner_.config_center_->IsPortableServicePromptSuppressed();
@@ -325,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_ =
@@ -333,7 +338,6 @@ int SettingsManager::Load() {
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_;
LOG_INFO("Load settings from cache file");
return 0;
@@ -0,0 +1,514 @@
#include "rendering/slint_video_presenter.h"
#include <libyuv.h>
#include <slint.h>
#include <algorithm>
#include <cmath>
#include <mutex>
#include <unordered_map>
#include <utility>
#include "nv12_scaler.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#if defined(__APPLE__)
#include "platform/macos/gui/metal_video_renderer.h"
#else
#if defined(_WIN32)
#include <windows.h>
#endif
#include <GL/gl.h>
#include "platform/common/gui/opengl_video_renderer.h"
#endif
namespace crossdesk {
namespace {
#if !defined(__APPLE__)
constexpr GLint kGlClampToEdge = 0x812F;
constexpr float kStreamWindowCornerRadius = 12.0f;
#else
constexpr int kMetalAttachmentAttemptLimit = 30;
#endif
struct VideoRenderSize {
int width = 0;
int height = 0;
bool operator==(const VideoRenderSize&) const = default;
};
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
int area_width, int area_height) {
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
area_height <= 0) {
return {};
}
const double scale =
std::min({1.0, static_cast<double>(area_width) / source_width,
static_cast<double>(area_height) / source_height});
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
width &= ~1;
height &= ~1;
return {std::min(width, source_width & ~1),
std::min(height, source_height & ~1)};
}
} // namespace
struct SlintVideoPresenter::Impl {
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
VideoRenderer& renderer;
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
StateProvider state_provider;
DirtyCallback dirty_callback;
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
std::vector<uint8_t> scaled_video_frame;
std::vector<uint8_t> scaled_video_frame_scratch;
#if defined(__APPLE__)
int attachment_attempts = 0;
bool skip_attachment_once = false;
#else
std::mutex gl_mutex;
std::vector<uint8_t> gl_conversion_frame;
std::vector<uint8_t> gl_pending_frame;
uint32_t gl_texture = 0;
VideoRenderSize gl_texture_size;
VideoRenderSize gl_image_size;
VideoRenderSize gl_pending_size;
bool gl_pending_dirty = false;
#endif
SurfaceState CurrentState() const {
return state_provider ? state_provider() : SurfaceState{};
}
void MarkDirty() const {
if (dirty_callback) {
dirty_callback();
}
}
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
if (remote_id.empty() || sequence == 0 ||
displayed_frame_sequence[remote_id] == sequence) {
return false;
}
displayed_frame_sequence[remote_id] = sequence;
return true;
}
#if !defined(__APPLE__)
void ConfigureOpenGlNotifier() {
if (!stream) {
return;
}
const auto error = (*stream)->window().set_rendering_notifier(
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
return;
}
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
if (state == slint::RenderingState::AfterRendering && stream &&
opengl_renderer &&
(*stream)->get_native_video_enabled() &&
opengl_renderer->ShouldContinueRendering()) {
// Queue the next frame directly from the display callback. A
// 16/17 ms polling timer can drift across vsync and make Windows
// coalesce every second request, effectively capping video near
// 30 fps even when decode and submission stay near 60 fps.
(*stream)->window().request_redraw();
return;
}
std::lock_guard lock(gl_mutex);
if (state == slint::RenderingState::RenderingSetup) {
GLuint texture = 0;
glGenTextures(1, &texture);
if (texture == 0) {
return;
}
GLint previous_texture = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_texture = texture;
if (opengl_renderer && opengl_renderer->Setup()) {
const std::string selected_stream =
CurrentState().selected_stream;
opengl_renderer->SetSelectedStream(selected_stream);
seeded_frame_sequence.erase(selected_stream);
MarkDirty();
}
return;
}
if (state == slint::RenderingState::BeforeRendering && stream &&
opengl_renderer && opengl_renderer->IsReady() &&
(*stream)->get_native_video_enabled()) {
const SurfaceState surface = CurrentState();
const auto window_size = (*stream)->window().size();
const float scale_factor =
std::max(1.0f, (*stream)->window().scale_factor());
const int top_inset =
!surface.fullscreen && surface.tab_count > 1
? static_cast<int>(std::lround(30.0f * scale_factor))
: 0;
const bool rounded_window = (*stream)->get_custom_titlebar() &&
!surface.fullscreen &&
!(*stream)->window().is_maximized();
const int corner_radius =
rounded_window ? static_cast<int>(std::lround(
kStreamWindowCornerRadius * scale_factor))
: 0;
opengl_renderer->RenderLatest(
surface.selected_stream, static_cast<int>(window_size.width),
static_cast<int>(window_size.height), top_inset, corner_radius);
return;
}
if (state == slint::RenderingState::BeforeRendering &&
gl_texture != 0 && gl_pending_dirty &&
!gl_pending_frame.empty()) {
GLint previous_texture = 0;
GLint previous_unpack_alignment = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, gl_texture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
if (gl_texture_size != gl_pending_size) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
gl_texture_size = gl_pending_size;
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
}
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_pending_dirty = false;
return;
}
if (state == slint::RenderingState::RenderingTeardown) {
if (opengl_renderer) {
opengl_renderer->Teardown();
}
if (gl_texture != 0) {
const GLuint texture = gl_texture;
glDeleteTextures(1, &texture);
}
gl_texture = 0;
gl_texture_size = {};
gl_image_size = {};
gl_pending_size = {};
gl_pending_dirty = false;
gl_pending_frame.clear();
}
});
if (error.has_value()) {
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
} else {
renderer.SetSelectedStream(CurrentState().selected_stream);
}
}
#endif
};
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
: impl_(std::make_unique<Impl>(renderer)) {}
SlintVideoPresenter::~SlintVideoPresenter() = default;
void SlintVideoPresenter::PrepareWindow(
slint::ComponentHandle<ui::StreamWindow>& stream,
StateProvider state_provider, DirtyCallback dirty_callback) {
impl_->stream = &stream;
impl_->state_provider = std::move(state_provider);
impl_->dirty_callback = std::move(dirty_callback);
#if defined(__APPLE__)
stream->set_native_video_enabled(false);
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
impl_->skip_attachment_once = true;
#else
impl_->ConfigureOpenGlNotifier();
#endif
}
bool SlintVideoPresenter::EnsureAttached() {
#if defined(__APPLE__)
if (!impl_->stream || impl_->attachment_attempts <= 0) {
return false;
}
if (impl_->skip_attachment_once) {
impl_->skip_attachment_once = false;
return false;
}
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (!metal_renderer) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (!metal_renderer->IsReady()) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (metal_renderer->IsAttached()) {
impl_->attachment_attempts = 0;
return false;
}
if (void* view = (*impl_->stream)->window().appkit_view()) {
(*impl_->stream)->set_native_video_enabled(true);
if (metal_renderer->Attach(view)) {
impl_->attachment_attempts = 0;
const SurfaceState surface = impl_->CurrentState();
impl_->displayed_frame_sequence.erase(surface.selected_stream);
impl_->seeded_frame_sequence.erase(surface.selected_stream);
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
impl_->MarkDirty();
}
return true;
}
(*impl_->stream)->set_native_video_enabled(false);
}
if (--impl_->attachment_attempts == 0) {
(*impl_->stream)->set_native_video_enabled(false);
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
}
#endif
return false;
}
bool SlintVideoPresenter::NeedsRedraw() const {
#if defined(__APPLE__)
const auto* metal_renderer =
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
return metal_renderer && metal_renderer->IsActive() &&
metal_renderer->NeedsSurfaceRedraw();
#else
return false;
#endif
}
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
if (changed && !remote_id.empty()) {
impl_->seeded_frame_sequence.erase(remote_id);
}
return changed;
}
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
const Frame& frame) {
PresentResult result;
if (!impl_->stream || frame.remote_id.empty()) {
return result;
}
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
frame.height > 0 && frame.sequence > 0 &&
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
const auto submit_result = impl_->renderer.SubmitCachedNv12(
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
frame.height);
if (submit_result == VideoRenderer::SubmitResult::submitted ||
submit_result == VideoRenderer::SubmitResult::dropped) {
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
}
}
#if defined(__APPLE__)
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (metal_renderer && metal_renderer->IsActive()) {
const SurfaceState surface = impl_->CurrentState();
const double top_inset =
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
const auto outcome = metal_renderer->RenderLatest(
frame.remote_id, top_inset, !surface.fullscreen);
if (outcome.result == VideoRenderer::RenderResult::rendered) {
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = outcome.width;
result.height = outcome.height;
result.new_frame =
impl_->MarkPresented(frame.remote_id, outcome.sequence);
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
(*impl_->stream)->set_has_frame(false);
}
return result;
}
#else
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
if (opengl_renderer && opengl_renderer->IsReady()) {
opengl_renderer->SetSelectedStream(frame.remote_id);
if (!(*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(true);
}
const bool has_frame =
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
(*impl_->stream)->set_has_frame(has_frame);
if (has_frame) {
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
}
(*impl_->stream)->window().request_redraw();
return result;
}
if ((*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(false);
}
#endif
const size_t nv12_size =
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
frame.nv12->size() < nv12_size) {
(*impl_->stream)->set_has_frame(false);
return result;
}
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
return result;
}
const auto window_size = (*impl_->stream)->window().size();
const VideoRenderSize render_size = FitVideoToRenderArea(
frame.width, frame.height, static_cast<int>(window_size.width),
static_cast<int>(window_size.height));
const uint8_t* y_plane = frame.nv12->data();
const uint8_t* uv_plane =
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
int output_width = frame.width;
int output_height = frame.height;
if (render_size.width > 0 && render_size.height > 0 &&
(render_size.width != frame.width ||
render_size.height != frame.height)) {
const size_t scaled_nv12_size =
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
impl_->scaled_video_frame.resize(scaled_nv12_size);
uint8_t* scaled_y = impl_->scaled_video_frame.data();
uint8_t* scaled_uv =
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
frame.width, frame.height, scaled_y, render_size.width,
scaled_uv, render_size.width, render_size.width,
render_size.height, libyuv::kFilterBox,
&impl_->scaled_video_frame_scratch) == 0) {
y_plane = scaled_y;
uv_plane = scaled_uv;
output_width = render_size.width;
output_height = render_size.height;
}
}
#if !defined(__APPLE__)
uint32_t texture_id = 0;
{
std::lock_guard lock(impl_->gl_mutex);
texture_id = impl_->gl_texture;
}
if (texture_id != 0) {
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
output_height * 4);
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
impl_->gl_conversion_frame.data(), output_width * 4,
output_width, output_height) != 0) {
return result;
}
{
std::lock_guard lock(impl_->gl_mutex);
if (impl_->gl_texture == 0) {
return result;
}
texture_id = impl_->gl_texture;
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
impl_->gl_pending_size = {output_width, output_height};
impl_->gl_pending_dirty = true;
}
const VideoRenderSize output_size{output_width, output_height};
if (impl_->gl_image_size != output_size) {
(*impl_->stream)
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
texture_id,
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
static_cast<uint32_t>(output_height)}));
impl_->gl_image_size = output_size;
}
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
(*impl_->stream)->window().request_redraw();
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
#endif
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
output_height);
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
reinterpret_cast<uint8_t*>(pixels.begin()),
output_width * 3, output_width, output_height) != 0) {
return result;
}
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
impl_->displayed_frame_sequence.erase(remote_id);
impl_->seeded_frame_sequence.erase(remote_id);
}
void SlintVideoPresenter::Detach() {
impl_->renderer.SetSelectedStream({});
#if defined(__APPLE__)
if (auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
metal_renderer->Detach();
}
impl_->attachment_attempts = 0;
impl_->skip_attachment_once = false;
#endif
impl_->stream = nullptr;
impl_->state_provider = {};
impl_->dirty_callback = {};
}
} // namespace crossdesk
@@ -0,0 +1,75 @@
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "crossdesk_ui.h"
namespace crossdesk {
class VideoRenderer;
// Owns the bridge between decoded NV12 frames, the selected native graphics
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
// details are intentionally contained in the implementation file.
class SlintVideoPresenter {
public:
struct SurfaceState {
std::string selected_stream;
bool fullscreen = false;
size_t tab_count = 0;
};
struct Frame {
std::string remote_id;
std::shared_ptr<std::vector<unsigned char>> nv12;
int width = 0;
int height = 0;
uint64_t sequence = 0;
};
struct PresentResult {
int width = 0;
int height = 0;
bool new_frame = false;
};
using StateProvider = std::function<SurfaceState()>;
using DirtyCallback = std::function<void()>;
explicit SlintVideoPresenter(VideoRenderer& renderer);
~SlintVideoPresenter();
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
// Called before the window is shown. OpenGL installs Slint's rendering
// notifier here; macOS deliberately defers native view attachment until a
// later UI pass, after AppKit has committed the ordinary window style.
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
StateProvider state_provider,
DirtyCallback dirty_callback);
// Returns true only when a deferred native surface became attached during
// this call, allowing the caller to seed it from a cached frame once.
bool EnsureAttached();
bool NeedsRedraw() const;
bool SelectStream(std::string remote_id);
PresentResult Present(const Frame& frame);
void ForgetStream(const std::string& remote_id);
void Detach();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
@@ -8,15 +8,10 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include <display_stream_id.h>
#include "localization.h"
#include "platform.h"
#if defined(_WIN32)
#include "platform/windows_opengl_video_renderer.h"
#endif
#ifdef __APPLE__
#include "platform/metal_video_renderer.h"
#endif
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
@@ -128,6 +123,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;
@@ -229,21 +228,14 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
#if defined(__APPLE__) || defined(_WIN32)
if ((!frame_snapshot || frame_snapshot->empty()) &&
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
frame_snapshot = props->thumbnail_frame_;
video_width = props->thumbnail_width_;
video_height = props->thumbnail_height_;
}
#endif
}
#if defined(__APPLE__) || defined(_WIN32)
#if defined(_WIN32)
auto* native_renderer = windows_opengl_video_renderer_.get();
#else
auto* native_renderer = mac_metal_video_renderer_.get();
#endif
auto* native_renderer = video_renderer_.get();
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer->CopyLatestNv12(props->remote_id_,
@@ -252,7 +244,6 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
frame_snapshot = std::move(native_snapshot);
}
}
#endif
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
@@ -276,12 +267,10 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
}
}
#if defined(__APPLE__) || defined(_WIN32)
if (native_renderer) {
native_renderer->DiscardStream(props->remote_id_);
video_frame_dirty_.store(true, std::memory_order_release);
}
#endif
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
@@ -343,18 +332,21 @@ void GuiRuntime::ResetRemoteSessionResources(
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
#if defined(__APPLE__) || defined(_WIN32)
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
#endif
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
{
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
}
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
@@ -0,0 +1,67 @@
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
#include <algorithm>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
inline void ResetCursorPosition(CursorState* state) {
if (!state) return;
state->position_update = true;
state->position_valid = false;
state->x = 0.5f;
state->y = 0.5f;
state->visual_offset_x = 0.0f;
state->visual_offset_y = 0.0f;
state->display_id = -1;
}
inline bool NormalizeCursorPosition(
double screen_x, double screen_y,
const std::vector<DisplayInfo>& displays, int preferred_display,
CursorState* state) {
if (!state) return false;
ResetCursorPosition(state);
auto contains = [&](int index) {
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
const auto& display = displays[index];
return display.width > 0 && display.height > 0 &&
screen_x >= display.left && screen_x < display.right &&
screen_y >= display.top && screen_y < display.bottom;
};
int display_id = contains(preferred_display) ? preferred_display : -1;
if (display_id < 0) {
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
if (contains(index)) {
display_id = index;
break;
}
}
}
if (display_id < 0) return false;
const auto& display = displays[display_id];
// Screen coordinates describe a continuous rectangle with an exclusive
// right/bottom edge. Use its full extent so feedback is the exact inverse of
// normalized input instead of accumulating a one-pixel edge convention.
const double horizontal_extent = std::max(display.width, 1);
const double vertical_extent = std::max(display.height, 1);
state->position_valid = true;
state->x = static_cast<float>(std::clamp(
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
state->y = static_cast<float>(std::clamp(
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
state->display_id = display_id;
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,136 @@
#include "runtime/cursor_state_provider.h"
#include <remote_action.h>
#if defined(_WIN32)
#include <windows.h>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
}
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
if (IsSystemCursor(cursor, IDC_APPSTARTING))
return RemoteCursorShape::progress;
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
if (IsSystemCursor(cursor, IDC_NO))
return RemoteCursorShape::not_allowed;
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
if (IsSystemCursor(cursor, IDC_SIZENESW))
return RemoteCursorShape::nesw_resize;
if (IsSystemCursor(cursor, IDC_SIZENWSE))
return RemoteCursorShape::nwse_resize;
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
return RemoteCursorShape::default_cursor;
}
} // namespace
struct CursorStateProvider::Impl {};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state) return false;
CURSORINFO info{};
info.cbSize = sizeof(info);
if (!GetCursorInfo(&info)) return false;
state->seq = 0;
state->visible = (info.flags & CURSOR_SHOWING) != 0;
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
: RemoteCursorShape::none;
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
preferred_display, state);
return true;
}
} // namespace crossdesk
#elif defined(__linux__) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <string>
#include "linux_cursor_shape.h"
#include "platform.h"
#include "shared_cursor_state.h"
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
for (size_t index = 0; index < pixel_count; ++index) {
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
}
return false;
}
} // namespace
struct CursorStateProvider::Impl {
Display* display = XOpenDisplay(nullptr);
~Impl() {
if (display) XCloseDisplay(display);
}
};
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
CursorStateProvider::~CursorStateProvider() = default;
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state) {
if (!state || !impl_) return false;
ResetCursorPosition(state);
if (IsWaylandSession()) {
SharedCursorState shared{};
if (GetSharedCursorState(&shared)) {
state->seq = 0;
state->visible = shared.visible;
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
return true;
}
}
if (!impl_->display) return false;
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
if (!image) return false;
const std::string name = image->name ? image->name : "";
state->seq = 0;
state->visible = CursorHasVisiblePixel(*image);
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
: RemoteCursorShape::none;
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
state);
XFree(image);
return true;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,33 @@
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
#include <memory>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
// Samples the cursor that is currently displayed by the controlled desktop
// and converts platform-specific cursor handles to protocol cursor shapes.
class CursorStateProvider {
public:
CursorStateProvider();
~CursorStateProvider();
CursorStateProvider(const CursorStateProvider&) = delete;
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
bool Sample(const std::vector<DisplayInfo>& displays,
int preferred_display, CursorState* state);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif
@@ -10,14 +10,9 @@
#include <thread>
#include <vector>
#include "display_stream_id.h"
#include <display_stream_id.h>
#include "localization.h"
#if defined(_WIN32)
#include "platform/windows_opengl_video_renderer.h"
#endif
#ifdef __APPLE__
#include "platform/metal_video_renderer.h"
#endif
#include "platform/video_renderer.h"
#include "rd_log.h"
namespace crossdesk {
@@ -146,6 +141,13 @@ 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)
// Windows accepts ref-counted CPU NV12 and CUDA device NV12. The OpenGL
// renderer retains the decoded frame until the UI thread has uploaded it.
params_.native_video_output = true;
#else
params_.native_video_output = false;
#endif
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
? true
@@ -160,8 +162,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;
@@ -15,12 +15,7 @@
namespace crossdesk {
#if defined(_WIN32)
class WindowsOpenGlVideoRenderer;
#endif
#ifdef __APPLE__
class MacMetalVideoRenderer;
#endif
class VideoRenderer;
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
@@ -90,12 +85,7 @@ class GuiRuntime : protected gui_detail::GuiState {
KeyboardController keyboard_;
PeerEventHandler peer_events_;
std::atomic<bool> video_frame_dirty_{false};
#if defined(_WIN32)
std::unique_ptr<WindowsOpenGlVideoRenderer> windows_opengl_video_renderer_;
#endif
#ifdef __APPLE__
std::unique_ptr<MacMetalVideoRenderer> mac_metal_video_renderer_;
#endif
std::unique_ptr<VideoRenderer> video_renderer_;
private:
friend class ClipboardController;
@@ -1,5 +1,7 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -97,6 +99,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
receiver.SetOnSendAck([runtime,
remote_user_id](const FileTransferAck &ack) -> int {
const auto encoded_ack = EncodeFileTransferAck(ack);
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
@@ -104,14 +107,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
if (props) {
PeerPtr *peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
peer, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
runtime->peer_, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
runtime->file_feedback_label_.c_str());
});
receiver.OnData(data, size);
@@ -164,6 +167,48 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
}
if (remote_action.type == ControlType::cursor_state) {
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
props = props_it->second;
}
}
if (!props) {
return;
}
std::lock_guard lock(props->remote_cursor_state_mutex_);
const uint32_t previous_seq = props->remote_cursor_state_.seq;
const bool is_newer =
!props->remote_cursor_state_received_ ||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
if (is_newer) {
const bool changed =
!props->remote_cursor_state_received_ ||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
props->remote_cursor_state_.shape != remote_action.cs.shape;
CursorState merged = remote_action.cs;
if (!remote_action.cs.position_update &&
props->remote_cursor_state_received_) {
merged.position_valid = props->remote_cursor_state_.position_valid;
merged.x = props->remote_cursor_state_.x;
merged.y = props->remote_cursor_state_.y;
merged.display_id = props->remote_cursor_state_.display_id;
}
props->remote_cursor_state_ = merged;
props->remote_cursor_state_received_ = true;
if (changed) {
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
remote_action.cs.seq, remote_action.cs.visible,
static_cast<int>(remote_action.cs.shape));
}
}
return;
}
if (remote_action.type == ControlType::host_infomation) {
bool is_client_mode = false;
std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -204,6 +249,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
} else {
// remote
if (runtime->is_server_mode_ &&
remote_action.type == ControlType::mouse) {
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
runtime->last_remote_pointer_input_time_[remote_id] =
std::chrono::steady_clock::now();
}
#if _WIN32
if (runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
@@ -1,5 +1,7 @@
#include "runtime/peer_event_handler.h"
#include <remote_action.h>
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -16,12 +18,7 @@
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#if defined(_WIN32)
#include "platform/windows_opengl_video_renderer.h"
#endif
#ifdef __APPLE__
#include "platform/metal_video_renderer.h"
#endif
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
@@ -254,6 +251,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
{
RemoteAction remote_action;
remote_action.i.display_num =
@@ -318,63 +320,52 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
props->remote_cursor_state_received_ = false;
}
#if defined(__APPLE__) || defined(_WIN32)
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
int native_snapshot_width = 0;
int native_snapshot_height = 0;
bool needs_native_snapshot = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
needs_native_snapshot = !props->thumbnail_frame_ ||
props->thumbnail_frame_->empty();
needs_native_snapshot =
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
}
if (needs_native_snapshot) {
#if defined(_WIN32)
auto* native_renderer =
runtime->windows_opengl_video_renderer_.get();
#else
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
#endif
auto* native_renderer = runtime->video_renderer_.get();
auto snapshot = std::make_shared<std::vector<unsigned char>>();
if (native_renderer && native_renderer->CopyLatestNv12(
remote_id, snapshot.get(),
&native_snapshot_width,
&native_snapshot_height)) {
if (native_renderer &&
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
&native_snapshot_width,
&native_snapshot_height)) {
native_snapshot = std::move(snapshot);
}
}
#endif
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
#if defined(__APPLE__) || defined(_WIN32)
if (native_snapshot &&
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
props->thumbnail_frame_ = std::move(native_snapshot);
props->thumbnail_width_ = native_snapshot_width;
props->thumbnail_height_ = native_snapshot_height;
}
#endif
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
#if defined(__APPLE__) || defined(_WIN32)
#if defined(_WIN32)
auto* native_renderer = runtime->windows_opengl_video_renderer_.get();
#else
auto* native_renderer = runtime->mac_metal_video_renderer_.get();
#endif
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer) {
native_renderer->DiscardStream(remote_id);
runtime->video_frame_dirty_.store(true,
std::memory_order_release);
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
#endif
runtime->focus_on_stream_window_ = false;

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