Compare commits

..
398 changed files with 5734 additions and 21917 deletions
+10 -12
View File
@@ -8,15 +8,13 @@
# Dependency manifests and local xmake package recipes are the only project
# files needed while pre-warming the build image.
!xmake.lua
!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/**
!xmake/
!xmake/**
!thirdparty/
!thirdparty/**
!submodules/
!submodules/xmake.lua
!submodules/minirtc/
!submodules/minirtc/xmake.lua
!submodules/minirtc/thirdparty/
!submodules/minirtc/thirdparty/**
+5 -5
View File
@@ -11,11 +11,11 @@ on:
- docker/linux-build/**
- xmake.lua
- xmake/**
- deps/thirdparty/**
- deps/submodules/xmake.lua
- deps/submodules/minirtc
- deps/submodules/minirtc/xmake.lua
- deps/submodules/minirtc/thirdparty/**
- thirdparty/**
- submodules/xmake.lua
- submodules/minirtc
- submodules/minirtc/xmake.lua
- submodules/minirtc/thirdparty/**
workflow_dispatch:
permissions:
+23 -244
View File
@@ -36,12 +36,12 @@ jobs:
- arch: amd64
runner: ubuntu-24.04
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
package_script: ./apps/desktop/scripts/linux/pkg_amd64.sh
package_script: ./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: ./apps/desktop/scripts/linux/pkg_arm64.sh
package_script: ./scripts/linux/pkg_arm64.sh
container:
image: ${{ matrix.image }}
@@ -138,17 +138,6 @@ 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 }}
@@ -189,13 +178,13 @@ jobs:
runner: macos-26-intel
cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./apps/desktop/scripts/macos/pkg_x64.sh
package_script: ./scripts/macosx/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: ./apps/desktop/scripts/macos/pkg_arm64.sh
package_script: ./scripts/macosx/pkg_arm64.sh
steps:
- name: Extract version number
@@ -293,7 +282,7 @@ jobs:
exit 1
fi
./apps/desktop/scripts/macos/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Upload build artifacts
uses: actions/upload-artifact@v6
@@ -306,200 +295,6 @@ 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
outputs:
version: ${{ steps.ios_version.outputs.version }}
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: Read independent iOS version
id: ios_version
shell: bash
run: |
IOS_VERSION_BASE="$(xcodebuild \
-project apps/ios/CrossDeskMobile.xcodeproj \
-scheme CrossDeskMobile \
-configuration Release \
-destination 'generic/platform=iOS' \
-showBuildSettings CODE_SIGNING_ALLOWED=NO | \
awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }')"
if [[ ! "${IOS_VERSION_BASE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid iOS marketing version: ${IOS_VERSION_BASE}" >&2
exit 1
fi
VERSION_NUM="v${IOS_VERSION_BASE}-${VERSION_NUM##*-}"
echo "IOS_VERSION_BASE=${IOS_VERSION_BASE}" >> "${GITHUB_ENV}"
echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}"
echo "version=${VERSION_NUM}" >> "${GITHUB_OUTPUT}"
- 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
APP_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${INFO_PLIST}")
if [[ "${APP_VERSION}" != "${IOS_VERSION_BASE}" ]]; then
echo "Unexpected iOS app version: expected ${IOS_VERSION_BASE}, got ${APP_VERSION}" >&2
exit 1
fi
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
@@ -609,7 +404,7 @@ jobs:
- name: Copy nsProcess plugin to NSIS folder
run: |
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
copy "${{ github.workspace }}\apps\desktop\scripts\windows\nsProcess.dll" $nsisPluginDir
copy "${{ github.workspace }}\scripts\windows\nsProcess.dll" $nsisPluginDir
- name: Build CrossDesk
run: |
@@ -619,14 +414,14 @@ jobs:
- name: Package
shell: pwsh
run: |
& "${{ github.workspace }}\apps\desktop\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
& "${{ github.workspace }}\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 }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
archive: false
if-no-files-found: error
retention-days: 1
@@ -652,7 +447,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 }}\apps\desktop\scripts\windows" $installerName
$destination = Join-Path "${{ github.workspace }}\scripts\windows" $installerName
if (!(Test-Path $signedInstaller)) {
throw "SignPath did not return the signed installer: $signedInstaller"
@@ -709,7 +504,7 @@ jobs:
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v6
@@ -720,7 +515,7 @@ jobs:
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-linux, build-macos, build-ios, build-windows-x64]
needs: [build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
@@ -776,25 +571,6 @@ jobs:
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Prepare release notes
env:
RELEASE_TAG: ${{ steps.version.outputs.VERSION_WITH_V }}
VERSION_BASE: ${{ steps.version.outputs.VERSION_BASE }}
run: |
if [[ -f "docs/releases/${RELEASE_TAG}.md" ]]; then
cp "docs/releases/${RELEASE_TAG}.md" release-notes.md
elif [[ -f "docs/releases/v${VERSION_BASE}.md" ]]; then
cp "docs/releases/v${VERSION_BASE}.md" release-notes.md
else
cat > release-notes.md << 'EOF'
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
EOF
fi
- name: Rename artifacts
run: |
mkdir -p release
@@ -802,7 +578,6 @@ 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-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.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
@@ -813,13 +588,17 @@ jobs:
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
target_commitish: ${{ github.sha }}
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
draft: false
prerelease: false
files: release/*
generate_release_notes: false
body_path: release-notes.md
body: |
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
- name: Create or update 'latest' tag
run: |
@@ -837,7 +616,12 @@ jobs:
prerelease: false
files: release/*
generate_release_notes: false
body_path: release-notes.md
body: |
Binary release only. Source code is not included.
## Code signing policy
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
- name: Upload artifacts to server
uses: burnett01/rsync-deployments@5.2
@@ -888,11 +672,6 @@ jobs:
}
}
EOF
jq --arg release_name "Release ${{ steps.version.outputs.VERSION_WITH_V }}" \
--rawfile release_notes release-notes.md \
'.releaseName = $release_name | .releaseNotes = $release_notes' \
version.json > version-with-notes.json
mv version-with-notes.json version.json
cat version.json
- name: Upload version.json to server
+1 -6
View File
@@ -1,14 +1,9 @@
# Xmake cache
.xmake/
build/
apps/ios/Vendor/
apps/ios/DerivedData*/
apps/ios/.xmake/
xcuserdata/
*.xcuserstate
# MacOS Cache
.DS_Store
# VSCode cache
.vscode
.vscode
+2 -2
View File
@@ -1,3 +1,3 @@
[submodule "deps/submodules/minirtc"]
path = deps/submodules/minirtc
[submodule "submodules/minirtc"]
path = submodules/minirtc
url = https://github.com/kunkundi/minirtc.git
+374 -155
View File
@@ -1,211 +1,429 @@
<div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk 标志" />
<h1>CrossDesk</h1>
<p>轻量、跨平台的远程桌面,让电脑、浏览器和 iPhone / iPad 连接到同一张桌面。</p>
<p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p>
</div>
# CrossDesk
<div align="center">
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
[![Platforms](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20iOS-blue)](#download)
[![Release](https://img.shields.io/github/v/release/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/releases)
[![Build](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE)
[![Stars](https://img.shields.io/github/stars/kunkundi/crossdesk?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)](https://www.crossdesk.cn/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)](https://github.com/kunkundi/crossdesk/issues)
[![GitHub stars](https://img.shields.io/github/stars/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/forks)
</div>
[ [English](README_EN.md) / 中文 ]
CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览器或原生 iOS 客户端控制电脑。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),提供实时音视频传输、键鼠控制、文件传输与自托管能力,仍在持续开发中。
> 本文按当前仓库代码整理。截图展示当前 Slint 桌面界面,使用演示 ID、密码、在线状态和连接记录;缩略图来自公开页面;下载包的功能与系统要求请同时核对对应 Release 说明。原生 iOS 客户端的构建、签名方式见下文。
[界面预览](#preview) · [下载与安装](#download) · [快速连接](#quick-start) · [会话操作](#session) · [设置](#settings) · [iOS](#ios) · [自托管](#self-hosting) · [源码构建](docs/BUILD.md) · [常见问题](docs/FAQ.md)
<a id="preview"></a>
---
## 界面预览
### 桌面客户端
### PC 客户端
主窗口集中展示本机 ID、连接密码、远程连接入口和近期连接记录。
<p align="center">
<img src="docs/images/desktop-main-zh.png" width="800" alt="当前桌面客户端示例:已连接服务器,三台在线设备均显示页面缩略图" />
</p>
![sup_example](https://github.com/user-attachments/assets/eeb64fbe-1f07-4626-be1c-b77396beb905)
### Web 客户端
在浏览器中输入远程设备 ID 和密码,无需在控制端安装桌面客户端。
<p align="center">
<img src="docs/images/web-client.png" width="800" alt="CrossDesk Web 客户端当前连接首页" />
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
图片来源与更新方式见 [截图说明](docs/images/README.md)。
---
## 主要功能
## 简介
| 能力 | 当前支持 |
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
---
## 系统要求
| 平台 | 最低版本 |
| --- | --- |
| 跨平台控制 | Windows / macOS / Linux 桌面互控;浏览器与原生 iOS 作为控制端 |
| 实时画面与声音 | H.264 / AV1、30 / 60 fps 设置、硬件编解码选项、远端声音播放;实际能力取决于设备与构建配置 |
| 多设备与多显示器 | 近期连接、设备别名、会话标签与远端显示器切换 |
| 输入与协作 | 键鼠输入、远端光标同步、组合键、文本剪贴板同步、文件传输 |
| 网络与部署 | P2P 直连、TURN 中继、SRTP 加密选项、自托管信令与中继服务 |
| Windows 锁屏控制 | 通过 CrossDesk Service 支持锁屏、登录界面及安全桌面的输入转发 |
| **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 20.04 及以上 |
各客户端的入口和能力有所不同;桌面会话操作见下文,iOS 的具体功能见 [开发说明](apps/ios/README.md)。
---
<a id="download"></a>
## 使用
## 下载与安装
在菜单栏“对端ID”处输入远端桌面的ID,点击“→”即可发起远程连接。
前往 [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) 或 [官方网站](https://www.crossdesk.cn/),按系统与 CPU 架构选择安装包。
![usage1](https://github.com/user-attachments/assets/3a4bb59f-c84c-44d2-9a20-11790aac510e)
| 平台 | 当前源码 / CI 基线 | 安装方式 |
| --- | --- | --- |
| Windows | Windows 10+,x64 | `.exe` 安装包;便携构建也受支持 |
| macOS | macOS 14.0+,Intel / Apple Silicon | 选择 x64 或 arm64 的 `.pkg` |
| Linux | Ubuntu 20.04+,amd64 / arm64,glibc 2.31 基线 | `.deb` 安装包 |
| iOS / iPadOS | iOS 16.0+,arm64 真机 | 原生客户端;使用 Xcode 构建并签名,CI 产物为未签名应用 |
| Web | 支持 WebRTC 的浏览器 | 访问 [web.crossdesk.cn](https://web.crossdesk.cn/) |
如果远端桌面设置了连接密码,则本端需填写正确的连接密码才能成功发起远程连接。
Linux 下载后,在下载目录中将文件名替换为实际安装包名称:
![password](https://github.com/user-attachments/assets/1beadcce-640d-4f5c-8e77-51917b5294d5)
```bash
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
```
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
**macOS 首次运行:** 按应用提示,在“系统设置 → 隐私与安全性”中授予 CrossDesk **屏幕录制**(新系统可能显示为“屏幕与系统音频录制”)及 **辅助功能**权限,再按系统提示重新打开应用。前者用于捕获桌面,后者用于远程键鼠输入。
### 使用 Web 客户端
<a id="quick-start"></a>
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
## 快速连接
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### 从另一台电脑连接
### Windows 服务(CrossDesk Service)
1. **准备被控电脑。** 安装并运行 CrossDesk,等待底部显示“已连接服务器”。在左侧“本桌面”查看 **本机 ID** 和 **密码**,提供给控制端。
2. **输入对端 ID。** 在控制端右侧“远程桌面 → 对端 ID”输入被控电脑的 ID,点击 **→**。
3. **完成密码验证。** 按弹窗输入被控电脑当前的连接密码,确认后等待远程画面出现。需要保存密码时勾选“记住密码”。
4. **再次连接。** 成功连接过的设备会出现在“近期连接”中,可通过卡片重连,也可编辑别名或删除记录。
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
本机密码旁的眼睛按钮用于显示 / 隐藏密码,铅笔按钮用于修改密码。修改时使用 **6 位数字或英文字母**,并保持连接服务器;等待修改成功、客户端重新连接后,再复制当前密码。已保存旧密码的控制端需重新输入。
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
### 从浏览器连接
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器;在被控端“设置”中保持 **启用 SRTP** 开启。
2. 打开 [Web 客户端](https://web.crossdesk.cn/),输入 **远程设备 ID** 和 **密码**,点击“连接”。
3. 连接后使用页面提供的显示器、鼠标模式和键盘控件操作远端;手机和平板也可通过浏览器访问。
如果使用自建服务器,需要让控制端与被控端接入同一服务;Web 部署配置见 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
<a id="session"></a>
## 会话中的常用操作
远程窗口通过会话标签切换设备,控制栏提供以下操作;控制栏收起时先点击展开按钮。将鼠标悬停在图标上可查看提示。
| 入口 | 用途 |
| --- | --- |
| 显示器 | 切换远端显示器 |
| 键盘 | 发送组合键;Windows 的 `Ctrl+Alt+Del` 依赖远端服务 |
| 鼠标 | 控制 / 释放远端鼠标 |
| 扬声器 | 播放声音 / 静音 |
| 文件夹 | 选择文件发送到远端,并查看传输进度 |
| 网络状态 | 查看流量、丢包率、帧率、分辨率及直连 / 中继状态 |
| 全屏 | 进入 / 退出全屏 |
| 断开连接 | 结束当前远程会话 |
桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。
<a id="settings"></a>
## 调整画面与连接设置
点击主窗口右上角 **☰ → 设置**,修改后点击“确认”保存。建议在建立连接前调整;会话进行中部分选项不可修改。设置列表可向下滚动,查看自托管、启动方式与文件保存路径。
<p align="center">
<img src="docs/images/desktop-settings-zh.png" width="720" alt="当前设置界面:画质、帧率、画面偏好、编解码、中继与 SRTP" />
</p>
| 设置 | 说明 |
| --- | --- |
| 画面质量 / 帧率 | 选择低、中、高画质与 30 / 60 fps;实际帧率取决于网络和设备 |
| 画面偏好 | 在帧率优先、画质优先、平衡之间选择 |
| 视频编码格式 | H.264 / AV1;硬件编解码是否可用取决于平台、硬件与构建选项 |
| 启用中继服务 | 允许借助 TURN 中继建立连接;P2P 连接失败时先检查此项 |
| 启用 SRTP | 媒体传输加密选项,两端配置需兼容 |
| 自托管配置 | 设置服务器地址、信令端口与中继端口,并勾选启用 |
| 开机自启 / 启用守护进程 | 调整启动与进程守护行为,按界面提示重启后生效 |
| 文件保存路径 | 选择接收文件的目录 |
<a id="ios"></a>
## 原生 iOS / iPadOS 客户端
仓库的 [`apps/ios`](apps/ios/README.md) 提供原生控制端,与桌面端使用同一套 MiniRTC 协议。它可以控制远端电脑;当前不提供将 iPhone / iPad 作为被控桌面的功能。
1. 按 [iOS 开发说明](apps/ios/README.md) 使用 Xcode 构建并签名,安装到 iOS 16+ 的真机。
2. 在首页输入对端 ID,点击连接并填写密码;成功连接后可从“最近连接”中重连。
3. 在设置中选择鼠标模式:**相对位置**像触控板一样移动光标,**绝对位置**将触摸位置直接映射到远端画面。
4. 点击悬浮图标展开菜单,可切换显示器、显示键盘、控制声音或发送文件。收到的文件位于应用的 `Documents/Received`,可通过传输状态旁的分享按钮导出。
当前支持接收远端文本到 iOS 剪贴板;界面暂未提供主动发送本机剪贴板的按钮。
当前支持单指点击、双指右键、长按拖动与捏合缩放;缩放后的双指平移用于移动视图。原生端与浏览器端分别维护,使用入口和手势可能不同。
<a id="windows-service"></a>
## Windows 锁屏与登录界面
**CrossDesk Service**(服务名 `CrossDeskService`)提供锁屏、登录、凭据输入及安全桌面的状态上报、`Ctrl+Alt+Del` 发送和键鼠输入转发。
安装版会注册按需启动的服务,客户端启动时尝试启动它;本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。该服务依赖客户端运行。
<details>
<summary>手动部署与服务管理命令</summary>
保留完整安装目录;自行部署构建产物时,将 `CrossDesk.exe`、`crossdesk_service.exe`、`crossdesk_session_helper.exe` 及同次构建输出的全部 `.dll` 放在同一目录。在该目录打开管理员 PowerShell,按需执行:
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
```powershell
# 安装
.\CrossDesk.exe --service-install
# 启动
.\CrossDesk.exe --service-start
# 查看状态
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-ping
# 停止或卸载服务
# 停止
.\CrossDesk.exe --service-stop
# 卸载
.\CrossDesk.exe --service-uninstall
```
</details>
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
出现“远端 Windows 服务不可用”时,检查被控电脑的安装和服务状态;普通桌面连接仍可使用,受保护界面的控制能力会受限。
---
<a id="self-hosting"></a>
## 如何编译
### 构建依赖
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
### Linux
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev \
libgl1-mesa-dev
```
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
CMake 3.21 或更高版本,无需手动替换系统 CMake。
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
因此上述命令同时安装并在后续配置中指定 GCC 10。
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
```bash
sudo apt-get install -y libdbus-1-dev libdrm-dev
# Ubuntu 22.04 及以上也可以直接安装系统开发包
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
下载子模块并编译 Release 版本:
```bash
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
# 已经克隆过仓库时执行这一行
git submodule update --init --recursive
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
启用 Wayland 和 DRM 的配置示例:
```bash
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
生成 Debian 安装包(必须在对应架构上先完成 Release 编译):
```bash
# amd64
./scripts/linux/pkg_amd64.sh 1.0.0
# arm64
./scripts/linux/pkg_arm64.sh 1.0.0
```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项
```text
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
--USE_WAYLAND=true/false: 在 Linux 上启用 Wayland/PipeWire 捕获,默认不启用
--USE_DRM=true/false: 在 Linux 上启用 DRM 捕获,默认不启用
--CROSSDESK_PORTABLE=true/false: 构建便携版本,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
运行:
```bash
xmake r crossdesk
```
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包。
进入容器,下载工程后执行:
```bash
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
```powershell
xmake require -vy "cuda 12.6.3"
```
安装完成后执行:
```powershell
xmake require --info "cuda 12.6.3"
```
输出如下:
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
```powershell
set CUDA_PATH=path_to_cuda_installdir
```
重新执行:
```powershell
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
#### 注意
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
### 关于 Xmake
#### 安装 Xmake
使用 curl:
```bash
curl -fsSL https://xmake.io/shget.text | bash
```
使用 wget:
```bash
wget https://xmake.io/shget.text -O - | bash
```
使用 powershell:
```powershell
irm https://xmake.io/psget.text | iex
```
#### 编译选项
```text
# 切换编译模式
xmake f -m debug/release
# 可选编译参数
-r :重新构建目标
-v :显示详细的构建日志
-y :自动确认提示
# 示例
xmake b -vy crossdesk
```
#### 运行选项
```bash
# 使用调试模式运行
xmake r -d crossdesk
```
更多使用方法可参考 [Xmake 官方文档](https://xmake.io/guide/quick-start.html)。
---
## 自托管服务器
可自行部署信令服务与 TURN 中继,并在桌面客户端 **☰ → 设置 → 自托管配置** 中填写连接信息。控制端与被控端都需要启用同一套配置。
### 服务端
- [自托管指南](docs/SELF_HOSTING.md):当前 Compose 部署入口、客户端设置、证书信任与排查。
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server):服务端源码与发布配置。
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client):浏览器端源码与自托管说明。
推荐使用 Docker 部署 CrossDesk Server。
## 开发与反馈
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
- [源码构建](docs/BUILD.md):Windows、macOS、Linux、可选编译参数与打包。
- [iOS 开发说明](apps/ios/README.md):原生端构建和真机验证。
- [GUI 架构](docs/gui-architecture.md):桌面 UI、平台实现与工程结构。
- [常见问题](docs/FAQ.md):连接失败、黑屏、文件位置与编译问题。
- [提交问题](https://github.com/kunkundi/crossdesk/issues):请附上两端系统、客户端版本、连接方式和可复现步骤。
上述命令中,用户需注意的参数如下:
## 致谢与许可
#### 配置参数
感谢 [HelloGitHub](https://hellogithub.com/)、[阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 与 [LinuxDo](https://linux.do) 社区的推荐、交流与支持。
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
- INTERNAL_IP:服务器内网 IP
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
- MIN_PORT/MAX_PORT:COTURN 服务使用的端口范围,例如:MIN_PORT=50000, MAX_PORT=60000,范围可根据客户端数量调整。
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
CrossDesk 使用 [GPL-3.0](LICENSE) 许可。隐私说明见 [隐私政策](PRIVACY.md#中文)。
#### 部署示例
### Code signing policy
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### 注意事项
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume,容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
#### 目录权限
如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
- 证书生成失败,容器启动脚本会报错退出
- 数据库目录创建失败,程序会抛出异常并崩溃
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
在启动容器前手动设置权限:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
**Windows:** 使用管理员权限打开 PowerShell 执行:
```powershell
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
**Linux:**
```bash
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
**macOS:**
```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### 自托管 Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
---
## 常见问题
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md)。
---
## 致谢
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
---
## Code signing policy
CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供代码签名。
@@ -213,3 +431,4 @@ CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
- **隐私政策:** [查看 CrossDesk 隐私政策](PRIVACY.md#中文)
+390 -154
View File
@@ -1,215 +1,451 @@
<div align="center">
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk logo" />
<h1>CrossDesk</h1>
<p>A lightweight, cross-platform remote desktop for computers, browsers, and iPhone / iPad.</p>
<p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p>
</div>
# CrossDesk
<div align="center">
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
[![Platforms](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux%20%7C%20iOS-blue)](#download)
[![Release](https://img.shields.io/github/v/release/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/releases)
[![Build](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
[![License: GPL v3](https://img.shields.io/badge/license-GPL--3.0-blue)](LICENSE)
[![Stars](https://img.shields.io/github/stars/kunkundi/crossdesk?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)](https://www.crossdesk.cn/)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
[![Build Status](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml/badge.svg)](https://github.com/kunkundi/crossdesk/actions)
[![Docker Pulls](https://img.shields.io/docker/pulls/crossdesk/crossdesk-server)](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
[![GitHub issues](https://img.shields.io/github/issues/kunkundi/crossdesk.svg)](https://github.com/kunkundi/crossdesk/issues)
[![GitHub stars](https://img.shields.io/github/stars/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/kunkundi/crossdesk.svg?style=social)](https://github.com/kunkundi/crossdesk/forks)
</div>
[ [中文](README.md) / English ]
CrossDesk connects Windows, macOS, and Linux desktops, with browser and native iOS clients for controlling computers. Built on [MiniRTC](https://github.com/kunkundi/minirtc), it supports real-time video and audio, keyboard and mouse control, file transfer, and self-hosting. The project is under active development.
> This guide follows the current repository source. Desktop screenshots render the current Slint UI with sample IDs, passwords, online statuses, and recent devices; thumbnails show public pages. Check each release's notes for the features and requirements of its binaries. Native iOS build and signing instructions are below.
[Preview](#preview) · [Download](#download) · [Quick start](#quick-start) · [Session controls](#session) · [Settings](#settings) · [iOS](#ios) · [Self-hosting](#self-hosting) · [Build](docs/BUILD_EN.md) · [FAQ](docs/FAQ.md#english)
<a id="preview"></a>
---
## Preview
### Desktop client
### PC Client
The main window brings together your device ID, connection password, remote connection entry, and recent devices.
![sup_example](https://github.com/user-attachments/assets/3f17d8f3-7c4a-4b63-bae4-903363628687)
### Web Client
<p align="center">
<img src="docs/images/desktop-main-en.png" width="800" alt="Current desktop UI example showing a connected server and three online devices with page thumbnails" />
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
### Web client
---
Enter a remote device ID and password in your browser without installing the desktop app on the controlling device.
## Introduction
<p align="center">
<img src="docs/images/web-client.png" width="800" alt="Current CrossDesk Web Client connection page in Chinese" />
</p>
CrossDesk is a lightweight cross-platform remote desktop software.
See the [screenshot notes](docs/images/README.md) for image sources and refresh instructions.
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
## Features
---
| Capability | Current support |
## System Requirements
| Platform | Minimum Version |
| --- | --- |
| Cross-platform control | Windows / macOS / Linux desktop control; browser and native iOS controllers |
| Live video and audio | H.264 / AV1, 30 / 60 fps settings, hardware codec options, and remote audio; availability depends on hardware and build configuration |
| Devices and displays | Recent connections, device aliases, session tabs, and remote display switching |
| Input and sharing | Keyboard/mouse input, remote cursor synchronization, shortcuts, text clipboard synchronization, and file transfer |
| Networking and deployment | Direct P2P connections, TURN relay, an SRTP encryption option, and self-hosted signaling/relay services |
| Windows protected desktops | CrossDesk Service forwards input on lock screens, sign-in screens, and secure desktops |
| **Windows** | Windows 10 or later (64-bit) |
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
| **Linux** | Ubuntu 20.04 or later |
Controls and capabilities vary by client. Desktop controls are described below; see the [iOS guide](apps/ios/README.md) for native mobile features.
---
<a id="download"></a>
## Usage
## Download and install
Enter the remote desktop ID in the menu bar’s “Remote ID” field and click “→” to initiate a remote connection.
Choose a package matching your OS and CPU architecture from [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) or the [website](https://www.crossdesk.cn/).
![usage1](https://github.com/user-attachments/assets/3a4bb59f-c84c-44d2-9a20-11790aac510e)
| Platform | Current source / CI baseline | Installation |
| --- | --- | --- |
| Windows | Windows 10+, x64 | `.exe` installer; portable builds are also supported |
| macOS | macOS 14.0+, Intel / Apple Silicon | x64 or arm64 `.pkg` |
| Linux | Ubuntu 20.04+, amd64 / arm64, glibc 2.31 baseline | `.deb` package |
| iOS / iPadOS | iOS 16.0+, arm64 physical device | Native controller; build and sign with Xcode, or sign the unsigned CI app |
| Web | A WebRTC-capable browser | Open [web.crossdesk.cn](https://web.crossdesk.cn/) |
If the remote desktop requires a connection password, you must enter the correct password on your side to successfully establish the connection.
On Linux, replace the example filename with the downloaded package name:
![password](https://github.com/user-attachments/assets/1beadcce-640d-4f5c-8e77-51917b5294d5)
```bash
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
```
Before connecting, you can customize configuration options in the settings, such as language and video encoding format.
**First launch on macOS:** follow the app prompts to grant **Screen Recording** (called **Screen & System Audio Recording** on newer systems) and **Accessibility** under **System Settings → Privacy & Security**. Reopen the app if prompted. These permissions allow desktop capture and remote keyboard/mouse input respectively.
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
<a id="quick-start"></a>
### Using the Web Client
## Quick start
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
### Connect from another computer
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
1. **Prepare the host.** Install and run CrossDesk on the computer to control. Wait for the bottom status bar to show that the server is connected. Share the **Local ID** and **Password** shown under **Local Desktop** with the controller.
2. **Enter the remote ID.** On the controlling computer, enter the host's ID in **Remote Desktop → Remote ID**, then click **→**.
3. **Verify the password.** Enter the host's current connection password when prompted and confirm. Select **Remember password** if you want to save it.
4. **Reconnect later.** Previously connected devices appear under **Recent Connections**. Reconnect from a card, edit its alias, or remove its record.
### Windows Service (CrossDesk Service)
The eye button shows or hides the local password; the pencil button changes it. Use **6 ASCII letters or digits** and stay connected to the server while changing it. Wait for the change to succeed and the client to reconnect, then copy the current password. Controllers that saved the old password must enter it again.
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
### Connect from a browser
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
- Remote `Ctrl+Alt+Del` (SAS) delivery.
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
1. Keep CrossDesk running and connected to the server on the host computer, with **Enable SRTP** enabled in its Settings.
2. Open the [Web Client](https://web.crossdesk.cn/), enter the **Remote Device ID** and **Password**, and click **Connect**.
3. Use the page's display, mouse-mode, and keyboard controls during the session. Phone and tablet browsers can also connect.
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
For self-hosting, connect both sides to the same service. Browser deployment is documented in [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
<a id="session"></a>
## Session controls
Switch devices using session tabs. Expand the control bar if it is collapsed, and hover over icons to see their tooltips.
| Control | Action |
| --- | --- |
| Display | Switch the remote monitor |
| Keyboard | Send shortcuts; Windows `Ctrl+Alt+Del` requires the remote service |
| Mouse | Enable / release remote mouse control |
| Speaker | Play / mute remote audio |
| Folder | Select a file to send and view transfer progress |
| Network statistics | Inspect traffic, packet loss, frame rate, resolution, and direct / relay mode |
| Fullscreen | Enter / exit fullscreen |
| Disconnect | End the current remote session |
Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely.
<a id="settings"></a>
## Video and connection settings
Open **☰ → Settings** in the top-right corner and click **OK** to save. Configure the session before connecting; some settings are disabled during an active session. Scroll down for self-hosting, startup behavior, and the file save path.
<p align="center">
<img src="docs/images/desktop-settings-en.png" width="720" alt="Current settings for quality, frame rate, video preference, codecs, relay, and SRTP" />
</p>
| Setting | Purpose |
| --- | --- |
| Video Quality / Video Capture Frame Rate | Low, medium, or high quality and 30 / 60 fps; actual performance depends on the connection and device |
| Video Preference | Choose frame-rate priority, quality priority, or balanced adaptation |
| Video Encode Format | H.264 / AV1; hardware codec availability depends on the platform, device, and build options |
| Enable TURN Service | Allow relay-assisted connections; check this setting if P2P fails |
| SRTP | Media encryption option; configurations on both ends must be compatible |
| Self-Hosted Config | Set the host, signaling port, and TURN port, then enable the checkbox |
| Auto Start / Enable Daemon | Configure startup and process supervision; restart as indicated by the UI |
| File Save Path | Choose where received files are saved |
<a id="ios"></a>
## Native iOS / iPadOS client
[`apps/ios`](apps/ios/README.md) contains a native controller using the same MiniRTC protocol as the desktop client. It controls remote computers; it does not expose an iPhone or iPad as a remotely controlled desktop.
1. Follow the [iOS guide](apps/ios/README.md) to build and sign the app with Xcode, then install it on an iOS 16+ physical device.
2. Enter a remote ID on the home screen, connect, and enter its password. Successful connections become available in the recent devices list.
3. Choose a mouse mode in Settings: **Relative Position** works like a trackpad; **Absolute Position** maps touches directly to the remote screen.
4. Tap the floating icon to expand the menu, then switch displays, open the keyboard, toggle audio, or send files. Received files are stored in `Documents/Received` and can be exported using the share button beside the transfer status.
The app currently receives remote text into the iOS clipboard. Its UI does not yet expose a button to send the local clipboard.
Gestures include one-finger click, two-finger right-click, long-press drag, and pinch-to-zoom. When zoomed in, two-finger panning moves the viewport. The native and browser clients are maintained separately, so their controls and gestures may differ. The current native UI uses Chinese labels.
<a id="windows-service"></a>
## Windows lock screens and sign-in
**CrossDesk Service** (`CrossDeskService`) reports protected desktop states and forwards `Ctrl+Alt+Del`, keyboard, and mouse input on lock screens, sign-in screens, credential prompts, and secure desktops.
The installer registers an on-demand service. CrossDesk tries to start it on launch, and the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges. The service depends on a running client.
<details>
<summary>Manual deployment and service commands</summary>
Keep the complete installation directory. For manual deployment, place `CrossDesk.exe`, `crossdesk_service.exe`, `crossdesk_session_helper.exe`, and all `.dll` files from the same build together. Open an administrator PowerShell in that directory and run the commands you need:
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
```powershell
# install
.\CrossDesk.exe --service-install
# start
.\CrossDesk.exe --service-start
# check status
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-ping
# Stop or uninstall the service
# stop
.\CrossDesk.exe --service-stop
# uninstall
.\CrossDesk.exe --service-uninstall
```
</details>
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
If **Remote Windows service unavailable** appears, check installation and service status on the host. Ordinary desktop connections remain available, while control of protected screens is limited.
---
<a id="self-hosting"></a>
## How to build
## Self-hosting
### Build requirements
Host your own signaling and TURN services, then enter their connection details under **☰ → Settings → Self-Hosted Config**. Enable the same server configuration on both the controller and host.
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
- [Self-hosting guide](docs/SELF_HOSTING_EN.md): current Compose deployment, client settings, certificate trust, and troubleshooting.
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server): server source and release configuration.
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client): browser source and deployment instructions.
### Linux
## Development and feedback
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages
use Ubuntu 20.04/glibc 2.31 as their compatibility baseline. Install the base
build dependencies first:
- [Build from source](docs/BUILD_EN.md): Windows, macOS, Linux, optional flags, and packaging.
- [iOS development](apps/ios/README.md): native build and physical-device checks.
- [GUI architecture](docs/gui-architecture.md): desktop UI and platform organization.
- [FAQ](docs/FAQ.md#english): connection failures, blank video, received files, and build issues.
- [Report an issue](https://github.com/kunkundi/crossdesk/issues): include both operating systems, client versions, connection mode, and reproduction steps.
```bash
sudo apt-get update
sudo apt-get install -y \
git curl unzip build-essential gcc-10 g++-10 python3-pip \
pkg-config binutils dpkg-dev \
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev \
libgl1-mesa-dev
```
## Acknowledgements and license
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
not required.
Thanks to [HelloGitHub](https://hellogithub.com/), [Ruanyf Weekly](https://github.com/ruanyf/weekly), and the [LinuxDo](https://linux.do) community for featuring CrossDesk and contributing feedback.
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
by Slint's C++ API, so the commands above install GCC 10 and the configuration
below selects it explicitly.
CrossDesk is licensed under [GPL-3.0](LICENSE). See the [Privacy Policy](PRIVACY.md#english).
Install the optional dependencies when Wayland capture or DRM capture is enabled:
### Code signing policy
```bash
sudo apt-get install -y libdbus-1-dev libdrm-dev
CrossDesk uses SignPath.io to sign official Windows releases built from this repository.
# Ubuntu 22.04 and later can alternatively use the distribution packages.
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
```
Clone the submodules and build a release binary:
```bash
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
cd crossdesk
# Run this when the repository has already been cloned
git submodule update --init --recursive
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
# header-only SDK used during compilation. Skip this when the distribution's
# PipeWire 0.3 development packages are already installed.
sudo ./docker/linux-build/install-pipewire-sdk.sh
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
xmake b -vy crossdesk
```
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
`--cc`/`--cxx` may still let dependency builds fall back to the system
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
configuration.
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
Example configuration with Wayland and DRM capture enabled:
```bash
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--toolchain=gcc-10 -y
xmake b -vy crossdesk
```
Build a Debian package after compiling a release binary on the matching architecture:
```bash
# amd64
./scripts/linux/pkg_amd64.sh 1.0.0
# arm64
./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.
PipeWire is not a mandatory runtime dependency. CrossDesk detects the host's
PipeWire 0.3 runtime dynamically and can still use X11 capture (and DRM when
enabled at build time) when it is unavailable.
### Common build options
```text
--USE_CUDA=true/false: Enable CUDA hardware codec acceleration, disabled by default
--USE_WAYLAND=true/false: Enable Wayland/PipeWire capture on Linux, disabled by default
--USE_DRM=true/false: Enable DRM capture on Linux, disabled by default
--CROSSDESK_PORTABLE=true/false: Build the portable variant, disabled by default
--CROSSDESK_VERSION=xxx: Set the CrossDesk version
# Example
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
Run:
```bash
xmake r crossdesk
```
### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
After entering the container, download the project and run:
```bash
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
```powershell
xmake require -vy "cuda 12.6.3"
```
After the installation is complete, execute:
```powershell
xmake require --info "cuda 12.6.3"
```
The output will look like this:
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
```powershell
set CUDA_PATH=path_to_cuda_installdir
```
Then re-run:
```powershell
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
#### Notice
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
### About Xmake
#### Installing Xmake
You can install Xmake using one of the following methods:
Using curl:
```bash
curl -fsSL https://xmake.io/shget.text | bash
```
Using wget:
```bash
wget https://xmake.io/shget.text -O - | bash
```
Using powershell:
```powershell
irm https://xmake.io/psget.text | iex
```
#### Build Options
```text
# Switch build mode
xmake f -m debug/release
# Optional build parameters
-r : Rebuild the target
-v : Show detailed build logs
-y : Automatically confirm prompts
# Example
xmake b -vy crossdesk
```
#### Run Options
```bash
# Run in debug mode
xmake r -d crossdesk
```
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html).
---
## Self-Hosted Server
### Server
It is recommended to deploy CrossDesk Server using Docker.
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
-e CROSSDESK_SERVER_PORT=xxxx \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
The parameters you need to pay attention to are as follows:
#### Parameters
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **INTERNAL_IP**: The server’s internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
#### Example
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### Notes
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you don’t mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`. As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`.
#### Directory permissions
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
- Certificate generation failure, leading to startup script errors and container exit.
- Database directory creation failure, causing the program to throw exceptions and crash.
- Log directory creation failure, preventing logs from being written (though the program may continue running).
Manually set permissions before starting the container:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
#### TLS certificate
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
### Client
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
**Windows:** Open PowerShell with administrator privileges and execute:
```powershell
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
**Linux:**
```bash
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
**macOS:**
```bash
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Self-Hosted Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
---
## FAQ
See [FAQ](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md).
---
## Acknowledgements
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
---
## Code signing policy
CrossDesk uses SignPath.io to code-sign official Windows releases built from this repository.
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
- **Approvers:** [kunkundi](https://github.com/kunkundi)
- **Privacy policy:** [Read the CrossDesk Privacy Policy](PRIVACY.md#english)
-76
View File
@@ -1,76 +0,0 @@
#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;
}
-21
View File
@@ -1,21 +0,0 @@
#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
-62
View File
@@ -1,62 +0,0 @@
#include "nv12_scaler.h"
#include <cstddef>
#include <limits>
#include "libyuv/planar_functions.h"
namespace crossdesk {
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_uv, int src_stride_uv, int src_width,
int src_height, uint8_t* dst_y, int dst_stride_y,
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
int dst_height, libyuv::FilterMode filtering,
std::vector<uint8_t>* scratch_buffer) {
if (!src_y || !src_uv || !dst_y || !dst_uv || !scratch_buffer ||
src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 ||
(src_width & 1) != 0 || (src_height & 1) != 0 || (dst_width & 1) != 0 ||
(dst_height & 1) != 0) {
return -1;
}
const int src_chroma_width = src_width / 2;
const int src_chroma_height = src_height / 2;
const int dst_chroma_width = dst_width / 2;
const int dst_chroma_height = dst_height / 2;
const size_t src_chroma_size =
static_cast<size_t>(src_chroma_width) * src_chroma_height;
const size_t dst_chroma_size =
static_cast<size_t>(dst_chroma_width) * dst_chroma_height;
const size_t max_size = std::numeric_limits<size_t>::max();
if (src_chroma_size > max_size / 2 || dst_chroma_size > max_size / 2) {
return -1;
}
const size_t src_planes_size = 2 * src_chroma_size;
const size_t dst_planes_size = 2 * dst_chroma_size;
if (src_planes_size > max_size - dst_planes_size) {
return -1;
}
scratch_buffer->resize(src_planes_size + dst_planes_size);
uint8_t* src_u = scratch_buffer->data();
uint8_t* src_v = src_u + src_chroma_size;
uint8_t* dst_u = src_v + src_chroma_size;
uint8_t* dst_v = dst_u + dst_chroma_size;
libyuv::SplitUVPlane(src_uv, src_stride_uv, src_u, src_chroma_width, src_v,
src_chroma_width, src_chroma_width, src_chroma_height);
const int result = libyuv::I420Scale(
src_y, src_stride_y, src_u, src_chroma_width, src_v, src_chroma_width,
src_width, src_height, dst_y, dst_stride_y, dst_u, dst_chroma_width,
dst_v, dst_chroma_width, dst_width, dst_height, filtering);
if (result != 0) {
return result;
}
libyuv::MergeUVPlane(dst_u, dst_chroma_width, dst_v, dst_chroma_width, dst_uv,
dst_stride_uv, dst_chroma_width, dst_chroma_height);
return 0;
}
} // namespace crossdesk
-23
View File
@@ -1,23 +0,0 @@
#ifndef CROSSDESK_COMMON_NV12_SCALER_H_
#define CROSSDESK_COMMON_NV12_SCALER_H_
#include <cstdint>
#include <vector>
#include "libyuv/scale.h"
namespace crossdesk {
// Scales NV12 by separating the interleaved chroma plane before filtering.
// This avoids the downward chroma rounding bias in libyuv::NV12Scale while
// preserving the requested filtering quality.
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
const uint8_t* src_uv, int src_stride_uv, int src_width,
int src_height, uint8_t* dst_y, int dst_stride_y,
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
int dst_height, libyuv::FilterMode filtering,
std::vector<uint8_t>* scratch_buffer);
} // namespace crossdesk
#endif // CROSSDESK_COMMON_NV12_SCALER_H_
@@ -1,44 +0,0 @@
#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
@@ -1,22 +0,0 @@
#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_
@@ -1,79 +0,0 @@
#include "task_queue_lock_free.h"
#include <exception>
#include <new>
#include <stdexcept>
#include <utility>
namespace crossdesk {
TaskQueueLockFree::TaskQueueLockFree()
: producer_(task_queue_), worker_([this] { WorkerThread(); }) {}
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
TaskItem item;
item.run = std::move(task);
auto result = item.completion.get_future();
{
std::lock_guard lock(mutex_);
if (stopping_) {
throw std::runtime_error("Cannot post to a stopped task queue");
}
// Count before publishing: the worker may dequeue as soon as enqueue returns.
++pending_tasks_;
if (!task_queue_.enqueue(producer_, std::move(item))) {
--pending_tasks_;
throw std::bad_alloc();
}
}
wake_.notify_one();
return result;
}
void TaskQueueLockFree::Stop() {
// Concurrent Stop callers all wait for the same worker shutdown to finish.
std::call_once(stop_once_, [this] {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
wake_.notify_all();
worker_.join();
});
}
void TaskQueueLockFree::WorkerThread() {
while (true) {
TaskItem item;
while (task_queue_.try_dequeue(item)) {
--pending_tasks_;
std::exception_ptr error;
try {
item.run();
} catch (...) {
error = std::current_exception();
}
// Release captured resources before marking the task complete.
item.run = nullptr;
if (error) {
item.completion.set_exception(error);
} else {
item.completion.set_value();
}
}
std::unique_lock lock(mutex_);
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
// A stop request must not discard tasks submitted while the worker was idle.
if (stopping_ && pending_tasks_ == 0) {
return;
}
}
}
} // namespace crossdesk
@@ -1,57 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _TASK_QUEUE_LOCK_FREE_H_
#define _TASK_QUEUE_LOCK_FREE_H_
#include <atomic>
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <mutex>
#include <thread>
#include <concurrentqueue.h>
namespace crossdesk {
// A single worker executes tasks in submission order. The concurrent queue
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
class TaskQueueLockFree {
public:
TaskQueueLockFree();
~TaskQueueLockFree();
// Returns completion or the task's exception. Throws if the queue is stopped.
std::future<void> PostTask(std::function<void()> task);
// Rejects new tasks, finishes accepted tasks and joins the worker.
// May be called repeatedly, but never from the queue's own tasks.
void Stop();
private:
struct TaskItem {
std::function<void()> run;
std::promise<void> completion;
};
void WorkerThread();
moodycamel::ConcurrentQueue<TaskItem> task_queue_;
// All submissions share one producer so different caller threads keep FIFO.
moodycamel::ProducerToken producer_;
std::atomic<std::size_t> pending_tasks_{0};
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false;
std::once_flag stop_once_;
std::thread worker_;
};
} // namespace crossdesk
#endif
@@ -1,52 +0,0 @@
/*
* @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
@@ -1,36 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVER_WINDOW_STATE_H_
#define _SERVER_WINDOW_STATE_H_
#include <algorithm>
#include <iterator>
#include <string>
#include <vector>
namespace crossdesk::server_window_state {
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
std::string* selected_id) {
if (!selected_id) {
return 0;
}
if (ids.empty()) {
selected_id->clear();
return 0;
}
const auto selected = std::find(ids.begin(), ids.end(), *selected_id);
if (selected == ids.end()) {
*selected_id = ids.front();
return 0;
}
return static_cast<int>(std::distance(ids.begin(), selected));
}
} // namespace crossdesk::server_window_state
#endif
@@ -1,49 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_GEOMETRY_H_
#define _WINDOW_GEOMETRY_H_
#include <algorithm>
namespace crossdesk::window_geometry {
struct PhysicalRect {
int x = 0;
int y = 0;
int width = 0;
int height = 0;
};
struct PhysicalSize {
int width = 0;
int height = 0;
};
struct PhysicalPosition {
int x = 0;
int y = 0;
};
constexpr PhysicalPosition CenteredPosition(const PhysicalRect& bounds,
const PhysicalSize& size) {
return {
bounds.x + (bounds.width - std::min(bounds.width, size.width)) / 2,
bounds.y + (bounds.height - std::min(bounds.height, size.height)) / 2,
};
}
constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
const PhysicalSize& size) {
return {
bounds.x + std::max(0, bounds.width - size.width),
bounds.y + std::max(0, bounds.height - size.height),
};
}
} // namespace crossdesk::window_geometry
#endif
@@ -1,502 +0,0 @@
#include "rendering/slint_video_presenter.h"
#include <libyuv.h>
#include <slint.h>
#include <algorithm>
#include <cmath>
#include <mutex>
#include <unordered_map>
#include <utility>
#include "nv12_scaler.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#if defined(__APPLE__)
#include "platform/macos/gui/metal_video_renderer.h"
#else
#if defined(_WIN32)
#include <windows.h>
#endif
#include <GL/gl.h>
#include "platform/common/gui/opengl_video_renderer.h"
#endif
namespace crossdesk {
namespace {
#if !defined(__APPLE__)
constexpr GLint kGlClampToEdge = 0x812F;
constexpr float kStreamWindowCornerRadius = 12.0f;
#else
constexpr int kMetalAttachmentAttemptLimit = 30;
#endif
struct VideoRenderSize {
int width = 0;
int height = 0;
bool operator==(const VideoRenderSize&) const = default;
};
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
int area_width, int area_height) {
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
area_height <= 0) {
return {};
}
const double scale =
std::min({1.0, static_cast<double>(area_width) / source_width,
static_cast<double>(area_height) / source_height});
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
width &= ~1;
height &= ~1;
return {std::min(width, source_width & ~1),
std::min(height, source_height & ~1)};
}
} // namespace
struct SlintVideoPresenter::Impl {
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
VideoRenderer& renderer;
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
StateProvider state_provider;
DirtyCallback dirty_callback;
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
std::vector<uint8_t> scaled_video_frame;
std::vector<uint8_t> scaled_video_frame_scratch;
#if defined(__APPLE__)
int attachment_attempts = 0;
bool skip_attachment_once = false;
#else
std::mutex gl_mutex;
std::vector<uint8_t> gl_conversion_frame;
std::vector<uint8_t> gl_pending_frame;
uint32_t gl_texture = 0;
VideoRenderSize gl_texture_size;
VideoRenderSize gl_image_size;
VideoRenderSize gl_pending_size;
bool gl_pending_dirty = false;
#endif
SurfaceState CurrentState() const {
return state_provider ? state_provider() : SurfaceState{};
}
void MarkDirty() const {
if (dirty_callback) {
dirty_callback();
}
}
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
if (remote_id.empty() || sequence == 0 ||
displayed_frame_sequence[remote_id] == sequence) {
return false;
}
displayed_frame_sequence[remote_id] = sequence;
return true;
}
#if !defined(__APPLE__)
void ConfigureOpenGlNotifier() {
if (!stream) {
return;
}
const auto error = (*stream)->window().set_rendering_notifier(
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
return;
}
std::lock_guard lock(gl_mutex);
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
if (state == slint::RenderingState::RenderingSetup) {
GLuint texture = 0;
glGenTextures(1, &texture);
if (texture == 0) {
return;
}
GLint previous_texture = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_texture = texture;
if (opengl_renderer && opengl_renderer->Setup()) {
const std::string selected_stream =
CurrentState().selected_stream;
opengl_renderer->SetSelectedStream(selected_stream);
seeded_frame_sequence.erase(selected_stream);
MarkDirty();
}
return;
}
if (state == slint::RenderingState::BeforeRendering && stream &&
opengl_renderer && opengl_renderer->IsReady() &&
(*stream)->get_native_video_enabled()) {
const SurfaceState surface = CurrentState();
const auto window_size = (*stream)->window().size();
const float scale_factor =
std::max(1.0f, (*stream)->window().scale_factor());
const int top_inset =
!surface.fullscreen && surface.tab_count > 1
? static_cast<int>(std::lround(30.0f * scale_factor))
: 0;
const bool rounded_window = (*stream)->get_custom_titlebar() &&
!surface.fullscreen &&
!(*stream)->window().is_maximized();
const int corner_radius =
rounded_window ? static_cast<int>(std::lround(
kStreamWindowCornerRadius * scale_factor))
: 0;
opengl_renderer->RenderLatest(
surface.selected_stream, static_cast<int>(window_size.width),
static_cast<int>(window_size.height), top_inset, corner_radius);
return;
}
if (state == slint::RenderingState::BeforeRendering &&
gl_texture != 0 && gl_pending_dirty &&
!gl_pending_frame.empty()) {
GLint previous_texture = 0;
GLint previous_unpack_alignment = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, gl_texture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
if (gl_texture_size != gl_pending_size) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
gl_texture_size = gl_pending_size;
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
gl_pending_frame.data());
}
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
gl_pending_dirty = false;
return;
}
if (state == slint::RenderingState::RenderingTeardown) {
if (opengl_renderer) {
opengl_renderer->Teardown();
}
if (gl_texture != 0) {
const GLuint texture = gl_texture;
glDeleteTextures(1, &texture);
}
gl_texture = 0;
gl_texture_size = {};
gl_image_size = {};
gl_pending_size = {};
gl_pending_dirty = false;
gl_pending_frame.clear();
}
});
if (error.has_value()) {
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
} else {
renderer.SetSelectedStream(CurrentState().selected_stream);
}
}
#endif
};
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
: impl_(std::make_unique<Impl>(renderer)) {}
SlintVideoPresenter::~SlintVideoPresenter() = default;
void SlintVideoPresenter::PrepareWindow(
slint::ComponentHandle<ui::StreamWindow>& stream,
StateProvider state_provider, DirtyCallback dirty_callback) {
impl_->stream = &stream;
impl_->state_provider = std::move(state_provider);
impl_->dirty_callback = std::move(dirty_callback);
#if defined(__APPLE__)
stream->set_native_video_enabled(false);
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
impl_->skip_attachment_once = true;
#else
impl_->ConfigureOpenGlNotifier();
#endif
}
bool SlintVideoPresenter::EnsureAttached() {
#if defined(__APPLE__)
if (!impl_->stream || impl_->attachment_attempts <= 0) {
return false;
}
if (impl_->skip_attachment_once) {
impl_->skip_attachment_once = false;
return false;
}
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (!metal_renderer) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (!metal_renderer->IsReady()) {
impl_->attachment_attempts = 0;
(*impl_->stream)->set_native_video_enabled(false);
return false;
}
if (metal_renderer->IsAttached()) {
impl_->attachment_attempts = 0;
return false;
}
if (void* view = (*impl_->stream)->window().appkit_view()) {
(*impl_->stream)->set_native_video_enabled(true);
if (metal_renderer->Attach(view)) {
impl_->attachment_attempts = 0;
const SurfaceState surface = impl_->CurrentState();
impl_->displayed_frame_sequence.erase(surface.selected_stream);
impl_->seeded_frame_sequence.erase(surface.selected_stream);
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
impl_->MarkDirty();
}
return true;
}
(*impl_->stream)->set_native_video_enabled(false);
}
if (--impl_->attachment_attempts == 0) {
(*impl_->stream)->set_native_video_enabled(false);
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
}
#endif
return false;
}
bool SlintVideoPresenter::NeedsRedraw() const {
#if defined(__APPLE__)
const auto* metal_renderer =
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
return metal_renderer && metal_renderer->IsActive() &&
metal_renderer->NeedsSurfaceRedraw();
#else
return false;
#endif
}
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
if (changed && !remote_id.empty()) {
impl_->seeded_frame_sequence.erase(remote_id);
}
return changed;
}
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
const Frame& frame) {
PresentResult result;
if (!impl_->stream || frame.remote_id.empty()) {
return result;
}
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
frame.height > 0 && frame.sequence > 0 &&
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
const auto submit_result = impl_->renderer.SubmitCachedNv12(
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
frame.height);
if (submit_result == VideoRenderer::SubmitResult::submitted ||
submit_result == VideoRenderer::SubmitResult::dropped) {
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
}
}
#if defined(__APPLE__)
auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
if (metal_renderer && metal_renderer->IsActive()) {
const SurfaceState surface = impl_->CurrentState();
const double top_inset =
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
const auto outcome = metal_renderer->RenderLatest(
frame.remote_id, top_inset, !surface.fullscreen);
if (outcome.result == VideoRenderer::RenderResult::rendered) {
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = outcome.width;
result.height = outcome.height;
result.new_frame =
impl_->MarkPresented(frame.remote_id, outcome.sequence);
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
(*impl_->stream)->set_has_frame(false);
}
return result;
}
#else
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
if (opengl_renderer && opengl_renderer->IsReady()) {
opengl_renderer->SetSelectedStream(frame.remote_id);
if (!(*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(true);
}
const bool has_frame =
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
(*impl_->stream)->set_has_frame(has_frame);
if (has_frame) {
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
}
(*impl_->stream)->window().request_redraw();
return result;
}
if ((*impl_->stream)->get_native_video_enabled()) {
(*impl_->stream)->set_native_video_enabled(false);
}
#endif
const size_t nv12_size =
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
frame.nv12->size() < nv12_size) {
(*impl_->stream)->set_has_frame(false);
return result;
}
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
return result;
}
const auto window_size = (*impl_->stream)->window().size();
const VideoRenderSize render_size = FitVideoToRenderArea(
frame.width, frame.height, static_cast<int>(window_size.width),
static_cast<int>(window_size.height));
const uint8_t* y_plane = frame.nv12->data();
const uint8_t* uv_plane =
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
int output_width = frame.width;
int output_height = frame.height;
if (render_size.width > 0 && render_size.height > 0 &&
(render_size.width != frame.width ||
render_size.height != frame.height)) {
const size_t scaled_nv12_size =
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
impl_->scaled_video_frame.resize(scaled_nv12_size);
uint8_t* scaled_y = impl_->scaled_video_frame.data();
uint8_t* scaled_uv =
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
frame.width, frame.height, scaled_y, render_size.width,
scaled_uv, render_size.width, render_size.width,
render_size.height, libyuv::kFilterBox,
&impl_->scaled_video_frame_scratch) == 0) {
y_plane = scaled_y;
uv_plane = scaled_uv;
output_width = render_size.width;
output_height = render_size.height;
}
}
#if !defined(__APPLE__)
uint32_t texture_id = 0;
{
std::lock_guard lock(impl_->gl_mutex);
texture_id = impl_->gl_texture;
}
if (texture_id != 0) {
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
output_height * 4);
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
impl_->gl_conversion_frame.data(), output_width * 4,
output_width, output_height) != 0) {
return result;
}
{
std::lock_guard lock(impl_->gl_mutex);
if (impl_->gl_texture == 0) {
return result;
}
texture_id = impl_->gl_texture;
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
impl_->gl_pending_size = {output_width, output_height};
impl_->gl_pending_dirty = true;
}
const VideoRenderSize output_size{output_width, output_height};
if (impl_->gl_image_size != output_size) {
(*impl_->stream)
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
texture_id,
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
static_cast<uint32_t>(output_height)}));
impl_->gl_image_size = output_size;
}
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
(*impl_->stream)->window().request_redraw();
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
#endif
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
output_height);
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
reinterpret_cast<uint8_t*>(pixels.begin()),
output_width * 3, output_width, output_height) != 0) {
return result;
}
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
(*impl_->stream)->set_has_frame(true);
(*impl_->stream)->set_receiving_text("");
result.width = frame.width;
result.height = frame.height;
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
return result;
}
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
impl_->displayed_frame_sequence.erase(remote_id);
impl_->seeded_frame_sequence.erase(remote_id);
}
void SlintVideoPresenter::Detach() {
impl_->renderer.SetSelectedStream({});
#if defined(__APPLE__)
if (auto* metal_renderer =
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
metal_renderer->Detach();
}
impl_->attachment_attempts = 0;
impl_->skip_attachment_once = false;
#endif
impl_->stream = nullptr;
impl_->state_provider = {};
impl_->dirty_callback = {};
}
} // namespace crossdesk
@@ -1,81 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_VIDEO_PRESENTER_H_
#define _SLINT_VIDEO_PRESENTER_H_
#include <cstddef>
#include <cstdint>
#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
@@ -1,73 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_POSITION_H_
#define _CURSOR_POSITION_H_
#include <algorithm>
#include <vector>
#include <remote_action.h>
#include "display_info.h"
namespace crossdesk {
inline void ResetCursorPosition(CursorState* state) {
if (!state) return;
state->position_update = true;
state->position_valid = false;
state->x = 0.5f;
state->y = 0.5f;
state->visual_offset_x = 0.0f;
state->visual_offset_y = 0.0f;
state->display_id = -1;
}
inline bool NormalizeCursorPosition(
double screen_x, double screen_y,
const std::vector<DisplayInfo>& displays, int preferred_display,
CursorState* state) {
if (!state) return false;
ResetCursorPosition(state);
auto contains = [&](int index) {
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
const auto& display = displays[index];
return display.width > 0 && display.height > 0 &&
screen_x >= display.left && screen_x < display.right &&
screen_y >= display.top && screen_y < display.bottom;
};
int display_id = contains(preferred_display) ? preferred_display : -1;
if (display_id < 0) {
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
if (contains(index)) {
display_id = index;
break;
}
}
}
if (display_id < 0) return false;
const auto& display = displays[display_id];
// Screen coordinates describe a continuous rectangle with an exclusive
// right/bottom edge. Use its full extent so feedback is the exact inverse of
// normalized input instead of accumulating a one-pixel edge convention.
const double horizontal_extent = std::max(display.width, 1);
const double vertical_extent = std::max(display.height, 1);
state->position_valid = true;
state->x = static_cast<float>(std::clamp(
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
state->y = static_cast<float>(std::clamp(
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
state->display_id = display_id;
return true;
}
} // namespace crossdesk
#endif
@@ -1,136 +0,0 @@
#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
@@ -1,39 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CURSOR_STATE_PROVIDER_H_
#define _CURSOR_STATE_PROVIDER_H_
#include <memory>
#include <vector>
#include <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
@@ -1,61 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_CACHE_H_
#define _DEVICE_PRESENCE_CACHE_H_
#include <chrono>
#include <mutex>
#include <string>
#include <unordered_map>
namespace crossdesk {
class DevicePresenceCache {
public:
using Clock = std::chrono::steady_clock;
static constexpr auto kMaxAge = std::chrono::seconds(60);
void SetSignalConnected(bool connected) {
std::lock_guard<std::mutex> lock(mutex_);
signal_connected_ = connected;
cache_.clear();
}
void SetOnline(const std::string &device_id, bool online,
Clock::time_point now = Clock::now()) {
std::lock_guard<std::mutex> lock(mutex_);
if (signal_connected_) {
cache_[device_id] = {online, now};
}
}
bool IsOnline(const std::string &device_id,
Clock::time_point now = Clock::now()) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = cache_.find(device_id);
return signal_connected_ && it != cache_.end() && it->second.online &&
now - it->second.updated_at < kMaxAge;
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
struct Entry {
bool online;
Clock::time_point updated_at;
};
bool signal_connected_ = false;
std::unordered_map<std::string, Entry> cache_;
mutable std::mutex mutex_;
};
} // namespace crossdesk
#endif
@@ -1,43 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_REQUEST_H_
#define _DEVICE_PRESENCE_REQUEST_H_
#include <algorithm>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace crossdesk {
inline nlohmann::json BuildDevicePresenceRequest(
const std::string& client_id, const std::vector<std::string>& recent_ids,
bool subscribe, const std::string& probe_id = {}) {
std::vector<std::string> devices;
for (const auto& id : recent_ids) {
// Recent connection keys contain Y/N plus display settings after the ID.
const std::string pure_id = id.substr(0, id.find_first_of("YN"));
if (!pure_id.empty() &&
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
devices.push_back(pure_id);
}
}
if (!probe_id.empty() &&
std::find(devices.begin(), devices.end(), probe_id) == devices.end()) {
devices.push_back(probe_id);
}
// Include recent devices even in a one-shot probe: older servers ignore
// subscribe=false and replace their subscriptions with this device list.
return {{"type", "recent_connections_presence"},
{"user_id", client_id},
{"devices", devices},
{"subscribe", subscribe}};
}
} // namespace crossdesk
#endif
-19
View File
@@ -1,19 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _GUI_STATE_H_
#define _GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
namespace crossdesk::gui_detail {
struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif
@@ -1,188 +0,0 @@
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "platform/video_renderer.h"
#include "runtime/gui_runtime.h"
#include "runtime/peer_event_handler.h"
namespace crossdesk {
namespace {
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
const MiniRtcNativeVideoFrame* GetNativeVideoFrame(const MiniRtcVideoFrame* frame) {
if (!frame || !frame->native_frame) {
return nullptr;
}
const auto* native = frame->native_frame;
return native->struct_size >=
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
native->owner && native->copy_to_nv12
? native
: nullptr;
}
} // namespace
void PeerEventHandler::OnReceiveVideoBuffer(
const MiniRtcVideoFrame* video_frame, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string remote_id(user_id, user_id_size);
auto props = runtime->FindRemoteSession(remote_id);
if (!props) return;
if (props->connection_established_) {
bool background_snapshot_only = false;
auto* native_renderer = runtime->video_renderer_.get();
if (native_renderer && native_renderer->IsActive()) {
VideoRenderer::SubmitResult submit_result =
VideoRenderer::SubmitResult::failed;
if (GetNativeVideoFrame(video_frame)) {
submit_result = native_renderer->SubmitNativeFrame(
remote_id, *video_frame->native_frame);
} else
{
submit_result = native_renderer->SubmitNv12(
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
video_frame->size, video_frame->width, video_frame->height);
}
if (submit_result == VideoRenderer::SubmitResult::submitted) {
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
// Once the native renderer owns the current stream, do not leave an
// older CPU frame ahead of the renderer's close snapshot.
props->front_frame_.reset();
props->back_frame_.reset();
props->thumbnail_frame_.reset();
props->thumbnail_width_ = 0;
props->thumbnail_height_ = 0;
props->background_snapshot_time_ = {};
++props->video_frame_sequence_;
props->streaming_ = true;
runtime->video_frame_dirty_.store(true, std::memory_order_release);
return;
}
if (submit_result == VideoRenderer::SubmitResult::dropped ||
submit_result == VideoRenderer::SubmitResult::failed) {
// Keep presenting the last native frame. A later decoded frame can
// reuse the renderer without changing ownership mid-window.
props->streaming_ = true;
return;
}
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
background_snapshot_only = true;
}
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
const auto now = std::chrono::steady_clock::now();
if (background_snapshot_only && props->thumbnail_frame_ &&
!props->thumbnail_frame_->empty() &&
props->background_snapshot_time_ !=
std::chrono::steady_clock::time_point{} &&
now - props->background_snapshot_time_ <
kBackgroundSnapshotInterval) {
props->streaming_ = true;
return;
}
size_t frame_size = video_frame->size;
const MiniRtcNativeVideoFrame* native_frame =
GetNativeVideoFrame(video_frame);
if (native_frame) {
frame_size = static_cast<size_t>(native_frame->width) *
native_frame->height * 3U / 2U;
}
if (frame_size == 0) {
return;
}
// Allocate a third buffer only while the UI still owns the old snapshot.
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(frame_size);
}
if (props->back_frame_->size() != frame_size) {
props->back_frame_->resize(frame_size);
}
if (native_frame) {
if (native_frame->copy_to_nv12(
native_frame->owner, props->back_frame_->data(),
props->back_frame_->size()) != 0) {
return;
}
} else
{
if (!video_frame->data || video_frame->size < frame_size) {
return;
}
std::memcpy(props->back_frame_->data(), video_frame->data, frame_size);
}
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = frame_size;
props->front_frame_.swap(props->back_frame_);
props->thumbnail_frame_ = props->front_frame_;
props->thumbnail_width_ = video_frame->width;
props->thumbnail_height_ = video_frame->height;
if (background_snapshot_only) {
props->background_snapshot_time_ = now;
}
++props->video_frame_sequence_;
}
props->streaming_ = true;
if (background_snapshot_only) {
return;
}
runtime->video_frame_dirty_.store(true, std::memory_order_release);
}
}
void PeerEventHandler::OnReceiveAudioBuffer(
const char* data, size_t size, const char* user_id, size_t user_id_size,
const char* src_id, size_t src_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
if (!handler) return;
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
runtime->audio_buffer_fresh_ = true;
runtime->devices_.PushAudio(data, size);
}
} // namespace crossdesk
@@ -1,47 +0,0 @@
#include "runtime/session_lifecycle.h"
namespace crossdesk::gui_detail {
CallbackGuard::CallbackGuard(SessionCallbackGate& callbacks)
: callbacks_(callbacks), accepted_(callbacks.BeginCallback()) {}
CallbackGuard::~CallbackGuard() {
if (accepted_) {
callbacks_.EndCallback();
}
}
bool SessionCallbackGate::IsActive() const {
std::lock_guard lock(mutex_);
return accepting_callbacks_;
}
CallbackGuard SessionCallbackGate::Enter() {
return CallbackGuard(*this);
}
bool SessionCallbackGate::BeginCallback() {
std::lock_guard lock(mutex_);
if (!accepting_callbacks_) {
return false;
}
++running_callbacks_;
return true;
}
void SessionCallbackGate::EndCallback() {
std::lock_guard lock(mutex_);
--running_callbacks_;
if (running_callbacks_ == 0) {
callbacks_finished_.notify_all();
}
}
void SessionCallbackGate::Close() {
std::unique_lock lock(mutex_);
accepting_callbacks_ = false;
// Waiting releases the lock so running callbacks can call EndCallback().
callbacks_finished_.wait(lock, [this] { return running_callbacks_ == 0; });
}
} // namespace crossdesk::gui_detail
@@ -1,58 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SESSION_LIFECYCLE_H_
#define _SESSION_LIFECYCLE_H_
#include <condition_variable>
#include <cstddef>
#include <mutex>
namespace crossdesk::gui_detail {
class SessionCallbackGate;
// Decrements the count on scope exit, including early returns and exceptions.
class CallbackGuard {
public:
explicit CallbackGuard(SessionCallbackGate& callbacks);
~CallbackGuard();
CallbackGuard(const CallbackGuard&) = delete;
CallbackGuard& operator=(const CallbackGuard&) = delete;
explicit operator bool() const { return accepted_; }
private:
SessionCallbackGate& callbacks_;
bool accepted_;
};
// Each peer counts callbacks as they enter and leave.
// Closing rejects new callbacks, then waits for running callbacks to finish.
class SessionCallbackGate {
public:
bool IsActive() const;
CallbackGuard Enter();
// Called by the session owner, never from within this session's callbacks.
void Close();
private:
friend class CallbackGuard;
bool BeginCallback();
void EndCallback();
mutable std::mutex mutex_;
std::condition_variable callbacks_finished_;
bool accepting_callbacks_ = true;
std::size_t running_callbacks_ = 0;
};
} // namespace crossdesk::gui_detail
#endif
-172
View File
@@ -1,172 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _UI_LOCALIZATION_H_
#define _UI_LOCALIZATION_H_
#include <slint.h>
#include <string>
#include "crossdesk_ui.h"
#include "localization.h"
namespace crossdesk::ui_localization {
inline slint::SharedString Text(const std::string& value) {
return slint::SharedString(value);
}
inline int ApplyMainWindowStrings(
const slint::ComponentHandle<ui::MainWindow>& window,
int language_index) {
const int language =
localization::detail::ClampLanguageIndex(language_index);
auto& strings = window->global<ui::UiStrings>();
strings.set_local_desktop(Text(localization::local_desktop[language]));
strings.set_remote_desktop(Text(localization::remote_desktop[language]));
strings.set_recent_connections(
Text(localization::recent_connections[language]));
strings.set_local_id(Text(localization::local_id[language]));
strings.set_device_name(Text(localization::device_name[language]));
strings.set_remote_id(Text(localization::remote_id[language]));
strings.set_online(Text(localization::online[language]));
strings.set_offline(Text(localization::offline[language]));
strings.set_password(Text(localization::password[language]));
strings.set_copied(
Text(localization::local_id_copied_to_clipboard[language]));
strings.set_connect(Text(localization::connect[language]));
strings.set_settings(Text(localization::settings[language]));
strings.set_about(Text(localization::about[language]));
strings.set_ok(Text(localization::ok[language]));
strings.set_cancel(Text(localization::cancel[language]));
strings.set_new_password(Text(localization::new_password[language]));
strings.set_invalid_password(Text(localization::max_password_len[language]));
strings.set_edit_alias(
Text(localization::input_connection_alias[language]));
strings.set_delete_connection(
Text(localization::delete_connection[language]));
strings.set_confirm_delete(
Text(localization::confirm_delete_connection[language]));
strings.set_language(Text(localization::language[language]));
strings.set_video_quality(Text(localization::video_quality[language]));
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
strings.set_adaptation_policy(
Text(localization::video_adaptation_policy[language]));
strings.set_priority_frame_rate(
Text(localization::video_priority_frame_rate[language]));
strings.set_priority_quality(
Text(localization::video_priority_quality[language]));
strings.set_priority_balanced(
Text(localization::video_priority_balanced[language]));
strings.set_codec(Text(localization::video_encode_format[language]));
strings.set_hardware_codec(
Text(localization::enable_hardware_video_codec[language]));
strings.set_turn_relay(Text(localization::enable_turn[language]));
strings.set_srtp(Text(localization::enable_srtp[language]));
strings.set_self_hosted(
Text(localization::self_hosted_server_config[language]));
strings.set_autostart(Text(localization::enable_autostart[language]));
strings.set_daemon(Text(localization::enable_daemon[language]));
strings.set_file_save_path(
Text(localization::file_transfer_save_path[language]));
strings.set_default_desktop(Text(localization::default_desktop[language]));
strings.set_server_host(
Text(localization::self_hosted_server_address[language]));
strings.set_server_port(
Text(localization::self_hosted_server_port[language]));
strings.set_coturn_port(
Text(localization::self_hosted_server_coturn_server_port[language]));
strings.set_version(Text(localization::version[language]));
strings.set_signal_connected(Text(localization::signal_connected[language]));
strings.set_signal_disconnected(
Text(localization::signal_disconnected[language]));
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
strings.set_update_available(
Text(localization::new_version_available[language]));
strings.set_release_notes(Text(localization::release_notes[language]));
strings.set_download(Text(localization::update[language]));
strings.set_input_password(Text(localization::input_password[language]));
strings.set_reinput_password(Text(localization::reinput_password[language]));
strings.set_remember_password(
Text(localization::remember_password[language]));
strings.set_validate_password(
Text(localization::validate_password[language]));
strings.set_request_permissions(
Text(localization::request_permissions[language]));
strings.set_permission_required(
Text(localization::permission_required_message[language]));
strings.set_screen_recording_permission(
Text(localization::screen_recording_permission[language]));
strings.set_accessibility_permission(
Text(localization::accessibility_permission[language]));
strings.set_service_setup_title(
Text(localization::windows_service_setup_title[language]));
strings.set_service_setup_message(
Text(localization::windows_service_setup_message[language]));
strings.set_service_settings_label(
Text(localization::windows_service_settings_label[language]));
strings.set_install_service(
Text(localization::install_windows_service[language]));
strings.set_service_installed(
Text(localization::windows_service_installed[language]));
strings.set_do_not_remind(
Text(localization::do_not_remind_again[language]));
strings.set_notification(Text(localization::notification[language]));
strings.set_service_suppressed_message(
Text(localization::windows_service_prompt_suppressed_message[language]));
strings.set_quality_low(Text(localization::video_quality_low[language]));
strings.set_quality_medium(
Text(localization::video_quality_medium[language]));
strings.set_quality_high(Text(localization::video_quality_high[language]));
strings.set_codec_h264(Text(localization::h264[language]));
strings.set_codec_av1(Text(localization::av1[language]));
strings.set_self_hosted_settings(
Text(localization::self_hosted_server_settings[language]));
strings.set_access_website(Text(localization::access_website[language]));
strings.set_release_date_label(Text(localization::release_date[language]));
strings.set_later(Text(localization::cancel[language]));
return language;
}
template <typename Window>
inline int ApplyStreamWindowStrings(
const slint::ComponentHandle<Window>& window, int language_index) {
const int language =
localization::detail::ClampLanguageIndex(language_index);
auto& strings = window->template global<ui::StreamStrings>();
strings.set_select_display(Text(localization::select_display[language]));
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
strings.set_control_mouse(Text(localization::control_mouse[language]));
strings.set_release_mouse(Text(localization::release_mouse[language]));
strings.set_audio(Text(localization::audio_capture[language]));
strings.set_mute(Text(localization::mute[language]));
strings.set_select_file(Text(localization::select_file[language]));
strings.set_show_stats(
Text(localization::show_net_traffic_stats[language]));
strings.set_hide_stats(
Text(localization::hide_net_traffic_stats[language]));
strings.set_fullscreen(Text(localization::fullscreen[language]));
strings.set_exit_fullscreen(Text(localization::exit_fullscreen[language]));
strings.set_disconnect(Text(localization::disconnect[language]));
strings.set_file_transfer(
Text(localization::file_transfer_progress[language]));
strings.set_expand_control(Text(localization::expand_control_bar[language]));
strings.set_collapse_control(
Text(localization::collapse_control_bar[language]));
strings.set_stats_in(Text(localization::in[language]));
strings.set_stats_out(Text(localization::out[language]));
strings.set_stats_loss_rate(Text(localization::loss_rate[language]));
strings.set_stats_resolution(Text(localization::resolution[language]));
strings.set_stats_connection_mode(
Text(localization::connection_mode[language]));
return language;
}
} // namespace crossdesk::ui_localization
#endif
@@ -1,60 +0,0 @@
#include "path_manager.h"
#include "platform/path_backend.h"
#ifndef CROSSDESK_PORTABLE
#define CROSSDESK_PORTABLE 0
#endif
namespace {
#if CROSSDESK_PORTABLE
std::filesystem::path GetPortableRootPath() {
std::filesystem::path executable_dir =
crossdesk::platform::GetExecutableDirectory();
if (!executable_dir.empty()) return executable_dir;
std::error_code error;
const std::filesystem::path current =
std::filesystem::current_path(error);
return error ? std::filesystem::path(".") : current;
}
#endif
} // namespace
namespace crossdesk {
PathManager::PathManager(const std::string& app_name) : app_name_(app_name) {}
std::filesystem::path PathManager::GetConfigPath() {
#if CROSSDESK_PORTABLE
return GetPortableRootPath() / "data";
#else
return platform::GetConfigPath(app_name_);
#endif
}
std::filesystem::path PathManager::GetCachePath() {
#if CROSSDESK_PORTABLE
return GetPortableRootPath() / "data";
#else
return platform::GetCachePath(app_name_);
#endif
}
std::filesystem::path PathManager::GetLogPath() {
#if CROSSDESK_PORTABLE
return GetPortableRootPath() / "logs";
#else
return platform::GetLogPath(app_name_);
#endif
}
bool PathManager::CreateDirectories(const std::filesystem::path& path) {
std::error_code error;
const bool created = std::filesystem::create_directories(path, error);
return !error && (created || std::filesystem::exists(path));
}
} // namespace crossdesk
@@ -1,16 +0,0 @@
#ifndef CROSSDESK_PLATFORM_AUTOSTART_BACKEND_H_
#define CROSSDESK_PLATFORM_AUTOSTART_BACKEND_H_
#include <string>
namespace crossdesk::platform {
std::string GetAutostartExecutablePath();
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path);
bool DisableAutostart(const std::string& app_name);
bool IsAutostartEnabled(const std::string& app_name);
} // namespace crossdesk::platform
#endif // CROSSDESK_PLATFORM_AUTOSTART_BACKEND_H_
@@ -1,13 +0,0 @@
#ifndef CROSSDESK_PLATFORM_CLIPBOARD_BACKEND_H_
#define CROSSDESK_PLATFORM_CLIPBOARD_BACKEND_H_
namespace crossdesk::platform {
bool ClipboardMonitoring();
void HandleClipboardChange();
void RunClipboardMonitor(int check_interval_ms);
void WakeClipboardMonitor();
} // namespace crossdesk::platform
#endif // CROSSDESK_PLATFORM_CLIPBOARD_BACKEND_H_
@@ -1,82 +0,0 @@
#include "platform/common/gui/cuda_driver.h"
#if CROSSDESK_HAS_CUDA_DRIVER
#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <type_traits>
#include "rd_log.h"
namespace crossdesk {
const CudaDriver* CudaDriver::Get() {
static const CudaDriver driver;
return driver.loaded_ ? &driver : nullptr;
}
void* CudaDriver::Symbol(const char* name) const {
#if defined(_WIN32)
return reinterpret_cast<void*>(
GetProcAddress(static_cast<HMODULE>(module_), name));
#else
return dlsym(module_, name);
#endif
}
CudaDriver::CudaDriver() {
#if defined(_WIN32)
module_ = LoadLibraryW(L"nvcuda.dll");
#else
module_ = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL);
#endif
if (!module_) return;
const auto load = [this](auto& function, const char* name) {
function = reinterpret_cast<std::decay_t<decltype(function)>>(Symbol(name));
return function != nullptr;
};
loaded_ = load(init, "cuInit") &&
load(device_get_count, "cuDeviceGetCount") &&
load(device_get, "cuDeviceGet") &&
load(context_create, "cuCtxCreate_v2") &&
load(context_destroy, "cuCtxDestroy_v2") &&
load(context_push, "cuCtxPushCurrent_v2") &&
load(context_pop, "cuCtxPopCurrent_v2") &&
load(error_name, "cuGetErrorName") &&
load(mem_alloc, "cuMemAlloc_v2") &&
load(mem_free, "cuMemFree_v2") &&
load(memcpy_2d, "cuMemcpy2D_v2") &&
load(memcpy_2d_async, "cuMemcpy2DAsync_v2") &&
load(stream_create, "cuStreamCreate") &&
load(stream_destroy, "cuStreamDestroy_v2") &&
load(stream_synchronize, "cuStreamSynchronize") &&
load(register_gl_buffer, "cuGraphicsGLRegisterBuffer") &&
load(unregister_resource, "cuGraphicsUnregisterResource") &&
load(map_resources, "cuGraphicsMapResources") &&
load(unmap_resources, "cuGraphicsUnmapResources") &&
load(mapped_pointer, "cuGraphicsResourceGetMappedPointer_v2");
}
CudaDriver::~CudaDriver() {
if (!module_) return;
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(module_));
#else
dlclose(module_);
#endif
}
bool CudaDriver::Check(CUresult result, const char* operation) const {
if (result == CUDA_SUCCESS) return true;
const char* name = nullptr;
error_name(result, &name);
LOG_WARN("CUDA/OpenGL {} failed: {} ({})", operation,
name ? name : "unknown CUDA error", static_cast<int>(result));
return false;
}
} // namespace crossdesk
#endif
@@ -1,63 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_DRIVER_H_
#define _CUDA_DRIVER_H_
#if defined(USE_CUDA) && USE_CUDA && \
(defined(_WIN32) || \
(defined(__linux__) && (defined(__x86_64__) || defined(__amd64__))))
#define CROSSDESK_HAS_CUDA_DRIVER 1
#include <cuda.h>
namespace crossdesk {
// Application-owned driver entry points. No codec library or MiniRTC internals
// are involved; the module remains loaded for this singleton's lifetime.
class CudaDriver {
public:
static const CudaDriver* Get();
bool Check(CUresult result, const char* operation) const;
decltype(&cuInit) init = nullptr;
decltype(&cuDeviceGetCount) device_get_count = nullptr;
decltype(&cuDeviceGet) device_get = nullptr;
CUresult(CUDAAPI* context_create)(CUcontext*, unsigned int, CUdevice) = nullptr;
decltype(&cuCtxDestroy) context_destroy = nullptr;
decltype(&cuCtxPushCurrent) context_push = nullptr;
decltype(&cuCtxPopCurrent) context_pop = nullptr;
decltype(&cuGetErrorName) error_name = nullptr;
decltype(&cuMemAlloc) mem_alloc = nullptr;
decltype(&cuMemFree) mem_free = nullptr;
decltype(&cuMemcpy2D) memcpy_2d = nullptr;
decltype(&cuMemcpy2DAsync) memcpy_2d_async = nullptr;
decltype(&cuStreamCreate) stream_create = nullptr;
decltype(&cuStreamDestroy) stream_destroy = nullptr;
decltype(&cuStreamSynchronize) stream_synchronize = nullptr;
CUresult(CUDAAPI* register_gl_buffer)(CUgraphicsResource*, unsigned int,
unsigned int) = nullptr;
decltype(&cuGraphicsUnregisterResource) unregister_resource = nullptr;
decltype(&cuGraphicsMapResources) map_resources = nullptr;
decltype(&cuGraphicsUnmapResources) unmap_resources = nullptr;
decltype(&cuGraphicsResourceGetMappedPointer) mapped_pointer = nullptr;
private:
CudaDriver();
~CudaDriver();
CudaDriver(const CudaDriver&) = delete;
CudaDriver& operator=(const CudaDriver&) = delete;
void* Symbol(const char* name) const;
void* module_ = nullptr;
bool loaded_ = false;
};
} // namespace crossdesk
#else
#define CROSSDESK_HAS_CUDA_DRIVER 0
#endif
#endif
@@ -1,198 +0,0 @@
#include "platform/common/gui/cuda_opengl_interop.h"
#include "platform/common/gui/cuda_driver.h"
#include <memory>
#include <new>
#include "platform/common/gui/native_video_frame_ref.h"
namespace crossdesk {
#if CROSSDESK_HAS_CUDA_DRIVER
namespace {
class ScopedCudaContext {
public:
ScopedCudaContext(const CudaDriver& driver, CUcontext context)
: cuda(driver),
active_(cuda.Check(cuda.context_push(context), "cuCtxPushCurrent")) {}
~ScopedCudaContext() { Pop(); }
explicit operator bool() const { return active_; }
bool Pop() {
if (!active_) return true;
active_ = false;
CUcontext context = nullptr;
return cuda.Check(cuda.context_pop(&context), "cuCtxPopCurrent");
}
private:
const CudaDriver& cuda;
bool active_;
};
bool IsCudaFrame(const MiniRtcNativeVideoFrame* frame) {
return frame && GetOpenGlNativeFrame(*frame) &&
frame->type == MiniRtcNativeVideoFrameCudaNv12;
}
} // namespace
struct CudaOpenGlVideoBuffer {
const CudaDriver* driver = nullptr;
CUcontext context = nullptr;
CUstream stream = nullptr;
CUgraphicsResource resource = nullptr;
bool mapped = false;
bool failed = false;
NativeVideoFrameRef source_frame;
};
bool IsCudaDeviceAvailable() {
const auto* cuda = CudaDriver::Get();
int count = 0;
return cuda && cuda->init(0) == CUDA_SUCCESS &&
cuda->device_get_count(&count) == CUDA_SUCCESS && count > 0;
}
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer) {
const auto* driver = CudaDriver::Get();
if (!IsCudaFrame(frame) || gl_buffer == 0 || !driver) {
return nullptr;
}
std::unique_ptr<CudaOpenGlVideoBuffer, decltype(&DestroyCudaOpenGlVideoBuffer)>
buffer(new (std::nothrow) CudaOpenGlVideoBuffer,
&DestroyCudaOpenGlVideoBuffer);
if (!buffer) return nullptr;
buffer->driver = driver;
const auto& cuda = *driver;
buffer->context = static_cast<CUcontext>(frame->payload.cuda_nv12.context);
buffer->source_frame = NativeVideoFrameRef(frame);
ScopedCudaContext context(cuda, buffer->context);
if (!context ||
!cuda.Check(cuda.stream_create(&buffer->stream,
CU_STREAM_NON_BLOCKING),
"cuStreamCreate") ||
!cuda.Check(cuda.register_gl_buffer(
&buffer->resource, gl_buffer,
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD),
"cuGraphicsGLRegisterBuffer") ||
!context.Pop()) {
return nullptr;
}
return buffer.release();
}
int UploadCudaFrameToOpenGlBuffer(
CudaOpenGlVideoBuffer* buffer, const MiniRtcNativeVideoFrame* frame) {
if (!buffer || buffer->failed || !IsCudaFrame(frame) ||
frame->payload.cuda_nv12.context != buffer->context) {
return -1;
}
const auto& cuda = *buffer->driver;
ScopedCudaContext context(cuda, buffer->context);
if (!context) {
buffer->failed = true;
return -1;
}
// The caller has completed this buffer's previous GL use. Hold the new
// surface through asynchronous copies, including partially failed uploads.
buffer->source_frame = NativeVideoFrameRef(frame);
const char* operation = "cuGraphicsMapResources";
CUresult result =
cuda.map_resources(1, &buffer->resource, buffer->stream);
buffer->mapped = result == CUDA_SUCCESS;
CUdeviceptr destination = 0;
size_t mapped_size = 0;
if (result == CUDA_SUCCESS) {
operation = "cuGraphicsResourceGetMappedPointer";
result = cuda.mapped_pointer(
&destination, &mapped_size, buffer->resource);
}
const size_t y_size = static_cast<size_t>(frame->width) * frame->height;
if (result == CUDA_SUCCESS && mapped_size < y_size + y_size / 2) {
operation = "validate mapped buffer size";
result = CUDA_ERROR_INVALID_VALUE;
}
if (result == CUDA_SUCCESS) {
CUDA_MEMCPY2D copy{};
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
copy.srcDevice = frame->payload.cuda_nv12.y_device_pointer;
copy.srcPitch = frame->payload.cuda_nv12.y_stride;
copy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
copy.dstDevice = destination;
copy.dstPitch = frame->width;
copy.WidthInBytes = frame->width;
copy.Height = frame->height;
operation = "cuMemcpy2DAsync (Y plane)";
result = cuda.memcpy_2d_async(&copy, buffer->stream);
if (result == CUDA_SUCCESS) {
copy.srcDevice = frame->payload.cuda_nv12.uv_device_pointer;
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
copy.dstDevice = destination + y_size;
copy.Height = frame->height / 2;
operation = "cuMemcpy2DAsync (UV plane)";
result = cuda.memcpy_2d_async(&copy, buffer->stream);
}
}
if (buffer->mapped) {
const CUresult unmap =
cuda.unmap_resources(1, &buffer->resource, buffer->stream);
buffer->mapped = unmap != CUDA_SUCCESS;
if (result == CUDA_SUCCESS) {
operation = "cuGraphicsUnmapResources";
result = unmap;
} else {
cuda.Check(unmap, "cuGraphicsUnmapResources (cleanup)");
}
}
const bool copied = cuda.Check(result, operation);
const bool popped = context.Pop();
buffer->failed = !copied || !popped;
return buffer->failed ? -1 : 0;
}
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer) {
if (!buffer) return;
// Keep the frame's device context alive until after the context is popped.
std::unique_ptr<CudaOpenGlVideoBuffer> owner(buffer);
const auto& cuda = *buffer->driver;
ScopedCudaContext context(cuda, buffer->context);
if (!context) return;
if (buffer->stream) {
cuda.Check(cuda.stream_synchronize(buffer->stream),
"cuStreamSynchronize (destroy)");
}
if (buffer->mapped) {
cuda.Check(cuda.unmap_resources(
1, &buffer->resource, buffer->stream),
"cuGraphicsUnmapResources (destroy)");
cuda.Check(cuda.stream_synchronize(buffer->stream),
"cuStreamSynchronize (unmap)");
}
if (buffer->resource) {
cuda.Check(cuda.unregister_resource(buffer->resource),
"cuGraphicsUnregisterResource");
}
if (buffer->stream) {
cuda.Check(cuda.stream_destroy(buffer->stream), "cuStreamDestroy");
}
}
#else
bool IsCudaDeviceAvailable() { return false; }
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame*, uint32_t) {
return nullptr;
}
int UploadCudaFrameToOpenGlBuffer(
CudaOpenGlVideoBuffer*, const MiniRtcNativeVideoFrame*) {
return -1;
}
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer*) {}
#endif
} // namespace crossdesk
@@ -1,35 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_OPENGL_INTEROP_H_
#define _CUDA_OPENGL_INTEROP_H_
#include "minirtc.h"
namespace crossdesk {
bool IsCudaDeviceAvailable();
struct CudaOpenGlVideoBuffer;
// These operations require the matching GL context to be current. The caller
// owns the GL buffer and must allocate its storage before registering it.
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer);
// Asynchronous, GPU-only NV12 copy. Retains the source through the next upload
// or destruction, including on failure. Before reusing the buffer, the caller
// must wait for the previous GL fence. Frames must use the same CUDA context.
int UploadCudaFrameToOpenGlBuffer(CudaOpenGlVideoBuffer* buffer,
const MiniRtcNativeVideoFrame* frame);
// Finish prior GL use before calling. Waits for CUDA work and releases retained
// frames; the caller may then resize/delete the GL buffer. Accepts null.
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer);
} // namespace crossdesk
#endif
@@ -1,99 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NATIVE_VIDEO_FRAME_REF_H_
#define _NATIVE_VIDEO_FRAME_REF_H_
#include <limits>
#include <utility>
#include "minirtc.h"
namespace crossdesk {
class NativeVideoFrameRef {
public:
NativeVideoFrameRef() = default;
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame *frame) {
if (frame && frame->owner && frame->retain && frame->release) {
frame_ = *frame;
frame_.struct_size = sizeof(frame_);
frame_.retain(frame_.owner);
valid_ = true;
}
}
NativeVideoFrameRef(const NativeVideoFrameRef &other)
: NativeVideoFrameRef(other.Get()) {}
NativeVideoFrameRef(NativeVideoFrameRef &&other) noexcept
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
other.frame_ = {};
}
NativeVideoFrameRef &operator=(NativeVideoFrameRef other) noexcept {
Swap(other);
return *this;
}
~NativeVideoFrameRef() { Reset(); }
void Reset() {
if (valid_ && frame_.owner && frame_.release) {
frame_.release(frame_.owner);
}
frame_ = {};
valid_ = false;
}
void Swap(NativeVideoFrameRef &other) noexcept {
std::swap(frame_, other.frame_);
std::swap(valid_, other.valid_);
}
const MiniRtcNativeVideoFrame *Get() const { return valid_ ? &frame_ : nullptr; }
explicit operator bool() const { return valid_; }
private:
MiniRtcNativeVideoFrame frame_{};
bool valid_ = false;
};
inline const MiniRtcNativeVideoFrame *GetOpenGlNativeFrame(
const MiniRtcNativeVideoFrame &frame) {
const MiniRtcNativeVideoFrame *native = &frame;
if (native->struct_size < static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) ||
!native->owner || !native->retain || !native->release ||
!native->copy_to_nv12 || native->width == 0 || native->height == 0 ||
(native->width & 1U) != 0 || (native->height & 1U) != 0 ||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
native->height > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
return nullptr;
}
if (native->type == MiniRtcNativeVideoFrameCpuNv12) {
return native->payload.cpu_nv12.y_plane &&
native->payload.cpu_nv12.uv_plane &&
native->payload.cpu_nv12.y_stride >= native->width &&
native->payload.cpu_nv12.uv_stride >= native->width
? native
: nullptr;
}
if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
return native->payload.cuda_nv12.y_device_pointer != 0 &&
native->payload.cuda_nv12.uv_device_pointer != 0 &&
native->payload.cuda_nv12.y_stride >= native->width &&
native->payload.cuda_nv12.uv_stride >= native->width &&
native->payload.cuda_nv12.context
? native
: nullptr;
}
return nullptr;
}
} // namespace crossdesk
#endif
File diff suppressed because it is too large Load Diff
@@ -1,72 +0,0 @@
#ifndef CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
#define CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "platform/video_renderer.h"
namespace crossdesk {
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
// MiniRTC submits cross-platform native NV12 descriptors or tightly packed CPU
// fallback frames from its decode callback thread. CUDA frames rotate through
// fenced OpenGL upload resources; CPU descriptors expose their planes without
// an intermediate queue copy. The UI thread always consumes the newest frame.
class OpenGlVideoRenderer final : public VideoRenderer {
public:
OpenGlVideoRenderer();
~OpenGlVideoRenderer() override;
OpenGlVideoRenderer(const OpenGlVideoRenderer &) = delete;
OpenGlVideoRenderer &operator=(const OpenGlVideoRenderer &) = delete;
// These methods must run while Slint's OpenGL context is current, from the
// RenderingSetup and RenderingTeardown notifier states respectively.
bool Setup();
void Teardown();
bool IsReady() const override;
bool IsActive() const override;
// Only the selected stream is queued for upload. Frames for other tabs are
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
bool SetSelectedStream(std::string remote_id) override;
void DiscardStream(std::string_view remote_id) override;
// Thread-safe; called from MiniRTC's decode callback thread.
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t *data,
size_t size, int width, int height) override;
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
size_t size, int width, int height) override;
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame &frame) override;
// Draws below Slint while its OpenGL context is current. All dimensions are
// physical pixels in the window client area. corner_radius_pixels clips the
// underlay to Slint's custom rounded window surface.
RenderOutcome RenderLatest(std::string_view remote_id, int target_width,
int target_height, int top_inset_pixels,
int corner_radius_pixels);
// Used during disconnect cleanup so thumbnail generation does not require a
// second per-frame CPU copy during normal playback.
bool CopyLatestNv12(std::string_view remote_id,
std::vector<unsigned char> *output, int *width,
int *height) const override;
private:
SubmitResult SubmitNv12Internal(std::string_view remote_id,
const uint8_t *data, size_t size, int width,
int height, bool replace_pending);
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PLATFORM_OPENGL_VIDEO_RENDERER_H_
@@ -1,42 +0,0 @@
#include "runtime/gui_runtime.h"
namespace crossdesk {
void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) {
props.remote_service_status_received_ = false;
props.remote_service_available_ = false;
props.remote_interactive_stage_.clear();
}
void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status) {
props.remote_service_status_received_ = true;
props.remote_service_available_ = status.available;
props.remote_interactive_stage_ = status.interactive_stage;
}
GuiRuntime::RemoteUnlockState GuiRuntime::GetRemoteUnlockState(
const RemoteSession& props) const {
if (!props.remote_service_status_received_) {
return RemoteUnlockState::none;
}
if (!props.remote_service_available_) {
return RemoteUnlockState::service_unavailable;
}
if (props.remote_interactive_stage_ == "credential-ui") {
return RemoteUnlockState::credential_ui;
}
if (props.remote_interactive_stage_ == "lock-screen") {
return RemoteUnlockState::lock_screen;
}
if (props.remote_interactive_stage_ == "secure-desktop") {
return RemoteUnlockState::secure_desktop;
}
return RemoteUnlockState::none;
}
#if !defined(_WIN32)
void GuiRuntime::HandleWindowsServiceIntegration() {}
#endif
} // namespace crossdesk
@@ -1,12 +0,0 @@
#include <memory>
#include "platform/common/gui/opengl_video_renderer.h"
#include "platform/video_renderer.h"
namespace crossdesk {
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
return std::make_unique<OpenGlVideoRenderer>();
}
} // namespace crossdesk
@@ -1,56 +0,0 @@
#include "platform/daemon_backend.h"
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <chrono>
#include <thread>
namespace crossdesk::platform {
volatile std::sig_atomic_t g_daemon_stop_requested = 0;
void ResetDaemonStop() { g_daemon_stop_requested = 0; }
void RequestDaemonStop() { g_daemon_stop_requested = 1; }
bool DaemonStopRequested() { return g_daemon_stop_requested != 0; }
DaemonChildResult RunDaemonChild(const std::string& executable_path,
const std::function<bool()>& keep_running) {
DaemonChildResult result;
const pid_t child = fork();
if (child == 0) {
execl(executable_path.c_str(), executable_path.c_str(), "--child",
nullptr);
_exit(1);
}
if (child < 0) return result;
result.started = true;
int status = 0;
pid_t waited = -1;
while (keep_running()) {
waited = waitpid(child, &status, WNOHANG);
if (waited == child || (waited < 0 && errno != EINTR)) break;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
if (!keep_running() && waited != child) {
kill(child, SIGTERM);
waited = waitpid(child, &status, 0);
}
if (waited < 0) return result;
if (WIFEXITED(status)) {
result.exit_code = WEXITSTATUS(status);
result.normal_exit = result.exit_code == 0;
} else if (WIFSIGNALED(status)) {
result.terminated_by_signal = true;
result.exit_code = WTERMSIG(status);
}
return result;
}
} // namespace crossdesk::platform
@@ -1,29 +0,0 @@
#ifndef CROSSDESK_PLATFORM_DAEMON_BACKEND_H_
#define CROSSDESK_PLATFORM_DAEMON_BACKEND_H_
#include <csignal>
#include <functional>
#include <string>
namespace crossdesk::platform {
extern volatile std::sig_atomic_t g_daemon_stop_requested;
struct DaemonChildResult {
bool started = false;
bool normal_exit = false;
bool terminated_by_signal = false;
int exit_code = 0;
};
void ResetDaemonStop();
void RequestDaemonStop();
bool DaemonStopRequested();
bool PrepareDaemon();
std::string GetDaemonExecutablePath();
DaemonChildResult RunDaemonChild(const std::string& executable_path,
const std::function<bool()>& keep_running);
} // namespace crossdesk::platform
#endif // CROSSDESK_PLATFORM_DAEMON_BACKEND_H_
@@ -1,65 +0,0 @@
#include "platform/autostart_backend.h"
#include <limits.h>
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
namespace crossdesk::platform {
namespace {
std::filesystem::path AutostartPath(const std::string& app_name) {
const char* home = std::getenv("HOME");
if (!home || !home[0]) return {};
return std::filesystem::path(home) / ".config" / "autostart" /
(app_name + ".desktop");
}
} // namespace
std::string GetAutostartExecutablePath() {
char path[PATH_MAX] = {};
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
return count > 0 ? std::string(path, static_cast<size_t>(count))
: std::string();
}
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path) {
const std::filesystem::path path = AutostartPath(app_name);
if (path.empty()) return false;
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
if (error) return false;
std::ofstream file(path);
if (!file.is_open()) return false;
file << "[Desktop Entry]\n"
<< "Type=Application\n"
<< "Exec=" << executable_path << "\n"
<< "Hidden=false\n"
<< "NoDisplay=false\n"
<< "X-GNOME-Autostart-enabled=true\n"
<< "Terminal=false\n"
<< "StartupNotify=false\n"
<< "Name=" << app_name << "\n";
file.close();
return file.good();
}
bool DisableAutostart(const std::string& app_name) {
const std::filesystem::path path = AutostartPath(app_name);
if (path.empty()) return false;
std::error_code error;
return std::filesystem::remove(path, error) && !error;
}
bool IsAutostartEnabled(const std::string& app_name) {
const std::filesystem::path path = AutostartPath(app_name);
return !path.empty() && std::filesystem::is_regular_file(path);
}
} // namespace crossdesk::platform
@@ -1,202 +0,0 @@
#include "clipboard.h"
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
#include <chrono>
#include <climits>
#include <string>
#include <thread>
#include "platform/clipboard_backend.h"
#include "rd_log.h"
namespace crossdesk {
std::string Clipboard::GetText() {
Display* display = XOpenDisplay(nullptr);
if (!display) {
LOG_ERROR("Clipboard::GetText: failed to open X display");
return {};
}
Window owner = XGetSelectionOwner(display, XA_PRIMARY);
if (owner == None) {
owner =
XGetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False));
if (owner == None) {
XCloseDisplay(display);
return {};
}
}
Atom selection = XA_PRIMARY;
Atom target = XInternAtom(display, "UTF8_STRING", False);
if (target == None) target = XA_STRING;
const Window window = XCreateSimpleWindow(
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
XSelectInput(display, window, PropertyChangeMask);
XConvertSelection(display, selection, target, XA_PRIMARY, window,
CurrentTime);
std::string result;
bool done = false;
while (!done) {
XEvent event = {};
XNextEvent(display, &event);
if (event.type != SelectionNotify) continue;
if (event.xselection.property == None) {
if (selection == XA_PRIMARY) {
selection = XInternAtom(display, "CLIPBOARD", False);
XConvertSelection(display, selection, target, XA_PRIMARY, window,
CurrentTime);
continue;
}
break;
}
Atom actual_type = None;
int actual_format = 0;
unsigned long item_count = 0;
unsigned long bytes_after = 0;
unsigned char* data = nullptr;
if (XGetWindowProperty(display, window, XA_PRIMARY, 0, LONG_MAX / 4,
False, AnyPropertyType, &actual_type,
&actual_format, &item_count, &bytes_after,
&data) == Success) {
if (data) {
result.assign(reinterpret_cast<char*>(data), item_count);
XFree(data);
}
done = true;
}
}
XDestroyWindow(display, window);
XCloseDisplay(display);
return result;
}
bool Clipboard::SetText(const std::string& text) {
Display* display = XOpenDisplay(nullptr);
if (!display) {
LOG_ERROR("Clipboard::SetText: failed to open X display");
return false;
}
const Window window = XCreateSimpleWindow(
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
const Atom utf8 = XInternAtom(display, "UTF8_STRING", False);
const Atom targets = XInternAtom(display, "TARGETS", False);
XSetSelectionOwner(display, clipboard, window, CurrentTime);
if (XGetSelectionOwner(display, clipboard) != window) {
XDestroyWindow(display, window);
XCloseDisplay(display);
return false;
}
XChangeProperty(display, window, XA_PRIMARY, utf8, 8, PropModeReplace,
reinterpret_cast<const unsigned char*>(text.data()),
static_cast<int>(text.size()));
while (true) {
XEvent event = {};
XNextEvent(display, &event);
if (event.type == SelectionClear) break;
if (event.type != SelectionRequest) continue;
const XSelectionRequestEvent& request = event.xselectionrequest;
XSelectionEvent response = {};
response.type = SelectionNotify;
response.display = request.display;
response.requestor = request.requestor;
response.selection = request.selection;
response.time = request.time;
response.target = request.target;
response.property = request.property;
if (request.target == targets) {
Atom supported[] = {utf8, XA_STRING, targets};
XChangeProperty(display, request.requestor, request.property, XA_ATOM,
32, PropModeReplace,
reinterpret_cast<unsigned char*>(supported), 3);
} else if (request.target == utf8 || request.target == XA_STRING) {
XChangeProperty(display, request.requestor, request.property,
request.target, 8, PropModeReplace,
reinterpret_cast<const unsigned char*>(text.data()),
static_cast<int>(text.size()));
} else {
response.property = None;
}
XSendEvent(display, request.requestor, False, 0,
reinterpret_cast<XEvent*>(&response));
XSync(display, False);
}
XDestroyWindow(display, window);
XCloseDisplay(display);
return true;
}
bool Clipboard::HasText() {
Display* display = XOpenDisplay(nullptr);
if (!display) return false;
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
Window owner = XGetSelectionOwner(display, clipboard);
if (owner == None) owner = XGetSelectionOwner(display, XA_PRIMARY);
XCloseDisplay(display);
return owner != None;
}
namespace platform {
void RunClipboardMonitor(int check_interval_ms) {
Display* display = XOpenDisplay(nullptr);
if (!display) {
LOG_ERROR("Failed to open X display for clipboard monitoring");
return;
}
int event_base = 0;
int error_base = 0;
if (!XFixesQueryExtension(display, &event_base, &error_base)) {
LOG_WARN("XFixes extension not available, falling back to polling");
XCloseDisplay(display);
while (ClipboardMonitoring()) {
std::this_thread::sleep_for(
std::chrono::milliseconds(check_interval_ms > 0 ? check_interval_ms
: 100));
if (ClipboardMonitoring()) HandleClipboardChange();
}
return;
}
const Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
const Window event_window = XCreateSimpleWindow(
display, DefaultRootWindow(display), 0, 0, 1, 1, 0, 0, 0);
XFixesSelectSelectionInput(display, event_window, clipboard,
XFixesSetSelectionOwnerNotifyMask |
XFixesSelectionWindowDestroyNotifyMask |
XFixesSelectionClientCloseNotifyMask);
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
while (ClipboardMonitoring()) {
while (ClipboardMonitoring() && XPending(display) > 0) {
XEvent event = {};
XNextEvent(display, &event);
if (event.type == event_base + XFixesSelectionNotify) {
HandleClipboardChange();
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
XFixesSelectSelectionInput(display, event_window, clipboard, 0);
XDestroyWindow(display, event_window);
XCloseDisplay(display);
}
void WakeClipboardMonitor() {}
} // namespace platform
} // namespace crossdesk
@@ -1,404 +0,0 @@
#include "linux_cursor_shape.h"
#include <remote_action.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xcursor/Xcursor.h>
#include <X11/Xlib.h>
#endif
namespace crossdesk {
namespace {
constexpr LinuxCursorAlias kCursorAliases[] = {
{"help", RemoteCursorShape::help},
{"question-arrow", RemoteCursorShape::help},
{"left-ptr-help", RemoteCursorShape::help},
{"whats-this", RemoteCursorShape::help},
{"dnd-ask", RemoteCursorShape::help},
{"pointer", RemoteCursorShape::pointer},
{"hand", RemoteCursorShape::pointer},
{"hand1", RemoteCursorShape::pointer},
{"hand2", RemoteCursorShape::pointer},
{"link", RemoteCursorShape::pointer},
{"pointing-hand", RemoteCursorShape::pointer},
{"progress", RemoteCursorShape::progress},
{"left-ptr-watch", RemoteCursorShape::progress},
{"half-busy", RemoteCursorShape::progress},
{"wait", RemoteCursorShape::wait},
{"watch", RemoteCursorShape::wait},
{"crosshair", RemoteCursorShape::crosshair},
{"cross", RemoteCursorShape::crosshair},
{"cross-reverse", RemoteCursorShape::crosshair},
{"tcross", RemoteCursorShape::crosshair},
{"cell", RemoteCursorShape::crosshair},
{"plus", RemoteCursorShape::crosshair},
{"target", RemoteCursorShape::crosshair},
{"dotbox", RemoteCursorShape::crosshair},
{"dot-box-mask", RemoteCursorShape::crosshair},
{"text", RemoteCursorShape::text},
{"xterm", RemoteCursorShape::text},
{"ibeam", RemoteCursorShape::text},
{"vertical-text", RemoteCursorShape::text},
{"alias", RemoteCursorShape::alias},
{"dnd-link", RemoteCursorShape::alias},
{"copy", RemoteCursorShape::copy},
{"dnd-copy", RemoteCursorShape::copy},
{"move", RemoteCursorShape::move},
{"fleur", RemoteCursorShape::move},
{"size-all", RemoteCursorShape::move},
{"all-scroll", RemoteCursorShape::move},
{"dnd-move", RemoteCursorShape::move},
{"no-drop", RemoteCursorShape::no_drop},
{"dnd-no-drop", RemoteCursorShape::no_drop},
{"dnd-none", RemoteCursorShape::no_drop},
{"not-allowed", RemoteCursorShape::not_allowed},
{"forbidden", RemoteCursorShape::not_allowed},
{"crossed-circle", RemoteCursorShape::not_allowed},
{"circle", RemoteCursorShape::not_allowed},
{"pirate", RemoteCursorShape::not_allowed},
{"grab", RemoteCursorShape::grab},
{"openhand", RemoteCursorShape::grab},
{"open-hand", RemoteCursorShape::grab},
{"grabbing", RemoteCursorShape::grabbing},
{"closedhand", RemoteCursorShape::grabbing},
{"closed-hand", RemoteCursorShape::grabbing},
{"col-resize", RemoteCursorShape::col_resize},
{"row-resize", RemoteCursorShape::row_resize},
{"n-resize", RemoteCursorShape::n_resize},
{"top-side", RemoteCursorShape::n_resize},
{"top-tee", RemoteCursorShape::n_resize},
{"sb-up-arrow", RemoteCursorShape::n_resize},
{"e-resize", RemoteCursorShape::e_resize},
{"right-side", RemoteCursorShape::e_resize},
{"right-tee", RemoteCursorShape::e_resize},
{"sb-right-arrow", RemoteCursorShape::e_resize},
{"s-resize", RemoteCursorShape::s_resize},
{"bottom-side", RemoteCursorShape::s_resize},
{"bottom-tee", RemoteCursorShape::s_resize},
{"sb-down-arrow", RemoteCursorShape::s_resize},
{"w-resize", RemoteCursorShape::w_resize},
{"left-side", RemoteCursorShape::w_resize},
{"left-tee", RemoteCursorShape::w_resize},
{"sb-left-arrow", RemoteCursorShape::w_resize},
{"ne-resize", RemoteCursorShape::ne_resize},
{"top-right-corner", RemoteCursorShape::ne_resize},
{"ur-angle", RemoteCursorShape::ne_resize},
{"nw-resize", RemoteCursorShape::nw_resize},
{"top-left-corner", RemoteCursorShape::nw_resize},
{"ul-angle", RemoteCursorShape::nw_resize},
{"se-resize", RemoteCursorShape::se_resize},
{"bottom-right-corner", RemoteCursorShape::se_resize},
{"lr-angle", RemoteCursorShape::se_resize},
{"sw-resize", RemoteCursorShape::sw_resize},
{"bottom-left-corner", RemoteCursorShape::sw_resize},
{"ll-angle", RemoteCursorShape::sw_resize},
{"ew-resize", RemoteCursorShape::ew_resize},
{"sb-h-double-arrow", RemoteCursorShape::ew_resize},
{"h-double-arrow", RemoteCursorShape::ew_resize},
{"size-hor", RemoteCursorShape::ew_resize},
{"split-h", RemoteCursorShape::ew_resize},
{"exchange", RemoteCursorShape::ew_resize},
{"ns-resize", RemoteCursorShape::ns_resize},
{"sb-v-double-arrow", RemoteCursorShape::ns_resize},
{"v-double-arrow", RemoteCursorShape::ns_resize},
{"double-arrow", RemoteCursorShape::ns_resize},
{"size-ver", RemoteCursorShape::ns_resize},
{"split-v", RemoteCursorShape::ns_resize},
{"nesw-resize", RemoteCursorShape::nesw_resize},
{"fd-double-arrow", RemoteCursorShape::nesw_resize},
{"size-bdiag", RemoteCursorShape::nesw_resize},
{"nwse-resize", RemoteCursorShape::nwse_resize},
{"bd-double-arrow", RemoteCursorShape::nwse_resize},
{"size-fdiag", RemoteCursorShape::nwse_resize},
{"default", RemoteCursorShape::default_cursor},
{"left-ptr", RemoteCursorShape::default_cursor},
{"left-arrow", RemoteCursorShape::default_cursor},
{"arrow", RemoteCursorShape::default_cursor},
{"top-left-arrow", RemoteCursorShape::default_cursor},
{"right-ptr", RemoteCursorShape::default_cursor},
{"center-ptr", RemoteCursorShape::default_cursor},
{"context-menu", RemoteCursorShape::default_cursor},
};
std::string NormalizeCursorName(const std::string& raw_name) {
std::string normalized;
normalized.reserve(raw_name.size());
for (unsigned char ch : raw_name) {
if (ch == '_' || std::isspace(ch)) {
normalized.push_back('-');
} else {
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
}
return normalized;
}
#if defined(__linux__) && !defined(__APPLE__)
struct CursorCandidate {
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
std::vector<uint32_t> argb;
};
uint8_t Alpha(uint32_t argb) {
return static_cast<uint8_t>((argb >> 24U) & 0xffU);
}
double BilinearAlpha(const CursorCandidate& candidate, double x, double y) {
if (candidate.width == 0 || candidate.height == 0) {
return 0.0;
}
x = std::max(0.0, std::min(x, static_cast<double>(candidate.width - 1)));
y = std::max(0.0, std::min(y, static_cast<double>(candidate.height - 1)));
const uint32_t x0 = static_cast<uint32_t>(std::floor(x));
const uint32_t y0 = static_cast<uint32_t>(std::floor(y));
const uint32_t x1 = std::min(x0 + 1, candidate.width - 1);
const uint32_t y1 = std::min(y0 + 1, candidate.height - 1);
const double fx = x - x0;
const double fy = y - y0;
const auto sample = [&](uint32_t px, uint32_t py) {
return static_cast<double>(
Alpha(candidate.argb[static_cast<size_t>(py) * candidate.width + px]));
};
const double top = sample(x0, y0) * (1.0 - fx) + sample(x1, y0) * fx;
const double bottom = sample(x0, y1) * (1.0 - fx) + sample(x1, y1) * fx;
return top * (1.0 - fy) + bottom * fy;
}
double SimilarityError(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (!image.argb || image.width == 0 || image.height == 0 ||
candidate.argb.empty()) {
return std::numeric_limits<double>::infinity();
}
double alpha_error = 0.0;
for (uint32_t y = 0; y < image.height; ++y) {
const double source_y =
(static_cast<double>(y) + 0.5) * candidate.height / image.height - 0.5;
for (uint32_t x = 0; x < image.width; ++x) {
const double source_x = (static_cast<double>(x) + 0.5) *
candidate.width / image.width -
0.5;
const double expected = BilinearAlpha(candidate, source_x, source_y);
const double actual = static_cast<double>(
Alpha(image.argb[static_cast<size_t>(y) * image.width + x]));
alpha_error += std::abs(actual - expected) / 255.0;
}
}
alpha_error /= static_cast<double>(image.width) * image.height;
const double observed_hot_x =
static_cast<double>(image.xhot) / std::max(1u, image.width);
const double observed_hot_y =
static_cast<double>(image.yhot) / std::max(1u, image.height);
const double candidate_hot_x =
static_cast<double>(candidate.xhot) / std::max(1u, candidate.width);
const double candidate_hot_y =
static_cast<double>(candidate.yhot) / std::max(1u, candidate.height);
const double hotspot_error =
std::abs(observed_hot_x - candidate_hot_x) +
std::abs(observed_hot_y - candidate_hot_y);
const double observed_aspect =
static_cast<double>(image.width) / image.height;
const double candidate_aspect =
static_cast<double>(candidate.width) / candidate.height;
const double aspect_error =
std::abs(std::log(observed_aspect / candidate_aspect));
return alpha_error + hotspot_error * 0.12 + aspect_error * 0.08;
}
bool SameImage(const LinuxCursorImageView& image,
const CursorCandidate& candidate) {
if (image.width != candidate.width || image.height != candidate.height ||
image.xhot != candidate.xhot || image.yhot != candidate.yhot) {
return false;
}
const size_t pixel_count =
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
return std::equal(image.argb, image.argb + pixel_count,
candidate.argb.begin());
}
#endif
} // namespace
const LinuxCursorAlias* LinuxCursorAliases(size_t* count) {
if (count) {
*count = sizeof(kCursorAliases) / sizeof(kCursorAliases[0]);
}
return kCursorAliases;
}
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& raw_name) {
const std::string name = NormalizeCursorName(raw_name);
for (const auto& alias : kCursorAliases) {
if (name == alias.name) {
return alias.shape;
}
}
// Some toolkits prefix a theme or toolkit name to the standard cursor name.
// Restrict the fallback to separators so names such as "pointer-progress"
// still prefer their exact semantic alias above.
for (const auto& alias : kCursorAliases) {
const std::string suffix = std::string("-") + alias.name;
if (name.size() > suffix.size() &&
name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0) {
return alias.shape;
}
}
return RemoteCursorShape::default_cursor;
}
#if defined(__linux__) && !defined(__APPLE__)
struct LinuxCursorThemeMatcher::Impl {
Display* display = XOpenDisplay(nullptr);
std::string theme;
int default_size = 24;
std::unordered_map<int, std::vector<CursorCandidate>> candidates_by_size;
Impl() {
if (!display) {
return;
}
if (const char* active_theme = XcursorGetTheme(display)) {
theme = active_theme;
}
const int active_size = XcursorGetDefaultSize(display);
if (active_size > 0) {
default_size = active_size;
}
}
~Impl() {
if (display) {
XCloseDisplay(display);
}
}
const std::vector<CursorCandidate>& Candidates(int requested_size) {
requested_size = std::max(1, requested_size);
auto existing = candidates_by_size.find(requested_size);
if (existing != candidates_by_size.end()) {
return existing->second;
}
std::vector<CursorCandidate> candidates;
const char* theme_name = theme.empty() ? nullptr : theme.c_str();
for (const auto& alias : kCursorAliases) {
XcursorImages* images =
XcursorLibraryLoadImages(alias.name, theme_name, requested_size);
if (!images) {
continue;
}
for (int index = 0; index < images->nimage; ++index) {
const XcursorImage* image = images->images[index];
if (!image || !image->pixels || image->width == 0 ||
image->height == 0) {
continue;
}
CursorCandidate candidate;
candidate.shape = alias.shape;
candidate.width = image->width;
candidate.height = image->height;
candidate.xhot = image->xhot;
candidate.yhot = image->yhot;
const size_t pixel_count = static_cast<size_t>(image->width) *
static_cast<size_t>(image->height);
candidate.argb.assign(image->pixels, image->pixels + pixel_count);
const bool duplicate =
std::any_of(candidates.begin(), candidates.end(),
[&](const CursorCandidate& current) {
return current.shape == candidate.shape &&
current.width == candidate.width &&
current.height == candidate.height &&
current.xhot == candidate.xhot &&
current.yhot == candidate.yhot &&
current.argb == candidate.argb;
});
if (!duplicate) {
candidates.push_back(std::move(candidate));
}
}
XcursorImagesDestroy(images);
}
return candidates_by_size
.emplace(requested_size, std::move(candidates))
.first->second;
}
};
#else
struct LinuxCursorThemeMatcher::Impl {};
#endif
LinuxCursorThemeMatcher::LinuxCursorThemeMatcher()
: impl_(std::make_unique<Impl>()) {}
LinuxCursorThemeMatcher::~LinuxCursorThemeMatcher() = default;
RemoteCursorShape LinuxCursorThemeMatcher::Match(
const LinuxCursorImageView& image) {
#if defined(__linux__) && !defined(__APPLE__)
if (!impl_ || !image.argb || image.width == 0 || image.height == 0) {
return RemoteCursorShape::default_cursor;
}
const int requested_size =
static_cast<int>(std::max(image.width, image.height));
const auto& candidates = impl_->Candidates(requested_size);
for (const auto& candidate : candidates) {
if (SameImage(image, candidate)) {
return candidate.shape;
}
}
double best_error = std::numeric_limits<double>::infinity();
RemoteCursorShape best_shape = RemoteCursorShape::default_cursor;
for (const auto& candidate : candidates) {
const double error = SimilarityError(image, candidate);
if (error < best_error) {
best_error = error;
best_shape = candidate.shape;
}
}
// Scaled theme cursors normally remain well below this threshold, while an
// unrelated custom application cursor should fall back to the default.
return best_error <= 0.20 ? best_shape
: RemoteCursorShape::default_cursor;
#else
(void)image;
return RemoteCursorShape::default_cursor;
#endif
}
} // namespace crossdesk
@@ -1,49 +0,0 @@
#ifndef CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#define CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <remote_cursor_shape.h>
namespace crossdesk {
struct LinuxCursorAlias {
const char* name;
RemoteCursorShape shape;
};
const LinuxCursorAlias* LinuxCursorAliases(size_t* count);
RemoteCursorShape ShapeFromLinuxCursorName(const std::string& name);
struct LinuxCursorImageView {
uint32_t width = 0;
uint32_t height = 0;
uint32_t xhot = 0;
uint32_t yhot = 0;
const uint32_t* argb = nullptr;
};
// Matches a compositor-provided cursor bitmap against the active Xcursor
// theme. This is needed on Wayland, where XFixes only observes XWayland
// cursors and cannot report cursors owned by native Wayland surfaces.
class LinuxCursorThemeMatcher {
public:
LinuxCursorThemeMatcher();
~LinuxCursorThemeMatcher();
LinuxCursorThemeMatcher(const LinuxCursorThemeMatcher&) = delete;
LinuxCursorThemeMatcher& operator=(const LinuxCursorThemeMatcher&) = delete;
RemoteCursorShape Match(const LinuxCursorImageView& image);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_COMMON_LINUX_CURSOR_SHAPE_H_
@@ -1,58 +0,0 @@
#include "platform/daemon_backend.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
namespace crossdesk::platform {
bool PrepareDaemon() {
const bool from_terminal =
isatty(STDIN_FILENO) != 0 || isatty(STDOUT_FILENO) != 0;
pid_t process = fork();
if (process < 0) return false;
if (process > 0) _exit(0);
if (setsid() < 0) return false;
process = fork();
if (process < 0) return false;
if (process > 0) _exit(0);
umask(0);
if (chdir("/") != 0) {
std::cerr << "Failed to change daemon working directory to /: "
<< std::strerror(errno) << std::endl;
return false;
}
const int null_fd = open("/dev/null", O_RDWR);
if (null_fd >= 0) {
dup2(null_fd, STDIN_FILENO);
if (!from_terminal) {
dup2(null_fd, STDOUT_FILENO);
dup2(null_fd, STDERR_FILENO);
}
if (null_fd > STDERR_FILENO) close(null_fd);
}
signal(SIGTERM, [](int) { g_daemon_stop_requested = 1; });
signal(SIGINT, [](int) { g_daemon_stop_requested = 1; });
signal(SIGPIPE, SIG_IGN);
return true;
}
std::string GetDaemonExecutablePath() {
char path[PATH_MAX] = {};
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
return count > 0 ? std::string(path, static_cast<size_t>(count))
: std::string();
}
} // namespace crossdesk::platform
@@ -1,53 +0,0 @@
#include "platform/linux/gui/cuda_graphics.h"
#include <cstdlib>
#include <initializer_list>
#include "rd_log.h"
#include "platform/common/gui/cuda_opengl_interop.h"
namespace crossdesk {
void ConfigureLinuxCudaGraphics(bool hardware_acceleration) {
#if USE_CUDA && (defined(__x86_64__) || defined(__amd64__))
if (!hardware_acceleration) {
return;
}
// Respect explicit GPU/driver selection, including a request to use Mesa
// or software rendering. Native frames can still use the CPU fallback.
for (const char* name : {"__NV_PRIME_RENDER_OFFLOAD",
"__NV_PRIME_RENDER_OFFLOAD_PROVIDER",
"__GLX_VENDOR_LIBRARY_NAME",
"__EGL_VENDOR_LIBRARY_FILENAMES",
"__EGL_VENDOR_LIBRARY_DIRS", "DRI_PRIME",
"LIBGL_ALWAYS_SOFTWARE",
"MESA_LOADER_DRIVER_OVERRIDE"}) {
const char* value = std::getenv(name);
if (value && value[0] != '\0') {
LOG_INFO("Keeping explicit Linux graphics selection: {}={}", name,
value);
return;
}
}
if (!IsCudaDeviceAvailable()) {
return;
}
// Both EGL and GLX need to render on NVIDIA to share CUDA frame buffers.
// These are process-local, not desktop settings.
// https://download.nvidia.com/XFree86/Linux-x86_64/595.45.04/README/primerenderoffload.html
if (setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 1) != 0 ||
setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 1) != 0) {
LOG_WARN("Unable to select NVIDIA PRIME rendering for CUDA video");
return;
}
LOG_INFO("Selected NVIDIA PRIME rendering for CUDA/OpenGL video interop");
#else
(void)hardware_acceleration;
#endif
}
} // namespace crossdesk
@@ -1,18 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CUDA_GRAPHICS_H_
#define _CUDA_GRAPHICS_H_
namespace crossdesk {
// Call before SDL or Slint initializes EGL/GLX. Hybrid laptops otherwise
// create the GUI context on the integrated GPU while NVDEC uses NVIDIA.
void ConfigureLinuxCudaGraphics(bool hardware_acceleration);
} // namespace crossdesk
#endif
@@ -1,19 +0,0 @@
#include "device_controller_factory.h"
#include "keyboard_capturer.h"
#include "mouse_controller.h"
namespace crossdesk {
DeviceController* DeviceControllerFactory::Create(Device device) {
switch (device) {
case Mouse:
return new PlatformMouseController();
case Keyboard:
return new PlatformKeyboardCapturer();
default:
return nullptr;
}
}
} // namespace crossdesk
@@ -1,51 +0,0 @@
#include "platform/path_backend.h"
#include <limits.h>
#include <unistd.h>
#include <cstdlib>
#include <vector>
namespace crossdesk::platform {
namespace {
std::filesystem::path Home() {
const char* value = std::getenv("HOME");
return value ? std::filesystem::path(value) : std::filesystem::path();
}
std::filesystem::path EnvOrDefault(const char* name,
const std::filesystem::path& fallback) {
const char* value = std::getenv(name);
return value ? std::filesystem::path(value) : fallback;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
std::vector<char> buffer(PATH_MAX);
while (true) {
const ssize_t length =
readlink("/proc/self/exe", buffer.data(), buffer.size() - 1);
if (length <= 0) return {};
if (static_cast<size_t>(length) < buffer.size() - 1) {
buffer[static_cast<size_t>(length)] = '\0';
return std::filesystem::path(buffer.data()).parent_path();
}
buffer.resize(buffer.size() * 2);
}
}
std::filesystem::path GetConfigPath(const std::string& app_name) {
return EnvOrDefault("XDG_CONFIG_HOME", Home() / ".config") / app_name;
}
std::filesystem::path GetCachePath(const std::string& app_name) {
return EnvOrDefault("XDG_CACHE_HOME", Home() / ".cache") / app_name;
}
std::filesystem::path GetLogPath(const std::string& app_name) {
return GetCachePath(app_name) / "logs";
}
} // namespace crossdesk::platform
@@ -1,11 +0,0 @@
#include "screen_capturer_factory.h"
#include "screen_capturer_linux.h"
namespace crossdesk {
ScreenCapturer* ScreenCapturerFactory::Create() {
return new ScreenCapturerLinux();
}
} // namespace crossdesk
@@ -1,11 +0,0 @@
#include "speaker_capturer_factory.h"
#include "speaker_capturer_linux.h"
namespace crossdesk {
SpeakerCapturer* SpeakerCapturerFactory::Create() {
return new SpeakerCapturerLinux();
}
} // namespace crossdesk
@@ -1,81 +0,0 @@
#include "platform.h"
#include <fcntl.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "rd_log.h"
namespace crossdesk {
std::string GetMac() {
const int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) return {};
char buffer[1024] = {};
struct ifconf config = {};
config.ifc_len = sizeof(buffer);
config.ifc_buf = buffer;
if (ioctl(socket_fd, SIOCGIFCONF, &config) < 0) {
close(socket_fd);
return {};
}
std::string result;
struct ifreq request = {};
struct ifreq* cursor = config.ifc_req;
const struct ifreq* end = cursor + config.ifc_len / sizeof(struct ifreq);
for (; cursor != end; ++cursor) {
std::strncpy(request.ifr_name, cursor->ifr_name,
sizeof(request.ifr_name) - 1);
if (ioctl(socket_fd, SIOCGIFFLAGS, &request) < 0 ||
(request.ifr_flags & IFF_LOOPBACK) ||
ioctl(socket_fd, SIOCGIFHWADDR, &request) < 0) {
continue;
}
char address[16] = {};
int length = 0;
for (int index = 0; index < 6; ++index) {
length += std::snprintf(
address + length, sizeof(address) - length, "%.2X",
static_cast<unsigned char>(request.ifr_hwaddr.sa_data[index]));
}
result = address;
break;
}
close(socket_fd);
return result;
}
std::string GetHostName() {
char hostname[256] = {};
if (gethostname(hostname, sizeof(hostname)) == -1) {
LOG_ERROR("gethostname failed");
return {};
}
return hostname;
}
bool IsWaylandSession() {
const char* session_type = std::getenv("XDG_SESSION_TYPE");
if (session_type) {
if (std::strcmp(session_type, "wayland") == 0 ||
std::strcmp(session_type, "Wayland") == 0) {
return true;
}
if (std::strcmp(session_type, "x11") == 0 ||
std::strcmp(session_type, "X11") == 0) {
return false;
}
}
const char* display = std::getenv("WAYLAND_DISPLAY");
return display && display[0] != '\0';
}
} // namespace crossdesk
@@ -1,74 +0,0 @@
#include "platform/autostart_backend.h"
#include <limits.h>
#include <mach-o/dyld.h>
#include <cstdlib>
#include <filesystem>
#include <fstream>
namespace crossdesk::platform {
namespace {
std::filesystem::path AutostartPath(const std::string& app_name) {
const char* home = std::getenv("HOME");
if (!home || !home[0]) return {};
return std::filesystem::path(home) / "Library" / "LaunchAgents" /
(app_name + ".plist");
}
} // namespace
std::string GetAutostartExecutablePath() {
char path[PATH_MAX] = {};
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) != 0) return {};
char resolved[PATH_MAX] = {};
return realpath(path, resolved) ? std::string(resolved) : std::string(path);
}
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path) {
const std::filesystem::path path = AutostartPath(app_name);
if (path.empty()) return false;
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
if (error) return false;
std::ofstream file(path);
if (!file.is_open()) return false;
file << R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>)"
<< app_name << R"(</string>
<key>ProgramArguments</key>
<array>
<string>)"
<< executable_path << R"(</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>)";
file.close();
return file.good();
}
bool DisableAutostart(const std::string& app_name) {
const std::filesystem::path path = AutostartPath(app_name);
if (path.empty()) return false;
std::error_code error;
return std::filesystem::remove(path, error) && !error;
}
bool IsAutostartEnabled(const std::string& app_name) {
const std::filesystem::path path = AutostartPath(app_name);
return !path.empty() && std::filesystem::is_regular_file(path);
}
} // namespace crossdesk::platform
@@ -1,20 +0,0 @@
#include "platform/daemon_backend.h"
#include <limits.h>
#include <mach-o/dyld.h>
#include <cstdlib>
namespace crossdesk::platform {
bool PrepareDaemon() { return true; }
std::string GetDaemonExecutablePath() {
char path[PATH_MAX] = {};
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) != 0) return {};
char resolved[PATH_MAX] = {};
return realpath(path, resolved) ? std::string(resolved) : std::string(path);
}
} // namespace crossdesk::platform
@@ -1,87 +0,0 @@
#ifndef CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
#define CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "platform/video_renderer.h"
namespace crossdesk {
// macOS video compositor used by CrossDesk only. VideoToolbox CVPixelBuffers
// are sampled directly through CVMetalTextureCache; packed CPU NV12 remains a
// fallback for software decoding and cached snapshots. AppKit attachment and
// rendering are main-thread operations, while frame submission is safe from
// MiniRTC's decode callback thread.
class MacMetalVideoRenderer final : public VideoRenderer {
public:
MacMetalVideoRenderer();
~MacMetalVideoRenderer() override;
MacMetalVideoRenderer(const MacMetalVideoRenderer&) = delete;
MacMetalVideoRenderer& operator=(const MacMetalVideoRenderer&) = delete;
bool IsReady() const override;
bool IsActive() const override;
// The selected stream is the only stream uploaded. Frames for background
// tabs are intentionally dropped to avoid wasting memory bandwidth.
bool SetSelectedStream(std::string remote_id) override;
void DiscardStream(std::string_view remote_id) override;
// Copies one tightly packed NV12 frame into a free shared Metal slot.
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
size_t size, int width, int height) override;
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
// command buffer using it has completed.
SubmitResult SubmitNativeFrame(std::string_view remote_id,
const MiniRtcNativeVideoFrame& frame) override;
// Seeds a newly selected stream from its retained CPU snapshot without
// replacing a newer decoded frame that is already queued or rendering.
SubmitResult SubmitCachedNv12(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height) override;
// Attaches a native CAMetalLayer-backed sibling below Slint's NSView and
// restores the containing AppKit window's ordinary opaque titlebar.
// |slint_view| is an NSView* kept opaque so this header remains valid C++.
bool Attach(void* slint_view);
void Detach();
bool IsAttached() const;
// Cheap main-thread geometry check used by the UI frame timer. New decoded
// frames are tracked by GuiRuntime's dirty flag; this covers native resize,
// scale, stream selection, and retry work without a full session sync.
bool NeedsSurfaceRedraw() const;
// Draws the newest queued frame for |remote_id|. top_inset_points reserves
// Slint's tab strip above the native video surface. A rendered outcome
// carries metadata from the exact slot submitted to the command buffer.
RenderOutcome RenderLatest(std::string_view remote_id,
double top_inset_points,
bool native_titlebar_visible);
// Used only when a session closes, so thumbnail generation does not require
// a second per-frame CPU copy during normal playback.
bool CopyLatestNv12(std::string_view remote_id,
std::vector<unsigned char>* output, int* width,
int* height) const override;
private:
SubmitResult SubmitNv12Internal(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height, bool replace_pending);
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PLATFORM_METAL_VIDEO_RENDERER_H_
File diff suppressed because it is too large Load Diff
@@ -1,193 +0,0 @@
#include "runtime/cursor_state_provider.h"
#include <remote_action.h>
#if defined(__APPLE__)
#import <AppKit/AppKit.h>
#import <CoreGraphics/CGRemoteOperation.h>
#include <cstdint>
#include <vector>
#include "runtime/cursor_position.h"
namespace crossdesk {
namespace {
// Quartz reports the arrow cursor's event hotspot. The visible apex in the
// current macOS system artwork is about 1.6 logical points above that hotspot
// (NSCursor.arrowCursor has a (5, 5) hotspot). Send this as presentation-only
// metadata so controllers can align their glyph without changing input.
constexpr double kDefaultArrowVisualTipYOffset = -1.6;
struct CursorFingerprint {
uint64_t pixel_hash = 0;
size_t width = 0;
size_t height = 0;
size_t bytes_per_row = 0;
CFIndex pixel_bytes = 0;
NSPoint hot_spot = NSZeroPoint;
};
struct KnownCursor {
CursorFingerprint fingerprint;
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
};
bool FingerprintCursor(NSCursor* cursor, CursorFingerprint* fingerprint) {
if (!cursor || !fingerprint || !cursor.image) return false;
NSRect proposed_rect = {NSZeroPoint, cursor.image.size};
CGImageRef image =
[cursor.image CGImageForProposedRect:&proposed_rect
context:nil
hints:nil];
if (!image) return false;
CGDataProviderRef provider = CGImageGetDataProvider(image);
if (!provider) return false;
CFDataRef data = CGDataProviderCopyData(provider);
if (!data) return false;
// NSCursor.currentSystemCursor returns a fresh NSCursor/NSImage wrapper on
// each query. Hash the decoded CGImage pixels so it can still be matched to
// AppKit's semantic cursor instances without encoding a large TIFF every
// frame.
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ULL;
constexpr uint64_t kFnvPrime = 1099511628211ULL;
uint64_t hash = kFnvOffsetBasis;
const UInt8* bytes = CFDataGetBytePtr(data);
const CFIndex length = CFDataGetLength(data);
for (CFIndex index = 0; index < length; ++index) {
hash ^= bytes[index];
hash *= kFnvPrime;
}
fingerprint->pixel_hash = hash;
fingerprint->width = CGImageGetWidth(image);
fingerprint->height = CGImageGetHeight(image);
fingerprint->bytes_per_row = CGImageGetBytesPerRow(image);
fingerprint->pixel_bytes = length;
fingerprint->hot_spot = cursor.hotSpot;
CFRelease(data);
return true;
}
bool SameCursor(const CursorFingerprint& left,
const CursorFingerprint& right) {
return left.pixel_hash == right.pixel_hash && left.width == right.width &&
left.height == right.height &&
left.bytes_per_row == right.bytes_per_row &&
left.pixel_bytes == right.pixel_bytes &&
NSEqualPoints(left.hot_spot, right.hot_spot);
}
void AddKnownCursor(std::vector<KnownCursor>* cursors, NSCursor* cursor,
RemoteCursorShape shape) {
CursorFingerprint fingerprint;
if (FingerprintCursor(cursor, &fingerprint)) {
cursors->push_back({fingerprint, shape});
}
}
std::vector<KnownCursor> BuildKnownCursors() {
std::vector<KnownCursor> cursors;
cursors.reserve(16);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
AddKnownCursor(&cursors, NSCursor.arrowCursor,
RemoteCursorShape::default_cursor);
AddKnownCursor(&cursors, NSCursor.pointingHandCursor,
RemoteCursorShape::pointer);
AddKnownCursor(&cursors, NSCursor.crosshairCursor,
RemoteCursorShape::crosshair);
AddKnownCursor(&cursors, NSCursor.IBeamCursor, RemoteCursorShape::text);
AddKnownCursor(&cursors, NSCursor.IBeamCursorForVerticalLayout,
RemoteCursorShape::text);
AddKnownCursor(&cursors, NSCursor.operationNotAllowedCursor,
RemoteCursorShape::not_allowed);
AddKnownCursor(&cursors, NSCursor.dragLinkCursor, RemoteCursorShape::alias);
AddKnownCursor(&cursors, NSCursor.dragCopyCursor, RemoteCursorShape::copy);
AddKnownCursor(&cursors, NSCursor.openHandCursor, RemoteCursorShape::grab);
AddKnownCursor(&cursors, NSCursor.closedHandCursor,
RemoteCursorShape::grabbing);
AddKnownCursor(&cursors, NSCursor.resizeLeftRightCursor,
RemoteCursorShape::ew_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpDownCursor,
RemoteCursorShape::ns_resize);
AddKnownCursor(&cursors, NSCursor.resizeUpCursor,
RemoteCursorShape::n_resize);
AddKnownCursor(&cursors, NSCursor.resizeRightCursor,
RemoteCursorShape::e_resize);
AddKnownCursor(&cursors, NSCursor.resizeDownCursor,
RemoteCursorShape::s_resize);
AddKnownCursor(&cursors, NSCursor.resizeLeftCursor,
RemoteCursorShape::w_resize);
#pragma clang diagnostic pop
return cursors;
}
RemoteCursorShape ShapeFromMacCursor(NSCursor* cursor) {
CursorFingerprint fingerprint;
if (!FingerprintCursor(cursor, &fingerprint)) {
return RemoteCursorShape::default_cursor;
}
// Sampling begins from the UI tick, after AppKit has initialized its cursor
// catalog. Cache those fingerprints because only the current cursor needs
// to be decoded on subsequent frames.
static const std::vector<KnownCursor> known_cursors = BuildKnownCursors();
for (const KnownCursor& known : known_cursors) {
if (SameCursor(fingerprint, known.fingerprint)) return known.shape;
}
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;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
NSCursor* cursor = NSCursor.currentSystemCursor;
const bool visible = CGCursorIsVisible();
#pragma clang diagnostic pop
state->seq = 0;
state->visible = visible && cursor != nil;
state->shape = state->visible ? ShapeFromMacCursor(cursor)
: RemoteCursorShape::none;
ResetCursorPosition(state);
CGEventRef event = CGEventCreate(nullptr);
if (event) {
const CGPoint location = CGEventGetLocation(event);
NormalizeCursorPosition(location.x, location.y, displays,
preferred_display, state);
if (state->position_valid && state->visible &&
state->shape == RemoteCursorShape::default_cursor &&
state->display_id >= 0 &&
state->display_id < static_cast<int>(displays.size())) {
const double display_height =
std::max(displays[state->display_id].height, 1);
state->visual_offset_y = static_cast<float>(
kDefaultArrowVisualTipYOffset / display_height);
}
CFRelease(event);
}
return true;
}
} // namespace crossdesk
#endif
@@ -1,13 +0,0 @@
#include "platform/video_renderer.h"
#include <memory>
#include "platform/macos/gui/metal_video_renderer.h"
namespace crossdesk {
std::unique_ptr<VideoRenderer> CreateVideoRenderer() {
return std::make_unique<MacMetalVideoRenderer>();
}
} // namespace crossdesk
@@ -1,19 +0,0 @@
#include "device_controller_factory.h"
#include "keyboard_capturer.h"
#include "mouse_controller.h"
namespace crossdesk {
DeviceController* DeviceControllerFactory::Create(Device device) {
switch (device) {
case Mouse:
return new PlatformMouseController();
case Keyboard:
return new PlatformKeyboardCapturer();
default:
return nullptr;
}
}
} // namespace crossdesk
@@ -1,48 +0,0 @@
#include "platform/path_backend.h"
#include <mach-o/dyld.h>
#include <cstdlib>
#include <vector>
namespace crossdesk::platform {
namespace {
std::filesystem::path Home() {
const char* value = std::getenv("HOME");
return value ? std::filesystem::path(value) : std::filesystem::path();
}
std::filesystem::path EnvOrDefault(const char* name,
const std::filesystem::path& fallback) {
const char* value = std::getenv(name);
return value ? std::filesystem::path(value) : fallback;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
uint32_t size = 0;
_NSGetExecutablePath(nullptr, &size);
std::vector<char> buffer(size + 1);
if (_NSGetExecutablePath(buffer.data(), &size) != 0) return {};
std::error_code error;
std::filesystem::path executable =
std::filesystem::weakly_canonical(buffer.data(), error);
if (error) executable = buffer.data();
return executable.parent_path();
}
std::filesystem::path GetConfigPath(const std::string& app_name) {
return EnvOrDefault("XDG_CONFIG_HOME", Home() / ".config") / app_name;
}
std::filesystem::path GetCachePath(const std::string& app_name) {
return EnvOrDefault("XDG_CACHE_HOME", Home() / ".cache") / app_name;
}
std::filesystem::path GetLogPath(const std::string& app_name) {
return Home() / "Library" / "Logs" / app_name;
}
} // namespace crossdesk::platform
@@ -1,11 +0,0 @@
#include "screen_capturer_factory.h"
#include "screen_capturer_sck.h"
namespace crossdesk {
ScreenCapturer* ScreenCapturerFactory::Create() {
return new ScreenCapturerSck();
}
} // namespace crossdesk
@@ -1,11 +0,0 @@
#include "speaker_capturer_factory.h"
#include "speaker_capturer_macosx.h"
namespace crossdesk {
SpeakerCapturer* SpeakerCapturerFactory::Create() {
return new SpeakerCapturerMacosx();
}
} // namespace crossdesk
@@ -1,67 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SYSTEM_INFO_H_
#define _SYSTEM_INFO_H_
#include "platform.h"
#include <ifaddrs.h>
#include <net/if_dl.h>
#include <net/if_types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include "rd_log.h"
namespace crossdesk {
std::string GetMac() {
struct ifaddrs* addresses = nullptr;
if (getifaddrs(&addresses) != 0) return {};
std::string result;
for (struct ifaddrs* cursor = addresses; cursor; cursor = cursor->ifa_next) {
if (!cursor->ifa_addr || cursor->ifa_addr->sa_family != AF_LINK ||
std::strcmp(cursor->ifa_name, "en0") != 0) {
continue;
}
const auto* link =
reinterpret_cast<const struct sockaddr_dl*>(cursor->ifa_addr);
if (link->sdl_type != IFT_ETHER) continue;
const auto* bytes = reinterpret_cast<const unsigned char*>(
&link->sdl_data[link->sdl_nlen]);
char address[16] = {};
int length = 0;
for (int index = 0; index < link->sdl_alen; ++index) {
length += std::snprintf(address + length, sizeof(address) - length,
"%.2X", bytes[index]);
}
result = address;
break;
}
freeifaddrs(addresses);
return result;
}
std::string GetHostName() {
char hostname[256] = {};
if (gethostname(hostname, sizeof(hostname)) == -1) {
LOG_ERROR("gethostname failed");
return {};
}
return hostname;
}
bool IsWaylandSession() { return false; }
} // namespace crossdesk
#endif
-16
View File
@@ -1,16 +0,0 @@
#ifndef CROSSDESK_PLATFORM_PATH_BACKEND_H_
#define CROSSDESK_PLATFORM_PATH_BACKEND_H_
#include <filesystem>
#include <string>
namespace crossdesk::platform {
std::filesystem::path GetExecutableDirectory();
std::filesystem::path GetConfigPath(const std::string& app_name);
std::filesystem::path GetCachePath(const std::string& app_name);
std::filesystem::path GetLogPath(const std::string& app_name);
} // namespace crossdesk::platform
#endif // CROSSDESK_PLATFORM_PATH_BACKEND_H_
@@ -1,78 +0,0 @@
#ifndef CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
#define CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "minirtc.h"
namespace crossdesk {
// Platform-independent contract for the native stream video renderer.
//
// Frame submission and snapshot methods may be called from MiniRTC callback
// threads. Surface setup, rendering and teardown remain backend-specific and
// are driven by the Slint presentation module on the UI thread.
class VideoRenderer {
public:
enum class SubmitResult {
submitted,
not_selected,
dropped,
failed,
};
enum class RenderResult {
rendered,
idle,
empty,
failed,
};
struct RenderOutcome {
RenderResult result = RenderResult::failed;
int width = 0;
int height = 0;
uint64_t sequence = 0;
};
virtual ~VideoRenderer() = default;
virtual bool IsReady() const = 0;
// True only while the renderer can accept and present native frames. Metal
// additionally requires attachment to an AppKit view; OpenGL requires an
// active Slint rendering context.
virtual bool IsActive() const = 0;
virtual bool SetSelectedStream(std::string remote_id) = 0;
virtual void DiscardStream(std::string_view remote_id) = 0;
virtual SubmitResult SubmitNv12(std::string_view remote_id,
const uint8_t* data, size_t size, int width,
int height) = 0;
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
const uint8_t* data, size_t size,
int width, int height) = 0;
// Retains a platform-native decoded frame when the backend supports its
// storage and handle types. Unsupported backends keep the default fallback.
virtual SubmitResult SubmitNativeFrame(std::string_view,
const MiniRtcNativeVideoFrame&) {
return SubmitResult::failed;
}
virtual bool CopyLatestNv12(std::string_view remote_id,
std::vector<unsigned char>* output, int* width,
int* height) const = 0;
};
// Exactly one factory implementation is linked for the target platform.
std::unique_ptr<VideoRenderer> CreateVideoRenderer();
} // namespace crossdesk
#endif // CROSSDESK_GUI_PLATFORM_VIDEO_RENDERER_H_
@@ -1,69 +0,0 @@
#include "platform/autostart_backend.h"
#include <windows.h>
#include <filesystem>
#include <string>
namespace crossdesk::platform {
namespace {
constexpr const char* kRunKey =
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
} // namespace
std::string GetAutostartExecutablePath() {
char path[32768] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
return length > 0 && length < sizeof(path) ? std::string(path, length)
: std::string();
}
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path) {
if (!std::filesystem::exists(executable_path)) return false;
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
return false;
}
std::string value = executable_path;
if (value.find(' ') != std::string::npos &&
(value.front() != '"' || value.back() != '"')) {
value = "\"" + value + "\"";
}
const LONG result = RegSetValueExA(
key, app_name.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(value.c_str()),
static_cast<DWORD>(value.size() + 1));
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
bool DisableAutostart(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
return false;
}
const LONG result = RegDeleteValueA(key, app_name.c_str());
RegCloseKey(key);
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
}
bool IsAutostartEnabled(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_READ, &key) !=
ERROR_SUCCESS) {
return false;
}
const LONG result =
RegQueryValueExA(key, app_name.c_str(), nullptr, nullptr, nullptr, nullptr);
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
} // namespace crossdesk::platform
@@ -1,139 +0,0 @@
#include "clipboard.h"
#include <windows.h>
#include <string>
#include "platform/clipboard_backend.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
HWND g_clipboard_window = nullptr;
constexpr const char* kClipboardWindowClass = "CrossDeskClipboardMonitor";
LRESULT CALLBACK ClipboardWindowProc(HWND window, UINT message, WPARAM wparam,
LPARAM lparam) {
if (message == WM_CLIPBOARDUPDATE) {
platform::HandleClipboardChange();
return 0;
}
return DefWindowProc(window, message, wparam, lparam);
}
} // namespace
std::string Clipboard::GetText() {
if (!OpenClipboard(nullptr)) {
LOG_ERROR("Clipboard::GetText: failed to open clipboard");
return {};
}
std::string result;
HANDLE data = GetClipboardData(CF_UNICODETEXT);
if (data) {
const wchar_t* text = static_cast<const wchar_t*>(GlobalLock(data));
if (text) {
const int size =
WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
if (size > 1) {
result.resize(static_cast<size_t>(size));
WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size,
nullptr, nullptr);
result.resize(static_cast<size_t>(size - 1));
}
GlobalUnlock(data);
}
}
CloseClipboard();
return result;
}
bool Clipboard::SetText(const std::string& text) {
if (!OpenClipboard(nullptr)) {
LOG_ERROR("Clipboard::SetText: failed to open clipboard");
return false;
}
if (!EmptyClipboard()) {
LOG_ERROR("Clipboard::SetText: failed to empty clipboard");
CloseClipboard();
return false;
}
const int size =
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
HGLOBAL memory =
size > 0 ? GlobalAlloc(GMEM_MOVEABLE, size * sizeof(wchar_t)) : nullptr;
if (!memory) {
LOG_ERROR("Clipboard::SetText: failed to allocate memory");
CloseClipboard();
return false;
}
wchar_t* output = static_cast<wchar_t*>(GlobalLock(memory));
if (!output) {
GlobalFree(memory);
CloseClipboard();
return false;
}
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, output, size);
GlobalUnlock(memory);
if (!SetClipboardData(CF_UNICODETEXT, memory)) {
GlobalFree(memory);
CloseClipboard();
return false;
}
CloseClipboard();
return true;
}
bool Clipboard::HasText() {
if (!OpenClipboard(nullptr)) return false;
const bool result = IsClipboardFormatAvailable(CF_UNICODETEXT) ||
IsClipboardFormatAvailable(CF_TEXT);
CloseClipboard();
return result;
}
namespace platform {
void RunClipboardMonitor(int) {
WNDCLASSA window_class = {};
window_class.lpfnWndProc = ClipboardWindowProc;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.lpszClassName = kClipboardWindowClass;
RegisterClassA(&window_class);
g_clipboard_window = CreateWindowA(
kClipboardWindowClass, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
nullptr, nullptr);
if (!g_clipboard_window ||
!AddClipboardFormatListener(g_clipboard_window)) {
LOG_ERROR("Failed to initialize clipboard monitor window");
if (g_clipboard_window) DestroyWindow(g_clipboard_window);
g_clipboard_window = nullptr;
return;
}
LOG_INFO("Clipboard event monitoring started (Windows)");
MSG message = {};
while (ClipboardMonitoring()) {
const BOOL result = GetMessage(&message, nullptr, 0, 0);
if (result == 0 || result == -1) break;
TranslateMessage(&message);
DispatchMessage(&message);
}
RemoveClipboardFormatListener(g_clipboard_window);
DestroyWindow(g_clipboard_window);
g_clipboard_window = nullptr;
UnregisterClassA(kClipboardWindowClass, GetModuleHandle(nullptr));
}
void WakeClipboardMonitor() {
if (g_clipboard_window) PostMessage(g_clipboard_window, WM_QUIT, 0, 0);
}
} // namespace platform
} // namespace crossdesk
@@ -1,64 +0,0 @@
#include "platform/daemon_backend.h"
#include <windows.h>
#include <atomic>
#include <chrono>
#include <thread>
#include <vector>
namespace crossdesk::platform {
namespace {
std::atomic<bool> g_stop_requested{false};
} // namespace
void ResetDaemonStop() { g_stop_requested.store(false); }
void RequestDaemonStop() { g_stop_requested.store(true); }
bool DaemonStopRequested() { return g_stop_requested.load(); }
bool PrepareDaemon() { return true; }
std::string GetDaemonExecutablePath() {
char path[32768] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
return length > 0 && length < sizeof(path) ? std::string(path, length)
: std::string();
}
DaemonChildResult RunDaemonChild(const std::string& executable_path,
const std::function<bool()>& keep_running) {
DaemonChildResult result;
STARTUPINFOA startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
std::string command = "\"" + executable_path + "\" --child";
std::vector<char> command_buffer(command.begin(), command.end());
command_buffer.push_back('\0');
if (!CreateProcessA(nullptr, command_buffer.data(), nullptr, nullptr, FALSE,
0, nullptr, nullptr, &startup, &process)) {
return result;
}
result.started = true;
while (keep_running()) {
const DWORD wait_result = WaitForSingleObject(process.hProcess, 200);
if (wait_result == WAIT_OBJECT_0 || wait_result == WAIT_FAILED) break;
}
if (!keep_running()) {
TerminateProcess(process.hProcess, 1);
WaitForSingleObject(process.hProcess, 3000);
}
DWORD exit_code = 0;
GetExitCodeProcess(process.hProcess, &exit_code);
result.exit_code = static_cast<int>(exit_code);
result.normal_exit = exit_code == 0;
CloseHandle(process.hProcess);
CloseHandle(process.hThread);
return result;
}
} // namespace crossdesk::platform
@@ -1,109 +0,0 @@
#include "platform/windows/gui/slint_backend.h"
#include <windows.h>
#include <cstdlib>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <string_view>
namespace crossdesk {
namespace {
struct HandleCloser {
void operator()(HANDLE handle) const noexcept {
if (handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
}
}
};
using ScopedHandle = std::unique_ptr<void, HandleCloser>;
std::string ErrorCode(DWORD code) {
std::ostringstream text;
text << "0x" << std::hex << std::uppercase << code;
return text.str();
}
std::string ProbeSlintRenderer(DWORD timeout_ms) {
std::wstring executable(32768, L'\0');
const DWORD length = GetModuleFileNameW(
nullptr, executable.data(), static_cast<DWORD>(executable.size()));
if (length == 0 || length >= executable.size()) {
return "Unable to locate renderer probe executable";
}
executable.resize(length);
// A job also cleans up the probe if the application is closed while the
// graphics driver is stuck. Create it suspended so assignment cannot race.
ScopedHandle job(CreateJobObjectW(nullptr, nullptr));
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!job ||
!SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
&limits, sizeof(limits))) {
return "Unable to create renderer probe job: " + ErrorCode(GetLastError());
}
constexpr std::string_view argument = kSlintRendererProbeArgument;
std::wstring command = L"\"" + executable + L"\" ";
command.append(argument.begin(), argument.end());
STARTUPINFOW startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr,
FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr,
nullptr, &startup, &process)) {
return "Unable to start renderer probe: " + ErrorCode(GetLastError());
}
ScopedHandle process_handle(process.hProcess);
ScopedHandle thread_handle(process.hThread);
if (!AssignProcessToJobObject(job.get(), process.hProcess) ||
ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
const DWORD error = GetLastError();
TerminateProcess(process.hProcess, 1);
return "Unable to run renderer probe: " + ErrorCode(error);
}
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
if (wait != WAIT_OBJECT_0) {
// Closing the job kills only our probe, including any stuck driver thread.
return wait == WAIT_TIMEOUT ? "Slint renderer probe timed out"
: "Waiting for renderer probe failed: " +
ErrorCode(GetLastError());
}
DWORD exit_code = 0;
if (!GetExitCodeProcess(process.hProcess, &exit_code)) {
return "Unable to read renderer probe result: " + ErrorCode(GetLastError());
}
return exit_code == 0
? std::string{}
: "Slint renderer probe exited with " + ErrorCode(exit_code);
}
} // namespace
SlintBackendSelection ConfigureWindowsSlintBackend(uint32_t probe_timeout_ms) {
if (const char* requested = std::getenv("SLINT_BACKEND");
requested && requested[0] != '\0') {
return {true, requested, "Using explicitly requested Slint backend"};
}
// Slint creates its own GL context. A successful SDL/WGL capability check
// cannot validate it, and a Rust panic cannot be caught by C++ exceptions.
// Exercise Slint in a separate process without touching GL in this process.
const std::string error = ProbeSlintRenderer(probe_timeout_ms);
if (error.empty()) {
return {true, "default", "Slint renderer probe rendered successfully"};
}
// _putenv_s updates the CRT and the OS environment read by slint_cpp.dll.
// This is process-local so a later launch can retry hardware rendering.
if (_putenv_s("SLINT_BACKEND", "winit-software") != 0) {
return {false, {}, "Unable to select Slint software rendering: " + error};
}
return {true, "winit-software", error};
}
} // namespace crossdesk
@@ -1,33 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SLINT_BACKEND_H_
#define _SLINT_BACKEND_H_
#include <cstdint>
#include <string>
namespace crossdesk {
struct SlintBackendSelection {
bool success = true;
std::string backend;
std::string diagnostic;
};
inline constexpr char kSlintRendererProbeArgument[] = "--slint-renderer-probe";
// Call before creating any Slint components. The executable's main() must
// dispatch kSlintRendererProbeArgument before services, settings or the daemon.
SlintBackendSelection ConfigureWindowsSlintBackend(
uint32_t probe_timeout_ms = 10000);
// Internal child-process entry point; never call in the main application.
int RunSlintRendererProbe();
} // namespace crossdesk
#endif
@@ -1,59 +0,0 @@
#include <SDL3/SDL.h>
#include <windows.h>
#include <chrono>
#include <iostream>
#include "crossdesk_ui.h"
#include "platform/windows/gui/slint_backend.h"
namespace crossdesk {
int RunSlintRendererProbe() {
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
if (!SDL_Init(SDL_INIT_VIDEO)) {
return 1;
}
int result = 2;
{
// Reuse the application's single AOT entry point. Independently generated
// headers can contain colliding internal component names across TUs.
auto probe = ui::MainWindow::create();
probe->set_custom_titlebar(true);
// AfterRendering confirms that the real GL context, shader initialization
// and first draw all succeeded. GPU readback from an off-screen window can
// be black even when rendering works, so do not use its pixels as a test.
const auto notifier_error = probe->window().set_rendering_notifier(
[&](slint::RenderingState state, slint::GraphicsAPI) {
if (state == slint::RenderingState::AfterRendering) {
result = 0;
}
});
probe->window().set_position(
slint::PhysicalPosition(slint::Point<int32_t>{-32000, -32000}));
probe->show();
slint::Timer timer;
timer.start(slint::TimerMode::SingleShot, std::chrono::milliseconds(100),
[&] {
// Renderers without a rendering notifier (software) support
// snapshots.
if (notifier_error) {
const auto pixels = probe->window().take_snapshot();
if (pixels && pixels->width() > 0 && pixels->height() > 0) {
result = 0;
} else {
std::cerr << "Slint probe could not read rendered pixels"
<< std::endl;
}
}
slint::quit_event_loop();
});
slint::run_event_loop();
probe->hide();
}
SDL_Quit();
return result;
}
} // namespace crossdesk
@@ -1,19 +0,0 @@
#include "device_controller_factory.h"
#include "keyboard_capturer.h"
#include "mouse_controller.h"
namespace crossdesk {
DeviceController* DeviceControllerFactory::Create(Device device) {
switch (device) {
case Mouse:
return new PlatformMouseController();
case Keyboard:
return new PlatformKeyboardCapturer();
default:
return nullptr;
}
}
} // namespace crossdesk
@@ -1,51 +0,0 @@
#include "platform/path_backend.h"
#include <shlobj.h>
#include <windows.h>
#include <vector>
namespace crossdesk::platform {
namespace {
std::filesystem::path GetKnownFolder(REFKNOWNFOLDERID id) {
PWSTR value = nullptr;
if (FAILED(SHGetKnownFolderPath(id, 0, nullptr, &value))) return {};
const std::filesystem::path result(value);
CoTaskMemFree(value);
return result;
}
} // namespace
std::filesystem::path GetExecutableDirectory() {
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
const DWORD length = GetModuleFileNameW(
nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (length == 0 || buffer.size() >= 32768) return {};
if (length < buffer.size()) {
return std::filesystem::path(buffer.data(), buffer.data() + length)
.parent_path();
}
buffer.resize(buffer.size() * 2);
}
}
std::filesystem::path GetConfigPath(const std::string& app_name) {
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name;
}
std::filesystem::path GetCachePath(const std::string& app_name) {
#ifdef CROSSDESK_DEBUG
return "cache";
#else
return GetKnownFolder(FOLDERID_LocalAppData) / app_name / "cache";
#endif
}
std::filesystem::path GetLogPath(const std::string& app_name) {
return GetKnownFolder(FOLDERID_LocalAppData) / app_name / "logs";
}
} // namespace crossdesk::platform
@@ -1,11 +0,0 @@
#include "screen_capturer_factory.h"
#include "screen_capturer_win.h"
namespace crossdesk {
ScreenCapturer* ScreenCapturerFactory::Create() {
return new ScreenCapturerWin();
}
} // namespace crossdesk
@@ -1,50 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_FRAME_SCHEDULE_H_
#define _SECURE_DESKTOP_FRAME_SCHEDULE_H_
#include <cstdint>
#include <string>
namespace crossdesk {
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
return error == "frame_wait_timeout" ||
error == "shared_frame_write_in_progress" ||
error == "shared_frame_changed_during_read";
}
// A 4K GDI frame can take longer than a single consumer wait. Do not issue
// another expensive screenshot while the shared producer is still healthy.
class SecureDesktopFrameSchedule {
public:
void Reset(uint64_t now) {
last_shared_frame_ = now;
next_start_attempt_ = 0;
}
void OnSharedFrame(uint64_t now) { last_shared_frame_ = now; }
bool SharedCaptureStalled(uint64_t now) const {
return now - last_shared_frame_ >= 1000;
}
bool CanStart(uint64_t now) const { return now >= next_start_attempt_; }
void OnStartFailure(uint64_t now) { next_start_attempt_ = now + 1000; }
static int RemainingFrameDelay(int interval_ms, uint64_t elapsed_ms) {
return elapsed_ms >= static_cast<uint64_t>(interval_ms)
? 0
: interval_ms - static_cast<int>(elapsed_ms);
}
private:
uint64_t last_shared_frame_ = 0;
uint64_t next_start_attempt_ = 0;
};
} // namespace crossdesk
#endif
@@ -1,110 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_STATUS_POLLER_H_
#define _SECURE_DESKTOP_STATUS_POLLER_H_
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
namespace crossdesk {
struct SecureDesktopServiceStatus {
bool service_available = false;
bool capture_active = false;
bool helper_running = false;
uint32_t helper_process_id = 0;
uint32_t active_session_id = 0xFFFFFFFF;
uint32_t error_code = 0;
std::string interactive_stage;
std::string interactive_desktop;
std::string error;
void UpdateFrom(SecureDesktopServiceStatus latest) {
if (latest.service_available) {
*this = std::move(latest);
} else {
// Failed IPC says nothing about the desktop. Retain the last confirmed
// capture state until a valid response reports a change or an unlock.
service_available = false;
error = std::move(latest.error);
error_code = latest.error_code;
}
}
};
// The query runs without holding the snapshot mutex. Slow service/helper IPC
// must not stop delivery of frames that are already available in shared memory.
class SecureDesktopStatusPoller {
public:
using Query = std::function<SecureDesktopServiceStatus()>;
struct Sample {
SecureDesktopServiceStatus status;
int64_t query_ms = 0;
};
explicit SecureDesktopStatusPoller(Query query,
std::chrono::milliseconds interval)
: thread_([this, query = std::move(query), interval]() {
std::unique_lock<std::mutex> lock(mutex_);
while (!stopping_) {
lock.unlock();
const auto started = std::chrono::steady_clock::now();
auto latest = query();
const auto elapsed =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - started);
lock.lock();
sample_.status.UpdateFrom(std::move(latest));
sample_.query_ms = elapsed.count();
pending_ = true;
// Even a failed/slow query gets a pause, avoiding a retry storm.
wake_.wait_for(lock, interval, [this] { return stopping_; });
}
}) {}
~SecureDesktopStatusPoller() {
{
std::lock_guard<std::mutex> lock(mutex_);
stopping_ = true;
}
wake_.notify_one();
thread_.join();
}
SecureDesktopStatusPoller(const SecureDesktopStatusPoller&) = delete;
SecureDesktopStatusPoller& operator=(const SecureDesktopStatusPoller&) =
delete;
std::optional<Sample> Take() {
std::lock_guard<std::mutex> lock(mutex_);
if (!pending_) {
return std::nullopt;
}
pending_ = false;
return sample_;
}
private:
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false;
bool pending_ = false;
Sample sample_;
std::thread thread_;
};
} // namespace crossdesk
#endif
@@ -1,101 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _INTERACTIVE_DESKTOP_H_
#define _INTERACTIVE_DESKTOP_H_
#include <Windows.h>
#include <string>
namespace crossdesk {
inline std::wstring GetDesktopNameW(HDESK desktop) {
DWORD bytes_needed = 0;
if (desktop == nullptr) {
return {};
}
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
if (bytes_needed == 0) {
return {};
}
std::wstring name(bytes_needed / sizeof(wchar_t), L'\0');
if (!GetUserObjectInformationW(desktop, UOI_NAME, name.data(), bytes_needed,
&bytes_needed)) {
return {};
}
while (!name.empty() && name.back() == L'\0') {
name.pop_back();
}
return name;
}
// Use only on worker threads without windows or hooks. Restore the original
// desktop before closing the handle: Windows cannot close a desktop in use.
class ScopedInteractiveDesktop {
public:
ScopedInteractiveDesktop()
: original_(GetThreadDesktop(GetCurrentThreadId())) {}
ScopedInteractiveDesktop(const ScopedInteractiveDesktop&) = delete;
ScopedInteractiveDesktop& operator=(const ScopedInteractiveDesktop&) = delete;
~ScopedInteractiveDesktop() {
if (desktop_ != nullptr) {
SetThreadDesktop(original_);
CloseDesktop(desktop_);
}
}
bool Bind(const std::wstring& fallback_name) {
constexpr ACCESS_MASK access =
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP | DESKTOP_JOURNALPLAYBACK;
// SendInput requires playback access on the attached desktop; otherwise
// it returns ERROR_ACCESS_DENIED even for a correctly attached SYSTEM helper.
// The service's stage/name can lag a lock, unlock, or UAC transition.
// Opening a cached desktop can succeed even when it is no longer visible.
HDESK desktop = OpenInputDesktop(0, FALSE, access);
if (desktop == nullptr && !fallback_name.empty()) {
desktop = OpenDesktopW(fallback_name.c_str(), 0, FALSE, access);
}
if (desktop == nullptr) {
return false;
}
if (!SetThreadDesktop(desktop)) {
const DWORD error = GetLastError();
CloseDesktop(desktop);
SetLastError(error);
return false;
}
if (desktop_ != nullptr) {
CloseDesktop(desktop_);
}
desktop_ = desktop;
name_ = GetDesktopNameW(desktop_);
return true;
}
bool IsInputDesktop() const {
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
if (input == nullptr) {
return false;
}
const std::wstring input_name = GetDesktopNameW(input);
CloseDesktop(input);
return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0;
}
const std::wstring& name() const { return name_; }
private:
HDESK original_ = nullptr;
HDESK desktop_ = nullptr;
std::wstring name_;
};
} // namespace crossdesk
#endif
@@ -1,143 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _NAMED_PIPE_DEADLINE_H_
#define _NAMED_PIPE_DEADLINE_H_
#include <Windows.h>
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
namespace crossdesk {
namespace pipe_deadline_detail {
struct Handle {
explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : value(handle) {}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
HANDLE value = INVALID_HANDLE_VALUE;
~Handle() {
if (value != INVALID_HANDLE_VALUE && value != nullptr) CloseHandle(value);
}
};
inline DWORD Remaining(ULONGLONG deadline) {
const ULONGLONG now = GetTickCount64();
return now >= deadline ? 0 : static_cast<DWORD>(deadline - now);
}
inline bool Transfer(HANDLE pipe, HANDLE event, bool write, void* data,
DWORD size, ULONGLONG deadline, DWORD* transferred,
DWORD* error) {
*transferred = 0;
if (Remaining(deadline) == 0) {
*error = ERROR_SEM_TIMEOUT;
return false;
}
ResetEvent(event);
OVERLAPPED operation{};
operation.hEvent = event;
const BOOL complete =
write ? WriteFile(pipe, data, size, transferred, &operation)
: ReadFile(pipe, data, size, transferred, &operation);
*error = complete ? ERROR_SUCCESS : GetLastError();
if (*error == ERROR_IO_PENDING) {
const DWORD wait = WaitForSingleObject(event, Remaining(deadline));
if (wait != WAIT_OBJECT_0) {
*error = wait == WAIT_TIMEOUT ? ERROR_SEM_TIMEOUT : GetLastError();
// Keep the OVERLAPPED and buffer alive until cancellation completes.
CancelIoEx(pipe, &operation);
DWORD ignored = 0;
GetOverlappedResult(pipe, &operation, &ignored, TRUE);
return false;
}
} else if (*error != ERROR_SUCCESS && *error != ERROR_MORE_DATA) {
return false;
}
// Immediate MORE_DATA also needs GetOverlappedResult: ReadFile may leave
// its byte-count output at zero even though part of the message was read.
const BOOL result = GetOverlappedResult(pipe, &operation, transferred, FALSE);
*error = result ? ERROR_SUCCESS : GetLastError();
return result != FALSE;
}
} // namespace pipe_deadline_detail
// One deadline covers connection, write, and every response chunk. A connect
// timeout alone does not bound a synchronous ReadFile on an unresponsive peer.
inline bool QueryNamedPipeWithDeadline(const std::wstring& name,
const std::string& command,
DWORD timeout_ms,
std::vector<uint8_t>* response,
std::string* error, DWORD* error_code) {
using namespace pipe_deadline_detail;
response->clear();
error->clear();
*error_code = ERROR_SUCCESS;
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
Handle pipe;
while (true) {
const DWORD remaining = Remaining(deadline);
if (remaining == 0) break;
if (WaitNamedPipeW(name.c_str(), remaining)) {
pipe.value =
CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
if (pipe.value != INVALID_HANDLE_VALUE) break;
}
*error_code = GetLastError();
if (*error_code != ERROR_FILE_NOT_FOUND && *error_code != ERROR_PIPE_BUSY &&
*error_code != ERROR_SEM_TIMEOUT)
break;
Sleep((std::min)(DWORD{10}, Remaining(deadline)));
}
if (pipe.value == INVALID_HANDLE_VALUE) {
*error = "pipe_unavailable";
if (*error_code == ERROR_SUCCESS) *error_code = ERROR_SEM_TIMEOUT;
return false;
}
DWORD mode = PIPE_READMODE_MESSAGE;
if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) {
*error = "pipe_connect_failed";
*error_code = GetLastError();
return false;
}
Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
if (event.value == nullptr) {
*error = "pipe_connect_failed";
*error_code = GetLastError();
return false;
}
DWORD transferred = 0;
if (!Transfer(pipe.value, event.value, true,
const_cast<char*>(command.data()),
static_cast<DWORD>(command.size()), deadline, &transferred,
error_code) ||
transferred != command.size()) {
*error = "pipe_write_failed";
return false;
}
std::vector<uint8_t> chunk(64 * 1024);
while (true) {
const bool complete = Transfer(pipe.value, event.value, false, chunk.data(),
static_cast<DWORD>(chunk.size()), deadline,
&transferred, error_code);
if (!complete && *error_code != ERROR_MORE_DATA) {
response->clear();
*error = "pipe_read_failed";
return false;
}
response->insert(response->end(), chunk.begin(),
chunk.begin() + transferred);
if (complete) return true;
}
}
} // namespace crossdesk
#endif
@@ -1,11 +0,0 @@
#include "speaker_capturer_factory.h"
#include "speaker_capturer_wasapi.h"
namespace crossdesk {
SpeakerCapturer* SpeakerCapturerFactory::Create() {
return new SpeakerCapturerWasapi();
}
} // namespace crossdesk
@@ -1,45 +0,0 @@
#include "platform.h"
#include <Winsock2.h>
#include <iphlpapi.h>
#include <cstdio>
#include "rd_log.h"
namespace crossdesk {
std::string GetMac() {
IP_ADAPTER_INFO adapters[16] = {};
DWORD size = sizeof(adapters);
if (GetAdaptersInfo(adapters, &size) != ERROR_SUCCESS) return {};
char address[16] = {};
int length = 0;
const PIP_ADAPTER_INFO adapter = adapters;
for (UINT index = 0; index < adapter->AddressLength; ++index) {
length += sprintf_s(address + length, sizeof(address) - length, "%.2X",
adapter->Address[index]);
}
return address;
}
std::string GetHostName() {
WSADATA data = {};
if (WSAStartup(MAKEWORD(2, 2), &data) != 0) {
LOG_ERROR("WSAStartup failed");
return {};
}
char hostname[256] = {};
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
LOG_ERROR("gethostname failed: {}", WSAGetLastError());
WSACleanup();
return {};
}
WSACleanup();
return hostname;
}
bool IsWaylandSession() { return false; }
} // namespace crossdesk
@@ -1,165 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#include "captured_nv12_frame.h"
#include <algorithm>
#include <cstring>
#include <limits>
#include <new>
#include <utility>
namespace crossdesk {
namespace {
bool GetNv12Size(uint32_t width, uint32_t height, size_t* size) {
if (!size || width == 0 || height == 0 || (width & 1U) != 0 ||
(height & 1U) != 0 ||
static_cast<size_t>(width) >
std::numeric_limits<size_t>::max() / height) {
return false;
}
const size_t pixels = static_cast<size_t>(width) * height;
if (pixels > std::numeric_limits<size_t>::max() / 3U * 2U) {
return false;
}
*size = pixels * 3U / 2U;
return true;
}
} // namespace
std::shared_ptr<CapturedNv12FramePool> CapturedNv12FramePool::Create(
size_t max_cached_buffers) {
return std::shared_ptr<CapturedNv12FramePool>(
new CapturedNv12FramePool(max_cached_buffers));
}
CapturedNv12FramePool::CapturedNv12FramePool(size_t max_cached_buffers)
: max_cached_buffers_(max_cached_buffers) {}
CapturedNv12Frame* CapturedNv12FramePool::CopyFrom(const uint8_t* data,
size_t size,
uint32_t width,
uint32_t height) {
size_t required_size = 0;
if (!data || !GetNv12Size(width, height, &required_size) ||
size < required_size) {
return nullptr;
}
std::vector<uint8_t> storage;
{
std::lock_guard<std::mutex> lock(mutex_);
auto best = free_buffers_.end();
for (auto it = free_buffers_.begin(); it != free_buffers_.end(); ++it) {
if (it->capacity() < required_size) {
continue;
}
if (best == free_buffers_.end() || it->capacity() < best->capacity()) {
best = it;
}
}
if (best == free_buffers_.end() && !free_buffers_.empty()) {
best = std::max_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
}
if (best != free_buffers_.end()) {
storage = std::move(*best);
free_buffers_.erase(best);
}
}
try {
storage.resize(required_size);
std::memcpy(storage.data(), data, required_size);
return new CapturedNv12Frame(shared_from_this(), std::move(storage), width,
height);
} catch (...) {
return nullptr;
}
}
void CapturedNv12FramePool::Recycle(std::vector<uint8_t> storage) {
if (max_cached_buffers_ == 0 || storage.empty()) {
return;
}
std::lock_guard<std::mutex> lock(mutex_);
if (free_buffers_.size() < max_cached_buffers_) {
free_buffers_.push_back(std::move(storage));
return;
}
auto smallest = std::min_element(
free_buffers_.begin(), free_buffers_.end(),
[](const auto& lhs, const auto& rhs) {
return lhs.capacity() < rhs.capacity();
});
if (smallest != free_buffers_.end() &&
smallest->capacity() < storage.capacity()) {
*smallest = std::move(storage);
}
}
CapturedNv12Frame::CapturedNv12Frame(
std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width, uint32_t height)
: pool_(std::move(pool)),
storage_(std::move(storage)),
width_(width),
height_(height) {
descriptor_.struct_size = sizeof(descriptor_);
descriptor_.type = MiniRtcNativeVideoFrameCpuNv12;
descriptor_.width = width_;
descriptor_.height = height_;
descriptor_.payload.cpu_nv12 = {
storage_.data(), storage_.data() + static_cast<size_t>(width_) * height_,
width_, width_};
descriptor_.owner = this;
descriptor_.retain = &RetainOwner;
descriptor_.release = &ReleaseOwner;
descriptor_.copy_to_nv12 = &CopyToNv12;
}
CapturedNv12Frame::~CapturedNv12Frame() {
if (pool_) {
pool_->Recycle(std::move(storage_));
}
}
void CapturedNv12Frame::AddRef() {
references_.fetch_add(1, std::memory_order_relaxed);
}
void CapturedNv12Frame::Release() {
if (references_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
}
}
void CapturedNv12Frame::RetainOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->AddRef();
}
void CapturedNv12Frame::ReleaseOwner(void* owner) {
static_cast<CapturedNv12Frame*>(owner)->Release();
}
int CapturedNv12Frame::CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size) {
auto* frame = static_cast<CapturedNv12Frame*>(owner);
if (!frame || !destination || destination_size < frame->Size()) {
return -1;
}
std::memcpy(destination, frame->storage_.data(), frame->Size());
return 0;
}
} // namespace crossdesk
@@ -1,78 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-04
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _CAPTURED_NV12_FRAME_H_
#define _CAPTURED_NV12_FRAME_H_
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <vector>
#include "minirtc.h"
namespace crossdesk {
class CapturedNv12Frame;
// Owns the pixels behind captured native frames. A frame may outlive the
// capture callback while MiniRTC waits for its asynchronous encoder, so capture
// buffers cannot be borrowed directly. Released allocations are cached to keep
// that ownership transfer inexpensive after the pipeline is warm.
class CapturedNv12FramePool final
: public std::enable_shared_from_this<CapturedNv12FramePool> {
public:
static std::shared_ptr<CapturedNv12FramePool> Create(
size_t max_cached_buffers = 4);
CapturedNv12Frame* CopyFrom(const uint8_t* data, size_t size,
uint32_t width, uint32_t height);
private:
explicit CapturedNv12FramePool(size_t max_cached_buffers);
void Recycle(std::vector<uint8_t> storage);
friend class CapturedNv12Frame;
const size_t max_cached_buffers_;
std::mutex mutex_;
std::vector<std::vector<uint8_t>> free_buffers_;
};
class CapturedNv12Frame final {
public:
const MiniRtcNativeVideoFrame* Descriptor() const { return &descriptor_; }
size_t Size() const { return storage_.size(); }
void AddRef();
void Release();
private:
friend class CapturedNv12FramePool;
CapturedNv12Frame(std::shared_ptr<CapturedNv12FramePool> pool,
std::vector<uint8_t> storage, uint32_t width,
uint32_t height);
~CapturedNv12Frame();
static void RetainOwner(void* owner);
static void ReleaseOwner(void* owner);
static int CopyToNv12(void* owner, uint8_t* destination,
size_t destination_size);
std::atomic<uint32_t> references_{1};
std::shared_ptr<CapturedNv12FramePool> pool_;
std::vector<uint8_t> storage_;
uint32_t width_ = 0;
uint32_t height_ = 0;
MiniRtcNativeVideoFrame descriptor_{};
};
} // namespace crossdesk
#endif
@@ -1,22 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2023-12-15
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_FACTORY_H_
#define _SCREEN_CAPTURER_FACTORY_H_
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerFactory {
public:
virtual ~ScreenCapturerFactory() {}
public:
ScreenCapturer* Create();
};
} // namespace crossdesk
#endif
@@ -1,22 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-07-22
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SPEAKER_CAPTURER_FACTORY_H_
#define _SPEAKER_CAPTURER_FACTORY_H_
#include "speaker_capturer.h"
namespace crossdesk {
class SpeakerCapturerFactory {
public:
virtual ~SpeakerCapturerFactory() {}
public:
SpeakerCapturer* Create();
};
} // namespace crossdesk
#endif
-86
View File
@@ -1,86 +0,0 @@
#include "clipboard.h"
#include <atomic>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include "platform/clipboard_backend.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::atomic<bool> g_monitoring{false};
std::thread g_monitor_thread;
std::mutex g_monitor_mutex;
std::string g_last_clipboard_text;
Clipboard::OnClipboardChanged g_on_clipboard_changed;
} // namespace
namespace platform {
bool ClipboardMonitoring() { return g_monitoring.load(); }
void HandleClipboardChange() {
if (!Clipboard::HasText()) {
std::lock_guard<std::mutex> lock(g_monitor_mutex);
if (!g_last_clipboard_text.empty()) {
g_last_clipboard_text.clear();
LOG_INFO("Clipboard content cleared");
}
return;
}
const std::string current_text = Clipboard::GetText();
std::lock_guard<std::mutex> lock(g_monitor_mutex);
if (current_text == g_last_clipboard_text) return;
g_last_clipboard_text = current_text;
if (!current_text.empty() && g_on_clipboard_changed) {
const int result =
g_on_clipboard_changed(current_text.c_str(), current_text.length());
if (result != 0) {
LOG_WARN("Clipboard callback returned error: {}", result);
}
}
}
} // namespace platform
void Clipboard::StartMonitoring(int check_interval_ms,
OnClipboardChanged on_changed) {
if (g_monitoring.load()) {
LOG_WARN("Clipboard monitoring is already running");
return;
}
{
std::lock_guard<std::mutex> lock(g_monitor_mutex);
g_on_clipboard_changed = std::move(on_changed);
g_last_clipboard_text = HasText() ? GetText() : std::string();
}
g_monitoring.store(true);
g_monitor_thread = std::thread(
[check_interval_ms] { platform::RunClipboardMonitor(check_interval_ms); });
LOG_INFO("Clipboard event monitoring started");
}
void Clipboard::StopMonitoring() {
if (!g_monitoring.exchange(false)) return;
platform::WakeClipboardMonitor();
if (g_monitor_thread.joinable()) g_monitor_thread.join();
{
std::lock_guard<std::mutex> lock(g_monitor_mutex);
g_last_clipboard_text.clear();
g_on_clipboard_changed = nullptr;
}
LOG_INFO("Clipboard monitoring stopped");
}
bool Clipboard::IsMonitoring() { return g_monitoring.load(); }
} // namespace crossdesk
@@ -1,30 +0,0 @@
#include <iostream>
#include <vector>
#include <remote_action.h>
#include "cursor_position.h"
#include "display_info.h"
namespace {
bool Expect(bool condition, const char* message) {
if (condition) return true;
std::cerr << message << '\n';
return false;
}
} // namespace
int main() {
const std::vector<crossdesk::DisplayInfo> displays = {
crossdesk::DisplayInfo("Test Display", 0, 0, 1920, 1080)};
crossdesk::CursorState normalized_cursor{};
const bool ok = Expect(
crossdesk::NormalizeCursorPosition(960.0, 540.0, displays, 0,
&normalized_cursor) &&
normalized_cursor.position_valid &&
normalized_cursor.x == 0.5f && normalized_cursor.y == 0.5f,
"cursor position should use continuous display extents");
return ok ? 0 : 1;
}
@@ -1,163 +0,0 @@
#include <array>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
namespace {
std::filesystem::path FindRepoRoot() {
std::filesystem::path current = std::filesystem::current_path();
while (!current.empty()) {
if (std::filesystem::exists(current / "xmake.lua") &&
std::filesystem::exists(
current /
"libs/wire/include/remote_action.h") &&
std::filesystem::exists(
current / "apps/desktop/xmake/targets.lua")) {
return current;
}
current = current.parent_path();
}
return {};
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
}
std::ostringstream contents;
contents << file.rdbuf();
return contents.str();
}
bool IsCheckedWireFile(const std::filesystem::path& path) {
static constexpr std::array<std::string_view, 8> kExtensions = {
".h", ".hpp", ".c", ".cc", ".cpp", ".m", ".mm", ".lua"};
const std::string extension = path.extension().string();
for (std::string_view candidate : kExtensions) {
if (extension == candidate) {
return true;
}
}
return false;
}
bool CheckWireBoundary(const std::filesystem::path& repo_root) {
static constexpr std::array<std::string_view, 12> kForbiddenReferences = {
"apps/desktop", "../desktop", "platform/common", "<windows.h>",
"<Windows.h>", "<AppKit/", "<Cocoa/", "<CoreGraphics/",
"<X11/", "<wayland-", "<linux/", "<d3d"};
bool ok = true;
const std::filesystem::path wire_root = repo_root / "libs/wire";
for (const auto& entry :
std::filesystem::recursive_directory_iterator(wire_root)) {
if (!entry.is_regular_file() || !IsCheckedWireFile(entry.path())) {
continue;
}
const std::string contents = ReadFile(entry.path());
for (std::string_view forbidden : kForbiddenReferences) {
if (contents.find(forbidden) == std::string::npos) {
continue;
}
std::cerr << entry.path().lexically_relative(repo_root).string()
<< " crosses the wire/app boundary with: " << forbidden
<< '\n';
ok = false;
}
}
return ok;
}
bool CheckPlatformIncludesArePrivate(
const std::filesystem::path& repo_root) {
const std::filesystem::path targets_path =
repo_root / "apps/desktop/xmake/targets.lua";
const std::string targets = ReadFile(targets_path);
if (targets.empty()) {
std::cerr << "failed to read "
<< targets_path.lexically_relative(repo_root).string() << '\n';
return false;
}
bool ok = true;
std::size_t cursor = 0;
constexpr std::string_view kCall = "add_includedirs(";
constexpr std::string_view kPlatformPath =
"apps/desktop/src/platform/";
while ((cursor = targets.find(kCall, cursor)) != std::string::npos) {
const std::size_t call_end = targets.find(')', cursor + kCall.size());
if (call_end == std::string::npos) {
std::cerr << "unterminated add_includedirs call in "
<< targets_path.lexically_relative(repo_root).string() << '\n';
return false;
}
const std::string_view call(targets.data() + cursor,
call_end - cursor + 1);
std::string compact_call;
compact_call.reserve(call.size());
for (char character : call) {
if (!std::isspace(static_cast<unsigned char>(character))) {
compact_call.push_back(character);
}
}
if (call.find(kPlatformPath) != std::string_view::npos &&
compact_call.find("public=true") != std::string::npos) {
std::cerr << "platform implementation include directory is public: "
<< call << '\n';
ok = false;
}
cursor = call_end + 1;
}
return ok;
}
bool CheckWireLayout(const std::filesystem::path& repo_root) {
static constexpr std::array<std::string_view, 5> kForbiddenPaths = {
"libs/wire/include/crossdesk", "libs/wire/xmake.lua", "libs/wire/.xmake",
"xmake.protocol.lua", "xmake.wire.lua"};
bool ok = true;
for (std::string_view relative_path : kForbiddenPaths) {
if (!std::filesystem::exists(repo_root / relative_path)) {
continue;
}
std::cerr << "forbidden legacy layer, standalone project, or wire cache: "
<< relative_path << '\n';
ok = false;
}
return ok;
}
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
if (repo_root.empty()) {
std::cerr << "failed to locate repository root\n";
return 1;
}
bool ok = true;
static constexpr std::array<std::string_view, 11> kLegacyRootDirectories = {
"src", "ios", "scripts", "icons",
"tests", "xmake", "shared", "platforms",
"wire", "submodules", "thirdparty"};
for (std::string_view directory : kLegacyRootDirectories) {
if (!std::filesystem::exists(repo_root / directory)) {
continue;
}
std::cerr << "legacy root directory still exists: " << directory << '\n';
ok = false;
}
ok &= CheckWireBoundary(repo_root);
ok &= CheckPlatformIncludesArePrivate(repo_root);
ok &= CheckWireLayout(repo_root);
return ok ? 0 : 1;
}
-643
View File
@@ -1,643 +0,0 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include "ui/ui_localization.h"
#include <cassert>
#include <charconv>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace {
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
// renderer to export deterministic active and inactive visual-check images.
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
const auto snapshot = window.take_snapshot();
if (!snapshot) {
return false;
}
std::ofstream output(path, std::ios::binary);
output << "P6\n"
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
for (const auto &pixel : *snapshot) {
output.put(static_cast<char>(pixel.r));
output.put(static_cast<char>(pixel.g));
output.put(static_cast<char>(pixel.b));
}
return output.good();
}
bool RegisterFontAwesome(slint::Window &window) {
return !window.window_handle()
.register_font_from_data(fa_solid_900_ttf,
fa_solid_900_ttf_len)
.has_value();
}
struct CaptureOptions {
bool enabled = false;
std::string page;
int language = 0;
std::string snapshot_path;
std::string demo_assets_path;
};
// Compatibility entry point for the former application-level debug capture
// mode. CROSSDESK_UI_CAPTURE keeps the interactive workflow, while
// CROSSDESK_UI_CAPTURE_SNAPSHOT writes a deterministic PPM and exits so every
// page can be exercised in automation. The legacy /tmp overrides still win.
std::string ReadFirstLine(const char *path) {
std::ifstream input(path);
std::string value;
std::getline(input, value);
return value;
}
CaptureOptions LoadCaptureOptions() {
CaptureOptions options;
if (const char *capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
options.enabled = std::string_view(capture) != "0";
}
if (!options.enabled) {
return options;
}
if (const char *page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
options.page = page;
}
if (const std::string page_override =
ReadFirstLine("/tmp/crossdesk-ui-capture-page");
!page_override.empty()) {
options.page = page_override;
}
std::string language_value =
ReadFirstLine("/tmp/crossdesk-ui-capture-language");
if (language_value.empty()) {
if (const char *language =
std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")) {
language_value = language;
}
}
if (!language_value.empty()) {
int language = 0;
const auto [end, error] = std::from_chars(
language_value.data(), language_value.data() + language_value.size(),
language);
if (error == std::errc{} &&
end == language_value.data() + language_value.size()) {
options.language =
crossdesk::localization::detail::ClampLanguageIndex(language);
}
}
if (const char *snapshot =
std::getenv("CROSSDESK_UI_CAPTURE_SNAPSHOT")) {
options.snapshot_path = snapshot;
}
if (const char *assets = std::getenv("CROSSDESK_UI_CAPTURE_DEMO_ASSETS")) {
options.demo_assets_path = assets;
}
return options;
}
// Opt-in documentation data, separate from the smoke test's baseline state.
// The images are public page captures, never a user's cached remote desktops.
bool ConfigureDocumentationDemo(
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
const CaptureOptions &options) {
if (options.demo_assets_path.empty()) {
return true;
}
struct DemoDevice {
const char *id;
const char *name;
const char *thumbnail;
};
const DemoDevice devices[] = {
{"987654321", "Dev Workstation", "fixtures/source.png"},
{"246813579", "Office PC", "fixtures/website.png"},
{"135792468", "Home Laptop", "web-client.png"},
};
std::vector<crossdesk::ui::RecentConnection> connections;
for (const auto &device : devices) {
const std::string path = options.demo_assets_path + "/" + device.thumbnail;
const auto thumbnail =
slint::Image::load_from_path(slint::SharedString(path.c_str()));
if (thumbnail.size().width == 0 || thumbnail.size().height == 0) {
std::cerr << "Missing documentation thumbnail: " << path << '\n';
return false;
}
crossdesk::ui::RecentConnection connection;
connection.remote_id = device.id;
connection.display_name = device.name;
connection.host_name = device.name;
connection.online = true;
connection.thumbnail = thumbnail;
connections.emplace_back(std::move(connection));
}
window->set_recent_connections(
std::make_shared<slint::VectorModel<crossdesk::ui::RecentConnection>>(
std::move(connections)));
window->set_signal_connected(true);
window->set_hardware_codec_available(true);
return true;
}
bool HasNonEmptyEnvironmentVariable(const char *name) {
const char *value = std::getenv(name);
return value != nullptr && value[0] != '\0';
}
void ResetMainCaptureState(
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window) {
window->set_settings_open(false);
window->set_self_host_settings_open(false);
window->set_about_open(false);
window->set_update_open(false);
window->set_update_available(false);
window->set_reset_password_open(false);
window->set_reset_password_invalid(false);
window->set_new_password_input("");
window->set_alias_open(false);
window->set_alias_input("");
window->set_delete_open(false);
window->set_offline_warning("");
window->set_connection_dialog_open(false);
window->set_connection_status_text("");
window->set_connection_pending(false);
window->set_connection_password_required(false);
window->set_connection_validating(false);
window->set_permission_dialog_open(false);
window->set_screen_recording_granted(true);
window->set_accessibility_granted(true);
window->set_settings_session_active(false);
window->set_portable_service_settings_visible(false);
window->set_portable_service_dialog_open(false);
window->set_portable_service_suppressed_notice_open(false);
}
void ConfigureMainCapturePage(
const slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
const CaptureOptions &options) {
const int language = crossdesk::ui_localization::ApplyMainWindowStrings(
window, options.language);
crossdesk::ui_localization::ApplyStreamWindowStrings(window, language);
window->set_language_index(language);
ResetMainCaptureState(window);
#if _WIN32
window->set_custom_titlebar(true);
window->set_wayland_titlebar(false);
#if CROSSDESK_PORTABLE
window->set_portable_service_settings_visible(true);
#endif
#elif defined(__linux__)
const bool has_x11 = HasNonEmptyEnvironmentVariable("DISPLAY");
const bool use_xwayland =
has_x11 && HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY");
window->set_custom_titlebar(has_x11);
window->set_wayland_titlebar(use_xwayland);
#else
window->set_custom_titlebar(false);
window->set_wayland_titlebar(false);
#endif
if (options.page == "settings") {
window->set_settings_open(true);
} else if (options.page == "self-hosted") {
window->set_settings_open(true);
window->set_self_host_settings_open(true);
} else if (options.page == "about") {
window->set_current_version("0.0.0");
window->set_about_open(true);
} else if (options.page == "update") {
window->set_update_available(true);
window->set_latest_version("v0.0.1");
window->set_release_name("CrossDesk Preview");
window->set_release_date("2026-08-25");
window->set_update_open(true);
} else if (options.page == "reset-password") {
window->set_reset_password_open(true);
} else if (options.page == "reset-password-invalid") {
window->set_reset_password_open(true);
window->set_reset_password_invalid(true);
window->set_new_password_input("123");
} else if (options.page == "alias") {
window->set_alias_input("Office workstation");
window->set_alias_open(true);
} else if (options.page == "delete") {
window->set_delete_open(true);
} else if (options.page == "offline") {
window->set_offline_warning(crossdesk::ui_localization::Text(
crossdesk::localization::device_offline[language]));
} else if (options.page == "connection") {
window->set_connection_status_text(crossdesk::ui_localization::Text(
crossdesk::localization::p2p_connecting[language]));
window->set_connection_pending(true);
window->set_connection_dialog_open(true);
} else if (options.page == "connection-password") {
window->set_connection_status_text(crossdesk::ui_localization::Text(
crossdesk::localization::reinput_password[language]));
window->set_connection_password_required(true);
window->set_connection_dialog_open(true);
} else if (options.page == "service") {
window->set_portable_service_settings_visible(true);
window->set_portable_service_dialog_open(true);
} else if (options.page == "service-notice") {
window->set_portable_service_settings_visible(true);
window->set_portable_service_suppressed_notice_open(true);
} else if (options.page == "permission") {
window->set_permission_dialog_open(true);
window->set_screen_recording_granted(false);
window->set_accessibility_granted(false);
}
}
slint::Image CreateStreamPreviewImage() {
constexpr int preview_width = 640;
constexpr int preview_height = 360;
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(preview_width,
preview_height);
auto *data = reinterpret_cast<uint8_t *>(pixels.begin());
for (int y = 0; y < preview_height; ++y) {
for (int x = 0; x < preview_width; ++x) {
const bool border = x < 8 || y < 8 || x >= preview_width - 8 ||
y >= preview_height - 8;
const size_t offset = (static_cast<size_t>(y) * preview_width + x) * 3;
data[offset] =
border ? 240 : static_cast<uint8_t>(35 + 150 * x / preview_width);
data[offset + 1] =
border ? 70 : static_cast<uint8_t>(35 + 150 * y / preview_height);
data[offset + 2] = border ? 70 : 90;
}
}
return slint::Image(std::move(pixels));
}
void ConfigureStreamCapturePage(
const slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
int language) {
language = crossdesk::ui_localization::ApplyStreamWindowStrings(
stream, language);
#if defined(__linux__)
stream->set_custom_titlebar(
HasNonEmptyEnvironmentVariable("DISPLAY") &&
HasNonEmptyEnvironmentVariable("WAYLAND_DISPLAY"));
#else
stream->set_custom_titlebar(false);
#endif
stream->set_native_video_enabled(false);
stream->set_window_maximized(false);
stream->set_fullscreen_enabled(false);
stream->set_stats_visible(false);
stream->set_file_transfer_visible(false);
stream->set_receiving_text("");
stream->set_status_text("");
crossdesk::ui::StreamTab tab;
tab.remote_id = "589173341";
tab.title = "Mac";
tab.connected = true;
stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab}));
stream->set_displays(
std::make_shared<slint::VectorModel<slint::SharedString>>(
std::vector{slint::SharedString("Display 1")}));
stream->set_frame(CreateStreamPreviewImage());
stream->set_has_frame(true);
const float height = stream->get_custom_titlebar() ? 752.0f : 720.0f;
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, height}));
}
void ConfigureServerCapturePage(
const slint::ComponentHandle<crossdesk::ui::ServerWindow> &server,
int language) {
language = crossdesk::localization::detail::ClampLanguageIndex(language);
crossdesk::ui::ControllerEntry controller;
controller.remote_id = "589173341";
controller.display_name = "Mac";
server->set_controllers(
std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
std::vector{controller}));
server->set_controller_names(
std::make_shared<slint::VectorModel<slint::SharedString>>(
std::vector{slint::SharedString("Mac")}));
server->set_language_index(language);
server->set_controller_label(crossdesk::ui_localization::Text(
crossdesk::localization::controller[language]));
server->set_connection_label(crossdesk::ui_localization::Text(
crossdesk::localization::connection_status[language]));
server->set_connection_status(crossdesk::ui_localization::Text(
crossdesk::localization::p2p_connected[language]));
server->set_file_transfer_label(crossdesk::ui_localization::Text(
crossdesk::localization::file_transfer[language]));
server->set_select_file_label(crossdesk::ui_localization::Text(
crossdesk::localization::select_file[language]));
server->set_file_transfer_visible(false);
server->set_sending_file(false);
const float width = language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
server->window().set_size(
slint::LogicalSize(slint::Size<float>{width, 150.0f}));
}
int RunCaptureMode(
const CaptureOptions &options,
slint::ComponentHandle<crossdesk::ui::MainWindow> &window,
slint::ComponentHandle<crossdesk::ui::StreamWindow> &stream,
slint::ComponentHandle<crossdesk::ui::ServerWindow> &server) {
ConfigureMainCapturePage(window, options);
if (!ConfigureDocumentationDemo(window, options)) {
return 8;
}
stream->hide();
server->hide();
slint::Window *capture_window = &window->window();
if (options.page == "stream") {
ConfigureStreamCapturePage(stream, options.language);
window->hide();
stream->show();
capture_window = &stream->window();
} else if (options.page == "server") {
ConfigureServerCapturePage(server, options.language);
window->hide();
server->show();
capture_window = &server->window();
} else {
window->show();
}
if (!options.snapshot_path.empty()) {
return WriteWindowSnapshot(*capture_window, options.snapshot_path.c_str())
? 0
: 7;
}
if (options.page == "stream" || options.page == "server") {
slint::run_event_loop();
} else {
window->run();
}
return 0;
}
} // namespace
int main() {
const CaptureOptions capture_options = LoadCaptureOptions();
auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create();
if (!RegisterFontAwesome(window->window()) ||
!RegisterFontAwesome(stream->window()) ||
!RegisterFontAwesome(server->window())) {
return 1;
}
window->set_local_id("123 456 789");
window->set_local_password("123456");
window->set_connection_dialog_open(true);
window->set_connection_password_required(true);
window->set_custom_titlebar(true);
window->set_wayland_titlebar(true);
window->set_settings_session_active(true);
window->set_hardware_codec_available(false);
window->set_portable_service_settings_visible(true);
assert(std::string(window->get_local_id()) == "123 456 789");
assert(window->get_connection_dialog_open());
assert(window->get_connection_password_required());
assert(window->get_custom_titlebar());
assert(window->get_wayland_titlebar());
assert(window->get_settings_session_active());
assert(!window->get_hardware_codec_available());
assert(window->get_portable_service_settings_visible());
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 2;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
window->set_window_active(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
window->set_window_active(true);
if (!snapshot_written) {
return 3;
}
}
window->set_settings_open(true);
window->set_about_open(true);
assert(window->get_settings_open());
assert(window->get_about_open());
if (const char *snapshot_path =
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
window->set_about_open(false);
window->set_connection_dialog_open(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 6;
}
window->set_connection_dialog_open(true);
window->set_about_open(true);
}
window->set_remote_id_input("987654321");
window->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty());
std::vector<crossdesk::ui::RecentConnection> connections;
crossdesk::ui::RecentConnection connection;
connection.remote_id = "987654321";
connection.display_name = "Office PC";
connection.host_name = "office-pc";
connection.online = true;
connections.emplace_back(std::move(connection));
window->set_recent_connections(
std::make_shared<slint::VectorModel<crossdesk::ui::RecentConnection>>(
std::move(connections)));
assert(window->get_recent_connections()->row_count() == 1);
bool connect_requested = false;
window->on_connect_requested([&](slint::SharedString remote_id) {
connect_requested = std::string(remote_id) == "987654321";
});
window->invoke_connect_requested("987654321");
assert(connect_requested);
bool password_submitted = false;
window->on_connection_submit_password(
[&](slint::SharedString password, bool remember) {
password_submitted = std::string(password) == "654321" && remember;
});
window->invoke_connection_submit_password("654321", true);
assert(password_submitted);
crossdesk::ui::StreamTab tab;
tab.remote_id = "123456789";
tab.title = "Remote host";
tab.connected = true;
stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_native_video_enabled(true);
stream->set_window_maximized(true);
assert(stream->get_tabs()->row_count() == 1);
assert(stream->get_custom_titlebar());
assert(stream->get_native_video_enabled());
assert(stream->get_window_maximized());
// Keep snapshots on the CPU-rendered UI path; the native Metal video view
// is intentionally outside Slint's component snapshot.
stream->set_native_video_enabled(false);
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_maximized(true);
if (!snapshot_written) {
return 4;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->set_window_active(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_active(true);
stream->set_window_maximized(true);
if (!snapshot_written) {
return 5;
}
}
bool tab_reordered = false;
stream->on_reorder_tab([&](int index, float x, float width) {
tab_reordered = index == 0 && x == 120.0f && width == 110.0f;
});
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
assert(tab_reordered);
bool maximize_requested = false;
stream->on_toggle_maximize_stream_window(
[&] { maximize_requested = true; });
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip";
transfer.status = "Sending";
transfer.progress = 0.5f;
transfer.speed = "1.0 mbps";
transfer.size = "2.00 MB";
stream->set_file_transfers(
std::make_shared<
slint::VectorModel<crossdesk::ui::FileTransferEntry>>(
std::vector{transfer}));
stream->set_file_transfer_visible(true);
crossdesk::ui::NetworkStatsRow video_stats;
video_stats.label = "Video";
video_stats.inbound = "427 kbps";
video_stats.outbound = "5 kbps";
video_stats.loss_rate = "0%";
stream->set_stats_rows(
std::make_shared<
slint::VectorModel<crossdesk::ui::NetworkStatsRow>>(
std::vector{video_stats}));
stream->set_stats_fps("53");
stream->set_stats_resolution("3024x1964");
stream->set_stats_connection_mode("Direct");
stream->set_stats_visible(true);
stream->set_fullscreen_enabled(true);
assert(stream->get_file_transfers()->row_count() == 1);
assert(stream->get_file_transfer_visible());
assert(stream->get_stats_rows()->row_count() == 1);
assert(std::string(stream->get_stats_fps()) == "53");
assert(std::string(stream->get_stats_resolution()) == "3024x1964");
assert(std::string(stream->get_stats_connection_mode()) == "Direct");
assert(stream->get_stats_visible());
assert(stream->get_fullscreen_enabled());
bool display_switched = false;
stream->on_switch_display(
[&](int index) { display_switched = index == 1; });
stream->invoke_switch_display(1);
assert(display_switched);
auto &stream_strings = stream->global<crossdesk::ui::StreamStrings>();
stream_strings.set_select_display("Select Display");
stream_strings.set_expand_control("Expand Control Bar");
assert(std::string(stream_strings.get_select_display()) == "Select Display");
assert(std::string(stream_strings.get_expand_control()) ==
"Expand Control Bar");
crossdesk::ui::ControllerEntry controller;
controller.remote_id = "123456789";
controller.display_name = "Remote host";
server->set_controllers(
std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
std::vector{controller}));
server->set_file_transfer_visible(true);
server->set_sending_file(true);
server->set_file_progress(0.5f);
server->set_current_file_name("archive.zip");
server->set_file_size_text("1.00 MB / 2.00 MB");
assert(server->get_controllers()->row_count() == 1);
assert(server->get_file_transfer_visible());
assert(server->get_sending_file());
assert(server->get_file_progress() == 0.5f);
bool controller_selected = false;
server->on_controller_selected(
[&](int index) { controller_selected = index == 0; });
server->invoke_controller_selected(0);
assert(controller_selected);
if (capture_options.enabled) {
return RunCaptureMode(capture_options, window, stream, server);
}
return 0;
}
-424
View File
@@ -1,424 +0,0 @@
function setup_targets()
add_packages("spdlog", "libsdl3", "nlohmann_json")
includes("deps/submodules", "deps/thirdparty")
local function copy_slint_runtime(target)
if not target:is_plat("windows") then
return
end
local slint = target:pkg("slint")
if not slint and target:dep("gui") then
slint = target:dep("gui"):pkg("slint")
end
assert(slint, "the Slint package is required to copy its Windows runtime")
local runtime_dir = path.join(slint:installdir(), "lib")
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
os.cp(runtime_dll, target:targetdir())
end
local crossdesk_windows_resource = "apps/desktop/resources/windows/crossdesk.rc"
if is_config("CROSSDESK_PORTABLE", true) then
crossdesk_windows_resource = "apps/desktop/resources/windows/crossdesk_portable.rc"
end
target("rd_log")
set_kind("object")
add_packages("spdlog")
add_files("apps/desktop/src/log/rd_log.cpp")
add_includedirs("apps/desktop/src/log", {public = true})
target("common")
set_kind("object")
add_deps("rd_log", "crossdesk_wire")
add_packages("libyuv")
add_packages("concurrentqueue", {public = true})
add_files("apps/desktop/src/common/*.cpp")
remove_files("apps/desktop/src/common/rounded_corner_button.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/system_info.cpp")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/system_info.cpp")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/system_info.cpp",
"apps/desktop/src/platform/linux/common/*.cpp")
add_includedirs("apps/desktop/src/platform/linux/common")
end
add_includedirs("apps/desktop/src/common", {public = true})
target("path_manager")
set_kind("object")
add_deps("rd_log")
add_files("apps/desktop/src/path_manager/*.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/path_backend.cpp")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/path_backend.cpp")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/path_backend.cpp")
end
add_includedirs("apps/desktop/src", {public = true})
add_includedirs("apps/desktop/src/path_manager", {public = true})
target("path_manager_portable_test")
set_kind("binary")
set_default(false)
set_policy("build.ccache", false)
add_defines("CROSSDESK_PORTABLE=1")
add_includedirs("apps/desktop/src", "apps/desktop/src/path_manager")
add_files("apps/desktop/tests/path_manager_portable_test.cpp",
"apps/desktop/src/path_manager/path_manager.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/path_backend.cpp")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/path_backend.cpp")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/path_backend.cpp")
end
target("macos_keyboard_modifier_state_test")
set_kind("binary")
set_default(false)
add_includedirs("apps/desktop/src/platform/macos/input")
add_files("apps/desktop/tests/macos_keyboard_modifier_state_test.cpp")
target("cursor_position_test")
set_kind("binary")
set_default(false)
add_deps("crossdesk_wire")
add_includedirs("apps/desktop/src/common",
"apps/desktop/src/gui/runtime")
add_files("apps/desktop/tests/cursor_position_test.cpp")
target("repository_structure_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/repository_structure_test.cpp")
target("connection_status_protocol_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/connection_status_protocol_test.cpp")
target("video_callback_lifetime_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/video_callback_lifetime_test.cpp")
target("windows_manifest_resource_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/windows_manifest_resource_test.cpp")
target("windows_service_mouse_ipc_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/windows_service_mouse_ipc_test.cpp")
target("windows_mouse_controller_safety_test")
set_kind("binary")
set_default(false)
add_files("apps/desktop/tests/windows_mouse_controller_safety_test.cpp")
target("windows_sas_guard_test")
set_kind("binary")
set_default(false)
add_includedirs("apps/desktop/src/platform/windows/service")
add_files("apps/desktop/tests/windows_sas_guard_test.cpp")
target("slint_ui_smoke_test")
set_kind("binary")
set_languages("c++20")
set_default(false)
if is_os("windows") then
add_cxxflags("/bigobj")
end
add_packages("slint")
add_includedirs("apps/desktop/src/gui", "apps/desktop/src/gui/assets/fonts",
"apps/desktop/src/gui/assets/localization")
add_rules("slint")
add_files("apps/desktop/src/gui/ui/crossdesk_ui.slint")
add_files("apps/desktop/tests/slint_ui_smoke_test.cpp")
after_build(copy_slint_runtime)
target("version_checker_test")
set_kind("binary")
set_default(false)
add_packages("cpp-httplib")
add_deps("rd_log")
add_includedirs("apps/desktop/src/version_checker")
add_files("apps/desktop/tests/version_checker_test.cpp",
"apps/desktop/src/version_checker/version_checker.cpp")
if is_os("macosx") then
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
add_frameworks("Security", "CoreFoundation")
end
target("screen_capturer")
set_kind("object")
add_deps("rd_log", "common", "crossdesk_wire")
add_includedirs("apps/desktop/src/screen_capturer", {public = true})
add_includedirs("deps/submodules/minirtc/src/api", {public = true})
if is_os("windows") then
add_packages("libyuv")
add_files("apps/desktop/src/screen_capturer/captured_nv12_frame.cpp")
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_gdi.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp",
"apps/desktop/src/platform/windows/screen_capturer/screen_capturer_factory.cpp")
add_includedirs("apps/desktop/src/platform/windows/screen_capturer",
"apps/desktop/src/platform/windows/service")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/screen_capturer/*.cpp",
"apps/desktop/src/platform/macos/screen_capturer/*.mm")
add_includedirs("apps/desktop/src/platform/macos/screen_capturer")
elseif is_os("linux") then
add_packages("libyuv")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_drm.cpp")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_factory.cpp")
if is_config("USE_WAYLAND", true) then
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_wayland.cpp")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_wayland_portal.cpp")
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_wayland_pipewire.cpp")
end
add_includedirs("apps/desktop/src/platform/linux/screen_capturer",
"apps/desktop/src/platform/linux/common")
end
target("speaker_capturer")
set_kind("object")
add_deps("rd_log", "crossdesk_wire")
add_includedirs("apps/desktop/src/speaker_capturer", {public = true})
if is_os("windows") then
add_packages("miniaudio")
add_files("apps/desktop/src/platform/windows/speaker_capturer/*.cpp")
add_includedirs("apps/desktop/src/platform/windows/speaker_capturer")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/speaker_capturer/*.cpp",
"apps/desktop/src/platform/macos/speaker_capturer/*.mm")
add_includedirs("apps/desktop/src/platform/macos/speaker_capturer")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/speaker_capturer/*.cpp")
add_includedirs("apps/desktop/src/platform/linux/speaker_capturer")
end
target("device_controller")
set_kind("object")
add_deps("rd_log", "common", "crossdesk_wire")
add_includedirs("apps/desktop/src/device_controller", {public = true})
add_includedirs("apps/desktop/src/platform/common/input")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/input/mouse/*.cpp",
"apps/desktop/src/platform/windows/input/keyboard/*.cpp",
"apps/desktop/src/platform/windows/input/device_controller_factory.cpp")
add_includedirs("apps/desktop/src/platform/windows/input/mouse",
"apps/desktop/src/platform/windows/input/keyboard",
"apps/desktop/src/platform/windows/input")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/input/mouse/*.cpp",
"apps/desktop/src/platform/macos/input/keyboard/*.cpp",
"apps/desktop/src/platform/macos/input/device_controller_factory.cpp")
add_includedirs("apps/desktop/src/platform/macos/input/mouse",
"apps/desktop/src/platform/macos/input/keyboard",
"apps/desktop/src/platform/macos/input")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/input/mouse/*.cpp",
"apps/desktop/src/platform/linux/input/keyboard/*.cpp",
"apps/desktop/src/platform/linux/input/device_controller_factory.cpp")
add_includedirs("apps/desktop/src/platform/linux/input/mouse",
"apps/desktop/src/platform/linux/input/keyboard",
"apps/desktop/src/platform/linux/input",
"apps/desktop/src/platform/linux/common")
end
if is_os("linux") then
target("linux_keyboard_x11_integration_test")
set_kind("binary")
set_default(false)
add_deps("device_controller")
add_files("apps/desktop/tests/linux_keyboard_x11_integration_test.cpp")
end
target("thumbnail")
set_kind("object")
add_packages("libyuv", "openssl3")
add_deps("rd_log", "common")
add_files("apps/desktop/src/thumbnail/*.cpp")
add_includedirs("apps/desktop/src/thumbnail", {public = true})
target("autostart")
set_kind("object")
add_deps("rd_log")
add_files("apps/desktop/src/autostart/*.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/autostart.cpp")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/autostart.cpp")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/autostart.cpp")
end
add_includedirs("apps/desktop/src")
add_includedirs("apps/desktop/src/autostart", {public = true})
target("config_center")
set_kind("object")
add_deps("rd_log", "autostart")
add_files("apps/desktop/src/config_center/*.cpp")
add_includedirs("apps/desktop/src/config_center", {public = true})
target("assets")
set_kind("headeronly")
add_includedirs("apps/desktop/src/gui/assets/localization",
"apps/desktop/src/gui/assets/fonts",
"apps/desktop/src/gui/assets/icons",
"apps/desktop/src/gui/assets/layouts", {public = true})
target("version_checker")
set_kind("object")
add_packages("cpp-httplib")
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
add_deps("rd_log")
add_files("apps/desktop/src/version_checker/*.cpp")
add_includedirs("apps/desktop/src/version_checker", {public = true})
if is_os("macosx") then
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
add_frameworks("Security", "CoreFoundation")
end
target("tools")
set_kind("object")
add_deps("rd_log", "common", "crossdesk_wire")
add_files("apps/desktop/src/tools/*.cpp")
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/clipboard.cpp")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/clipboard.mm")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/linux/clipboard.cpp")
end
add_includedirs("apps/desktop/src", "apps/desktop/src/tools", {public = true})
target("gui")
set_kind("object")
set_languages("c++20")
-- spdlog 1.14 bundles fmt 10, whose consteval parser is rejected by
-- current Apple Clang in C++20 mode. Keep the established dependency
-- version and use fmt's supported runtime-parser fallback here.
add_defines("FMT_CONSTEVAL=")
add_packages("slint", {public = true})
add_packages("libyuv", "tinyfiledialogs")
add_rules("slint")
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
"path_manager", "screen_capturer", "speaker_capturer",
"device_controller", "thumbnail", "version_checker", "tools",
"crossdesk_wire")
add_files("apps/desktop/src/gui/render.cpp", "apps/desktop/src/gui/application/gui_application.cpp",
"apps/desktop/src/gui/rendering/*.cpp",
"apps/desktop/src/gui/runtime/*.cpp",
"apps/desktop/src/platform/common/gui/service_status_runtime.cpp",
"apps/desktop/src/gui/features/devices/*.cpp", "apps/desktop/src/gui/features/input/*.cpp",
"apps/desktop/src/gui/features/clipboard/*.cpp", "apps/desktop/src/gui/features/file_transfer/*.cpp",
"apps/desktop/src/gui/features/settings/*.cpp", "apps/desktop/src/gui/ui/crossdesk_ui.slint")
add_includedirs("apps/desktop/src", "apps/desktop/src/gui", {public = true})
add_includedirs("apps/desktop/src/platform/common/input")
if is_os("windows") then
add_cxxflags("/bigobj")
add_links("opengl32")
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
"apps/desktop/src/platform/windows/gui/slint_backend.cpp",
"apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp",
"apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp",
"apps/desktop/src/platform/windows/gui/application/portable_service_integration.cpp")
add_includedirs("apps/desktop/src/platform/common/gui",
"apps/desktop/src/platform/windows/gui/tray")
add_includedirs("apps/desktop/src/platform/windows/service")
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/gui/runtime/*.mm",
"apps/desktop/src/platform/macos/gui/tray/*.mm",
"apps/desktop/src/platform/macos/gui/window_drag_mac.mm",
"apps/desktop/src/platform/macos/gui/metal_video_renderer.mm",
"apps/desktop/src/platform/macos/gui/video_renderer_factory_metal.mm")
add_includedirs("apps/desktop/src/platform/macos/gui",
"apps/desktop/src/platform/macos/gui/tray")
elseif is_os("linux") then
add_links("GL")
add_files("apps/desktop/src/platform/linux/gui/tray/linux_tray.cpp",
"apps/desktop/src/platform/linux/gui/cuda_graphics.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",
"apps/desktop/src/platform/common/gui/cuda_opengl_interop.cpp",
"apps/desktop/src/platform/common/gui/video_renderer_factory_opengl.cpp")
add_includedirs("apps/desktop/src/platform/common/gui",
"apps/desktop/src/platform/linux/gui/tray",
"apps/desktop/src/platform/linux/common")
end
if is_os("windows") then
target("wgc_plugin")
set_kind("shared")
add_packages("libyuv")
add_deps("rd_log", "path_manager", "crossdesk_wire")
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
-- Keep the project on C++17 while C++/WinRT still falls back to
-- MSVC's deprecated experimental coroutine header.
add_defines("_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS")
add_links("windowsapp")
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp",
"apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp",
"apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp")
add_files("apps/desktop/resources/windows/wgc_plugin.rc")
add_includedirs("apps/desktop/src/common", "apps/desktop/src/screen_capturer",
"apps/desktop/src/platform/windows/screen_capturer",
"deps/submodules/minirtc/src/api")
target("crossdesk_service")
set_kind("binary")
add_deps("rd_log", "path_manager")
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_files("apps/desktop/src/platform/windows/service/main.cpp",
"apps/desktop/src/platform/windows/service/service_host.cpp")
add_files("apps/desktop/resources/windows/crossdesk_service.rc")
add_includedirs("apps/desktop/src/platform/windows/service")
target("crossdesk_session_helper")
set_kind("binary")
add_packages("libyuv")
add_deps("rd_log", "path_manager")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc")
add_includedirs("apps/desktop/src/common",
"apps/desktop/src/platform/windows/service")
add_includedirs("apps/desktop/src/platform/windows/input")
end
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("apps/desktop/src/app/*.cpp")
add_includedirs("apps/desktop/src", "apps/desktop/src/app", {public = true})
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/daemon_backend.cpp")
add_files("apps/desktop/src/platform/windows/service/service_host.cpp")
add_includedirs("apps/desktop/src/platform/windows/service")
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files(crossdesk_windows_resource)
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/common/posix_daemon_backend.cpp",
"apps/desktop/src/platform/macos/daemon_backend.cpp")
elseif is_os("linux") then
add_files("apps/desktop/src/platform/common/posix_daemon_backend.cpp",
"apps/desktop/src/platform/linux/daemon_backend.cpp")
end
after_build(copy_slint_runtime)
end
@@ -1,384 +0,0 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 60;
objects = {
/* Begin PBXBuildFile section */
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000001 /* CrossDeskMobileApp.swift */; };
100000000000000000000002 /* RemoteSessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000002 /* RemoteSessionModel.swift */; };
100000000000000000000003 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000003 /* ContentView.swift */; };
100000000000000000000004 /* RemoteSessionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000004 /* RemoteSessionView.swift */; };
100000000000000000000005 /* CrossDeskRTCBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000006 /* CrossDeskRTCBridge.mm */; };
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000009 /* NativeMetalVideoView.swift */; };
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000A /* RemoteTouchInputView.swift */; };
100000000000000000000008 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000D /* Assets.xcassets */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
200000000000000000000001 /* CrossDeskMobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossDeskMobileApp.swift; sourceTree = "<group>"; };
200000000000000000000002 /* RemoteSessionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSessionModel.swift; sourceTree = "<group>"; };
200000000000000000000003 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
200000000000000000000004 /* RemoteSessionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSessionView.swift; sourceTree = "<group>"; };
200000000000000000000005 /* CrossDeskRTCBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrossDeskRTCBridge.h; sourceTree = "<group>"; };
200000000000000000000006 /* CrossDeskRTCBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CrossDeskRTCBridge.mm; sourceTree = "<group>"; };
200000000000000000000007 /* CrossDeskMobile-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CrossDeskMobile-Bridging-Header.h"; sourceTree = "<group>"; };
200000000000000000000008 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
200000000000000000000009 /* NativeMetalVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeMetalVideoView.swift; sourceTree = "<group>"; };
20000000000000000000000A /* RemoteTouchInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTouchInputView.swift; sourceTree = "<group>"; };
20000000000000000000000B /* CrossDeskMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CrossDeskMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
20000000000000000000000C /* build_minirtc_ios.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_minirtc_ios.sh; sourceTree = "<group>"; };
20000000000000000000000D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
300000000000000000000001 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
400000000000000000000001 = {
isa = PBXGroup;
children = (
400000000000000000000002 /* CrossDeskMobile */,
400000000000000000000006 /* scripts */,
400000000000000000000007 /* Products */,
);
sourceTree = "<group>";
};
400000000000000000000002 /* CrossDeskMobile */ = {
isa = PBXGroup;
children = (
200000000000000000000001 /* CrossDeskMobileApp.swift */,
200000000000000000000002 /* RemoteSessionModel.swift */,
200000000000000000000003 /* ContentView.swift */,
200000000000000000000004 /* RemoteSessionView.swift */,
400000000000000000000003 /* Bridge */,
400000000000000000000004 /* Rendering */,
400000000000000000000005 /* Input */,
20000000000000000000000D /* Assets.xcassets */,
200000000000000000000007 /* CrossDeskMobile-Bridging-Header.h */,
200000000000000000000008 /* Info.plist */,
);
path = CrossDeskMobile;
sourceTree = "<group>";
};
400000000000000000000003 /* Bridge */ = {
isa = PBXGroup;
children = (
200000000000000000000005 /* CrossDeskRTCBridge.h */,
200000000000000000000006 /* CrossDeskRTCBridge.mm */,
);
path = Bridge;
sourceTree = "<group>";
};
400000000000000000000004 /* Rendering */ = {
isa = PBXGroup;
children = (
200000000000000000000009 /* NativeMetalVideoView.swift */,
);
path = Rendering;
sourceTree = "<group>";
};
400000000000000000000005 /* Input */ = {
isa = PBXGroup;
children = (
20000000000000000000000A /* RemoteTouchInputView.swift */,
);
path = Input;
sourceTree = "<group>";
};
400000000000000000000006 /* scripts */ = {
isa = PBXGroup;
children = (
20000000000000000000000C /* build_minirtc_ios.sh */,
);
path = scripts;
sourceTree = "<group>";
};
400000000000000000000007 /* Products */ = {
isa = PBXGroup;
children = (
20000000000000000000000B /* CrossDeskMobile.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
500000000000000000000001 /* CrossDeskMobile */ = {
isa = PBXNativeTarget;
buildConfigurationList = 900000000000000000000002 /* Build configuration list for PBXNativeTarget "CrossDeskMobile" */;
buildPhases = (
700000000000000000000001 /* Build native MiniRTC */,
600000000000000000000001 /* Sources */,
300000000000000000000001 /* Frameworks */,
600000000000000000000002 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = CrossDeskMobile;
productName = CrossDeskMobile;
productReference = 20000000000000000000000B /* CrossDeskMobile.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
800000000000000000000001 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2660;
LastUpgradeCheck = 2660;
TargetAttributes = {
500000000000000000000001 = {
CreatedOnToolsVersion = 26.6;
};
};
};
buildConfigurationList = 900000000000000000000001 /* Build configuration list for PBXProject "CrossDeskMobile" */;
compatibilityVersion = "Xcode 15.0";
developmentRegion = zh_CN;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
zh_CN,
);
mainGroup = 400000000000000000000001;
productRefGroup = 400000000000000000000007 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
500000000000000000000001 /* CrossDeskMobile */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
600000000000000000000002 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
100000000000000000000008 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
700000000000000000000001 /* Build native MiniRTC */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Build native MiniRTC";
outputFileListPaths = (
);
outputPaths = (
"$(PROJECT_DIR)/Vendor/iphoneos/$(CONFIGURATION)/libCrossDeskMiniRTC.a",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
600000000000000000000001 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */,
100000000000000000000002 /* RemoteSessionModel.swift in Sources */,
100000000000000000000003 /* ContentView.swift in Sources */,
100000000000000000000004 /* RemoteSessionView.swift in Sources */,
100000000000000000000005 /* CrossDeskRTCBridge.mm in Sources */,
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */,
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A00000000000000000000001 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
A00000000000000000000002 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
GCC_C_LANGUAGE_STANDARD = gnu17;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
A00000000000000000000003 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = 8DNC3SZ86S;
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
"$(PROJECT_DIR)/../../libs/wire/include",
);
INFOPLIST_FILE = CrossDeskMobile/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 0.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"$(PROJECT_DIR)/Vendor/iphoneos/$(CONFIGURATION)/libCrossDeskMiniRTC.a",
"-liconv",
"-lresolv",
"-framework",
VideoToolbox,
"-framework",
CoreMedia,
"-framework",
CoreVideo,
"-framework",
Security,
"-framework",
SystemConfiguration,
"-framework",
CoreFoundation,
"-framework",
Foundation,
);
PRODUCT_BUNDLE_IDENTIFIER = cn.crossdesk.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "CrossDeskMobile/CrossDeskMobile-Bridging-Header.h";
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A00000000000000000000004 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = 8DNC3SZ86S;
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
"$(PROJECT_DIR)/../../libs/wire/include",
);
INFOPLIST_FILE = CrossDeskMobile/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MARKETING_VERSION = 0.0.1;
OTHER_LDFLAGS = (
"$(inherited)",
"$(PROJECT_DIR)/Vendor/iphoneos/$(CONFIGURATION)/libCrossDeskMiniRTC.a",
"-liconv",
"-lresolv",
"-framework",
VideoToolbox,
"-framework",
CoreMedia,
"-framework",
CoreVideo,
"-framework",
Security,
"-framework",
SystemConfiguration,
"-framework",
CoreFoundation,
"-framework",
Foundation,
);
PRODUCT_BUNDLE_IDENTIFIER = cn.crossdesk.mobile;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_OBJC_BRIDGING_HEADER = "CrossDeskMobile/CrossDeskMobile-Bridging-Header.h";
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
900000000000000000000001 /* Build configuration list for PBXProject "CrossDeskMobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A00000000000000000000001 /* Debug */,
A00000000000000000000002 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
900000000000000000000002 /* Build configuration list for PBXNativeTarget "CrossDeskMobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A00000000000000000000003 /* Debug */,
A00000000000000000000004 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 800000000000000000000001 /* Project object */;
}
@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2660"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "500000000000000000000001"
BuildableName = "CrossDeskMobile.app"
BlueprintName = "CrossDeskMobile"
ReferencedContainer = "container:CrossDeskMobile.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "500000000000000000000001"
BuildableName = "CrossDeskMobile.app"
BlueprintName = "CrossDeskMobile"
ReferencedContainer = "container:CrossDeskMobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "500000000000000000000001"
BuildableName = "CrossDeskMobile.app"
BlueprintName = "CrossDeskMobile"
ReferencedContainer = "container:CrossDeskMobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -1,14 +0,0 @@
{
"images" : [
{
"filename" : "CrossDeskAppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,132 +0,0 @@
#import <CoreVideo/CoreVideo.h>
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, CrossDeskSignalState) {
CrossDeskSignalStateConnecting = 0,
CrossDeskSignalStateConnected,
CrossDeskSignalStateFailed,
CrossDeskSignalStateClosed,
CrossDeskSignalStateReconnecting,
CrossDeskSignalStateServerClosed,
CrossDeskSignalStateTLSCertificateError,
};
typedef NS_ENUM(NSInteger, CrossDeskConnectionState) {
CrossDeskConnectionStateConnecting = 0,
CrossDeskConnectionStateConnected,
CrossDeskConnectionStateGathering,
CrossDeskConnectionStateDisconnected,
CrossDeskConnectionStateFailed,
CrossDeskConnectionStateClosed,
CrossDeskConnectionStateIncorrectPassword,
CrossDeskConnectionStateNoSuchID,
CrossDeskConnectionStateRemoteUnavailable,
};
typedef NS_ENUM(NSInteger, CrossDeskPointerAction) {
CrossDeskPointerActionMove = 0,
CrossDeskPointerActionLeftDown,
CrossDeskPointerActionLeftUp,
CrossDeskPointerActionRightDown,
CrossDeskPointerActionRightUp,
CrossDeskPointerActionMiddleDown,
CrossDeskPointerActionMiddleUp,
};
typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
CrossDeskVideoAdaptationPolicyFrameRatePriority = 0,
CrossDeskVideoAdaptationPolicyQualityPriority,
CrossDeskVideoAdaptationPolicyBalanced,
};
@class CrossDeskRTCBridge;
@protocol CrossDeskRTCBridgeDelegate <NSObject>
@optional
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didChangeSignalState:(CrossDeskSignalState)state;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didChangeConnectionState:(CrossDeskConnectionState)state
remoteID:(NSString *)remoteID;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didProvisionIdentity:(NSString *)identity;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceivePixelBuffer:(CVPixelBufferRef)pixelBuffer
width:(NSInteger)width
height:(NSInteger)height;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveHostName:(NSString *)hostName
displayNames:(NSArray<NSString *> *)displayNames
displaySizes:(NSArray<NSValue *> *)displaySizes;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceivePresence:(NSDictionary<NSString *, NSNumber *> *)presence
snapshot:(BOOL)snapshot;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveCursorVisible:(BOOL)visible
shape:(NSInteger)shape
positionUpdate:(BOOL)positionUpdate
positionValid:(BOOL)positionValid
x:(float)x
y:(float)y
visualOffsetX:(float)visualOffsetX
visualOffsetY:(float)visualOffsetY
displayIndex:(NSInteger)displayIndex
sequence:(uint32_t)sequence;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveAudioPCM:(NSData *)pcmData;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveClipboardText:(NSString *)text;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didUpdateFileTransfer:(NSString *)fileName
progress:(double)progress
sending:(BOOL)sending;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveFileAtURL:(NSURL *)fileURL;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didUpdateBitrate:(NSUInteger)bitsPerSecond
lossRate:(float)lossRate
usingTURN:(BOOL)usingTURN;
@end
/// Objective-C++ boundary around the native MiniRTC C API.
///
/// All MiniRTC ownership and calls are serialized internally. Delegate methods
/// are delivered on the main queue so SwiftUI never touches callback threads.
@interface CrossDeskRTCBridge : NSObject
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
- (void)configureWithSignalHost:(NSString *)host
signalPort:(NSInteger)signalPort
turnPort:(NSInteger)turnPort
enableSRTP:(BOOL)enableSRTP;
- (void)setHardwareAccelerationEnabled:(BOOL)enabled;
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
subscribe:(BOOL)subscribe
NS_SWIFT_NAME(requestPresence(remoteIDs:subscribe:));
- (void)invalidatePresence;
- (void)connectToRemoteID:(NSString *)remoteID password:(NSString *)password;
- (void)disconnect;
- (void)requestKeyFrame;
- (void)sendPointerAtX:(float)x
y:(float)y
action:(CrossDeskPointerAction)action
NS_SWIFT_NAME(sendPointer(x:y:action:));
- (void)sendScrollX:(float)x y:(float)y deltaX:(NSInteger)deltaX
deltaY:(NSInteger)deltaY;
- (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown;
- (void)sendText:(NSString *)text;
- (void)switchToDisplay:(NSInteger)displayIndex;
- (void)setAudioEnabled:(BOOL)enabled;
- (void)sendClipboardText:(NSString *)text;
- (void)sendFileAtURL:(NSURL *)fileURL;
- (void)sendSecureAttentionSequence;
@end
NS_ASSUME_NONNULL_END
File diff suppressed because it is too large Load Diff

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