Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16348444f0 | ||
|
|
e716346b26 | ||
|
|
b7e1adbec6 | ||
|
|
58f2daa9c1 | ||
|
|
8752e51921 | ||
|
|
a8cb0e21ce | ||
|
|
90184f5251 | ||
|
|
fb0ab4f14e | ||
|
|
0e3f3cec00 | ||
|
|
72af49eb80 | ||
|
|
36f05834b5 | ||
|
|
1a190fbaf5 | ||
|
|
1fe236da76 | ||
|
|
3b989fb70d | ||
|
|
7595ab8bd8 | ||
|
|
126f455681 | ||
|
|
d6d0a548b2 | ||
|
|
74ac849efe | ||
|
|
e2cd18dba4 | ||
|
|
d2b1329eca | ||
|
|
0653a95250 | ||
|
|
bd73ef55f1 | ||
|
|
b854512c24 | ||
|
|
4528caf0f4 | ||
|
|
f6e89725aa | ||
|
|
62f79a8e44 | ||
|
|
096bc68d35 | ||
|
|
8ea4668305 | ||
|
|
21eec76008 | ||
|
|
3e63675787 | ||
|
|
439f137764 | ||
|
|
f537147f54 |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||