Compare commits

...

42 Commits

Author SHA1 Message Date
dijunkun
07c7c7f179 [chore] update README 2025-11-21 02:40:51 +08:00
dijunkun
c5ceeb0d80 [ci] use China timezone for build date in version number 2025-11-21 02:33:39 +08:00
dijunkun
5ce0a891df [fix] resolve failures in connection destruction 2025-11-21 01:50:08 +08:00
dijunkun
f94ef49210 [fix] release keyboard hook after remote control disconnects, refs #23 2025-11-21 00:56:01 +08:00
dijunkun
5d0a4d1385 [fix] fix mouse wheel and touchpad swipe commands, refs #9, #23 2025-11-21 00:55:19 +08:00
dijunkun
dd482cee60 [fix] use static linking of libffi in glib to avoid version conflicts, fixes #16 2025-11-20 17:20:15 +08:00
dijunkun
e3c2edfb1c [fix] fix daemon not working on Linux 2025-11-20 15:35:37 +08:00
dijunkun
f3901d09ea [feat] add tooltip for the daemon option in the settings windows 2025-11-20 14:47:54 +08:00
Junkun Di
2b12749477 [chore] update README_EN.md 2025-11-20 13:05:14 +08:00
Junkun Di
759488f675 [chore] update README.md 2025-11-20 13:04:32 +08:00
dijunkun
4bb4240a9e [chore] update README 2025-11-20 11:15:34 +08:00
dijunkun
1457247a6a [feat] add build option USE_CUDA to enable or disable CUDA-based hardware codec acceleration and USE_CUDA=false by default 2025-11-19 23:15:56 +08:00
dijunkun
97ab9bfca5 [feat] add daemon support with automatic restart on crash 2025-11-19 22:09:51 +08:00
dijunkun
4dd3c3e073 [fix] clean display names by removing non-alphanumeric characters 2025-11-18 17:05:51 +08:00
dijunkun
4ba4f17a6b [feat] capture cursor when connected to a web client 2025-11-18 16:24:28 +08:00
dijunkun
f5d0291b5a [fix] fix crash when Unhook KeyboardCapturer on MacOS 2025-11-18 14:07:18 +08:00
dijunkun
1a64c1afef [feat] support auto-start on boot 2025-11-18 13:50:15 +08:00
dijunkun
18f4973d0a [fix] remove duplicate 'v' prefix in GitHub release name 2025-11-14 16:10:00 +08:00
dijunkun
37ede5861c [fix] update MiniRTC to fix crash when multiple peers join to remote server 2025-11-14 14:37:15 +08:00
dijunkun
497454ac51 [ci] remove 'v' prefix from Debian package version 2025-11-13 15:04:24 +08:00
dijunkun
4ebb7a6a4d [chore] update version format 2025-11-13 14:32:35 +08:00
dijunkun
7d1910df71 [ci] append build date to artifact names and write download URLs to version.json 2025-11-13 14:13:54 +08:00
dijunkun
52e70a26f3 [chore] update README 2025-11-13 00:06:14 +08:00
dijunkun
adb6cee326 [fix] fix version check issue 2025-11-12 00:21:18 +08:00
dijunkun
941b5e5cdc [ci] update version.json when new tag pushed 2025-11-12 00:11:32 +08:00
dijunkun
c602dea58f [chore] update README 2025-11-11 22:30:17 +08:00
dijunkun
e9ced9fa4f [ci] switch macos-13 to macos-15-intel due to the macOS-13 based runner images are being deprecated 2025-11-11 22:28:35 +08:00
dijunkun
6ab5e7487f [chore] update README 2025-11-11 21:59:50 +08:00
dijunkun
e3143f3e7a [feat] add automatic version check 2025-11-11 15:38:31 +08:00
dijunkun
538c17d182 [feat] disable AV1 encoding when connected to a Web client 2025-11-11 10:52:28 +08:00
dijunkun
2ad32ec2b4 [feat] add VideoQuality configuration option to limit maximum video resolution 2025-11-11 10:07:35 +08:00
dijunkun
b28f1dca81 [feat] update MiniRTC module 2025-11-11 01:32:40 +08:00
dijunkun
6947f7e1c3 Merge branch 'self-hosted-server' into libdatachannel 2025-11-11 01:18:24 +08:00
dijunkun
3817b222fd Merge branch 'libdatachannel' of https://github.com/kunkundi/crossdesk into libdatachannel 2025-11-11 01:00:19 +08:00
dijunkun
910cc9b587 [fix] stop devices control and capture when client peer destroyed 2025-11-11 00:59:54 +08:00
dijunkun
2ee3e93afe [feat] do not control mouse in debug mode 2025-11-11 00:56:23 +08:00
dijunkun
8875c6a6a1 [feat] update MiniRTC module 2025-11-11 00:53:10 +08:00
dijunkun
28062f5574 [config] use 60 fps streaming by default 2025-11-10 17:03:14 +08:00
dijunkun
017af3eea4 [feat] support multiple web clients connecting simultaneously 2025-11-10 16:54:56 +08:00
dijunkun
78eb069cc8 [feat] enable speaker capturer by default 2025-11-10 15:02:21 +08:00
dijunkun
0d591f769d [feat] send control commands via JSON 2025-11-06 17:31:06 +08:00
dijunkun
d4726355a7 [feat] update minirtc: support web streaming by using libdatachannel 2025-11-06 02:54:03 +08:00
46 changed files with 3713 additions and 1851 deletions

View File

@@ -35,12 +35,14 @@ jobs:
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="0.0.0-${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
@@ -53,7 +55,7 @@ jobs:
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
@@ -99,12 +101,14 @@ jobs:
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="0.0.0-${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="${VERSION_NUM}-${SHORT_SHA}"
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
@@ -116,7 +120,7 @@ jobs:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
@@ -144,7 +148,7 @@ jobs:
matrix:
include:
- arch: x64
runner: macos-13
runner: macos-15-intel
cache-key: intel
out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh
@@ -160,9 +164,11 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${VERSION#v}-${SHORT_SHA}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
@@ -183,7 +189,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${VERSION_NUM} -y
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
@@ -223,7 +229,9 @@ jobs:
$version = $version -replace '^v', ''
$version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
echo "VERSION_NUM=$version-$SHORT_SHA" >> $env:GITHUB_ENV
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
@@ -290,9 +298,9 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} -y
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: powershell
run: |
@@ -332,17 +340,25 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${VERSION#v}-${SHORT_SHA}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
- name: Rename artifacts
run: |
mkdir -p release
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_NUM }}.pkg
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_NUM }}.pkg
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_NUM }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_NUM }}.deb
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_NUM }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_NUM }}.exe
cp artifacts/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
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-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
- name: List release files
run: ls -lh release/
@@ -350,8 +366,8 @@ jobs:
- name: Upload to Versioned GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.VERSION_NUM }}
name: Release v${{ steps.version.outputs.VERSION_NUM }}
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
draft: false
prerelease: false
files: release/*
@@ -385,3 +401,45 @@ jobs:
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}
- name: Generate version.json
run: |
cat > version.json << EOF
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"downloads": {
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
},
"macos-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
},
"linux-amd64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
},
"linux-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb",
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb"
}
}
}
EOF
cat version.json
- name: Upload version.json to server
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: version.json
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}

View File

@@ -1,51 +0,0 @@
name: Update GitHub Pages Downloads
on:
push:
tags:
- "v*"
jobs:
update-pages:
runs-on: ubuntu-latest
steps:
- name: Checkout CrossDesk repo
uses: actions/checkout@v4
- name: Set version number
id: version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_NUM="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION_NUM#v}-${SHORT_SHA}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
- name: Checkout Pages repo
uses: actions/checkout@v4
with:
repository: kunkundi/kunkundi.github.io
token: ${{ secrets.GH_PAGES_PAT }}
path: pages
- name: Update download links
run: |
cd pages
echo "Updating download links to ${VERSION_NUM}"
sed -E -i "s/crossdesk-win-x64-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?\.exe/crossdesk-win-x64-${VERSION_NUM}.exe/g" index.html
sed -E -i "s/crossdesk-macos-x64-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?\.pkg/crossdesk-macos-x64-${VERSION_NUM}.pkg/g" index.html
sed -E -i "s/crossdesk-macos-arm64-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?\.pkg/crossdesk-macos-arm64-${VERSION_NUM}.pkg/g" index.html
sed -E -i "s/crossdesk-linux-amd64-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?\.deb/crossdesk-linux-amd64-${VERSION_NUM}.deb/g" index.html
sed -E -i "s/crossdesk-linux-arm64-[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?\.deb/crossdesk-linux-arm64-${VERSION_NUM}.deb/g" index.html
git diff index.html || true
- name: Commit & Push changes
run: |
cd pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add index.html
git commit -m "Update download links to v${VERSION_NUM}" || echo "No changes to commit"
git push origin main
env:
VERSION_NUM: ${{ env.VERSION_NUM }}

View File

@@ -2,7 +2,7 @@
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/self-hosted-server)
[![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)]()
@@ -11,14 +11,27 @@
[ [English](README_EN.md) / 中文 ]
PC 客户端
![sup_example](https://github.com/user-attachments/assets/eeb64fbe-1f07-4626-be1c-b77396beb905)
Web 客户端
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
## 简介
CrossDesk 是一个轻量级的跨平台远程桌面软件。
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** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
## 使用
@@ -33,6 +46,12 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
### Web 客户端
浏览器访问 [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" />
## 如何编译
依赖:
@@ -57,7 +76,14 @@ git submodule update
xmake b -vy crossdesk
```
编译选项
```
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
运行
```
xmake r crossdesk
@@ -65,13 +91,14 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
进入容器,下载工程后执行:
```
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -93,6 +120,7 @@ set CUDA_PATH=path_to_cuda_installdir
```
重新执行:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -153,7 +181,7 @@ sudo docker run -d \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:v1.0.0
crossdesk/crossdesk-server:v1.1.1
```
上述命令中,用户需注意的参数如下:
@@ -304,4 +332,4 @@ Generation complete. Deployment files::
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
# 常见问题
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。

View File

@@ -2,7 +2,7 @@
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/self-hosted-server)
[![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)]()
@@ -11,14 +11,29 @@
[ [中文](README.md) / English ]
PC Client
![sup_example](https://github.com/user-attachments/assets/3f17d8f3-7c4a-4b63-bae4-903363628687)
Web Client
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
# Intro
CrossDesk is a lightweight cross-platform remote desktop software.
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)).
## System Requirements
| Platform | Minimum Version |
|-----------|-----------------|
| **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 22.04 or later *(older versions can be built manually for compatibility)* |
## Usage
Enter the remote desktop ID in the menu bars “Remote ID” field and click “→” to initiate a remote connection.
@@ -33,6 +48,13 @@ Before connecting, you can customize configuration options in the settings, such
![settings](https://github.com/user-attachments/assets/8bc5468d-7bbb-4e30-95bd-da1f352ac08c)
### Web Client
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**:
<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" />
## How to build
Requirements:
@@ -57,7 +79,14 @@ git submodule update
xmake b -vy crossdesk
```
Build options:
```
--USE_CUDA=true/false: enable CUDA acceleration codec, default: false
--CROSSDESK_VERSION=xxx: set the version number
# example:
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
Run:
```
xmake r crossdesk
@@ -65,7 +94,7 @@ xmake r crossdesk
#### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment** installed, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
After entering the container, download the project and run:
@@ -73,6 +102,7 @@ After entering the container, download the project and run:
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -95,6 +125,7 @@ set CUDA_PATH=path_to_cuda_installdir:
```
Then re-run:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -158,7 +189,7 @@ sudo docker run -d \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:v1.0.0
crossdesk/crossdesk-server:v1.1.1
```
The parameters you need to pay attention to are as follows:
@@ -309,4 +340,4 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
# FAQ
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -9,7 +9,10 @@ ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
DEB_DIR="${PKG_NAME}-${APP_VERSION}"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
@@ -35,7 +38,7 @@ done
cat > "$DEBIAN_DIR/control" << EOF
Package: $PKG_NAME
Version: $APP_VERSION
Version: $DEB_VERSION
Architecture: $ARCHITECTURE
Maintainer: $MAINTAINER
Description: $DESCRIPTION
@@ -50,7 +53,7 @@ EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
[Desktop Entry]
Version=$APP_VERSION
Version=$DEB_VERSION
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME

View File

@@ -9,7 +9,10 @@ ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
DEB_DIR="${PKG_NAME}-${APP_VERSION}"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
@@ -35,7 +38,7 @@ done
cat > "$DEBIAN_DIR/control" << EOF
Package: $PKG_NAME
Version: $APP_VERSION
Version: $DEB_VERSION
Architecture: $ARCHITECTURE
Maintainer: $MAINTAINER
Description: $DESCRIPTION
@@ -49,7 +52,7 @@ EOF
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" << EOF
[Desktop Entry]
Version=$APP_VERSION
Version=$DEB_VERSION
Name=$APP_NAME
Comment=$DESCRIPTION
Exec=/usr/bin/$PKG_NAME

313
src/app/daemon.cpp Normal file
View File

@@ -0,0 +1,313 @@
#include "daemon.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <iostream>
#include <thread>
#include <vector>
#ifdef _WIN32
#include <process.h>
#include <tchar.h>
#include <windows.h>
#elif __APPLE__
#include <fcntl.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#endif
#ifndef _WIN32
Daemon* Daemon::instance_ = nullptr;
#endif
// get executable file path
static std::string GetExecutablePath() {
#ifdef _WIN32
char path[32768];
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
if (length > 0 && length < sizeof(path)) {
return std::string(path);
}
#elif __APPLE__
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char resolved_path[PATH_MAX];
if (realpath(path, resolved_path) != nullptr) {
return std::string(resolved_path);
}
return std::string(path);
}
#else
char path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (count != -1) {
path[count] = '\0';
return std::string(path);
}
#endif
return "";
}
Daemon::Daemon(const std::string& name)
: name_(name)
#ifdef _WIN32
,
running_(false)
#else
,
running_(true)
#endif
{
}
void Daemon::stop() { running_ = false; }
bool Daemon::isRunning() const { return running_; }
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
running_ = true;
return runWithRestart(loop);
#elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI
running_ = true;
return runWithRestart(loop);
#else
// linux: Daemonize first, then run with restart monitoring
instance_ = this;
// check if running from terminal before fork
bool from_terminal =
(isatty(STDIN_FILENO) != 0) || (isatty(STDOUT_FILENO) != 0);
// first fork: detach from terminal
pid_t pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process" << std::endl;
return false;
}
if (pid > 0) _exit(0);
if (setsid() < 0) {
std::cerr << "Failed to create new session" << std::endl;
return false;
}
pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process (second fork)" << std::endl;
return false;
}
if (pid > 0) _exit(0);
umask(0);
chdir("/");
// redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
if (!from_terminal) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
}
if (fd > 2) close(fd);
}
// set up signal handlers
signal(SIGTERM, [](int) {
if (instance_) instance_->stop();
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
// set up SIGCHLD handler to reap zombie processes
struct sigaction sa_chld;
sa_chld.sa_handler = [](int) {
// reap zombie processes
while (waitpid(-1, nullptr, WNOHANG) > 0) {
// continue until no more zombie children
}
};
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa_chld, nullptr);
running_ = true;
return runWithRestart(loop);
#endif
}
#ifdef _WIN32
static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
try {
loop();
return 0; // normal exit
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
return 1; // c++ exception
} catch (...) {
std::cerr << "Unknown exception caught" << std::endl;
return 1; // other exception
}
}
static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
__try {
return RunLoopCatchCpp(loop);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// catch system-level crashes (access violation, divide by zero, etc.)
DWORD code = GetExceptionCode();
std::cerr << "System crash detected (SEH exception code: 0x" << std::hex
<< code << std::dec << ")" << std::endl;
return 2; // System crash
}
}
#endif
// run with restart logic: parent monitors child process and restarts on crash
bool Daemon::runWithRestart(MainLoopFunc loop) {
int restart_count = 0;
std::string exe_path = GetExecutablePath();
if (exe_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(DAEMON_DEFAULT_RESTART_DELAY_MS));
}
}
return true;
}
while (isRunning()) {
#ifdef _WIN32
// windows: use CreateProcess to create child process
STARTUPINFOA si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
std::string cmd_line = "\"" + exe_path + "\" --child";
std::vector<char> cmd_line_buf(cmd_line.begin(), cmd_line.end());
cmd_line_buf.push_back('\0');
BOOL success = CreateProcessA(
nullptr, // executable file path (specified in command line)
cmd_line_buf.data(), // command line arguments
nullptr, // process security attributes
nullptr, // thread security attributes
FALSE, // don't inherit handles
0, // creation flags
nullptr, // environment variables (inherit from parent)
nullptr, // current directory
&si, // startup info
&pi // process information
);
if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
continue;
}
DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
#else
// linux: use fork + exec to create child process
pid_t pid = fork();
if (pid == 0) {
execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr);
_exit(1); // exec failed
} else if (pid > 0) {
int status = 0;
pid_t waited_pid = waitpid(pid, &status, 0);
if (waited_pid < 0) {
restart_count++;
std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue;
}
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else if (WIFSIGNALED(status)) {
restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else {
restart_count++;
std::cerr << "Child process exited with unknown status, restarting... "
"(attempt "
<< restart_count << ")" << std::endl;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} else {
std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
}
#endif
}
return true;
}

39
src/app/daemon.h Normal file
View File

@@ -0,0 +1,39 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-19
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <functional>
#include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
Daemon(const std::string& name);
bool start(MainLoopFunc loop);
void stop();
bool isRunning() const;
private:
std::string name_;
bool runWithRestart(MainLoopFunc loop);
#ifdef _WIN32
bool running_;
#else
static Daemon* instance_;
volatile bool running_;
#endif
};
#endif

View File

@@ -6,12 +6,61 @@
#endif
#endif
#include "rd_log.h"
#include <cstring>
#include <memory>
#include <string>
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "render.h"
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
int main(int argc, char* argv[]) {
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
}
if (is_child) {
// child process: run render directly
crossdesk::Render render;
render.Run();
return 0;
}
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cert_path =
(path_manager->GetCertPath() / "crossdesk.cn_root.crt").string();
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini",
cert_path);
enable_daemon = config_center.IsEnableDaemon();
}
if (enable_daemon) {
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
crossdesk::Render render;
render.Run();
daemon.stop();
};
// start daemon and return result
bool success = daemon.start(main_loop);
return success ? 0 : 1;
}
// run without daemon: direct execution
crossdesk::Render render;
render.Run();
return 0;
}

302
src/autostart/autostart.cpp Normal file
View File

@@ -0,0 +1,302 @@
#include "autostart.h"
#include <cstdlib>
#include <filesystem>
#include <fstream>
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <limits.h>
#include <mach-o/dyld.h>
#include <unistd.h>
#elif defined(__linux__)
#include <linux/limits.h>
#include <unistd.h>
#endif
namespace crossdesk {
static std::string get_home_dir() {
const char* home = std::getenv("HOME");
if (!home) {
return "";
}
return std::string(home);
}
static bool file_exists(const std::string& path) {
return std::filesystem::exists(path) &&
std::filesystem::is_regular_file(path);
}
static std::string GetExecutablePath() {
#ifdef _WIN32
char path[32768];
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
if (length > 0 && length < sizeof(path)) {
return std::string(path);
}
#elif defined(__APPLE__)
char path[1024];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char resolved_path[PATH_MAX];
if (realpath(path, resolved_path) != nullptr) {
return std::string(resolved_path);
}
return std::string(path);
}
#elif defined(__linux__)
char path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (count != -1) {
path[count] = '\0';
return std::string(path);
}
#endif
return "";
}
// Windows
#ifdef _WIN32
static constexpr const char* WINDOWS_RUN_KEY =
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
static bool windows_enable(const std::string& appName,
const std::string& exePath) {
if (exePath.empty() || !std::filesystem::exists(exePath)) {
return false;
}
HKEY hKey = nullptr;
// Use KEY_WRITE to ensure we have write permission
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
std::string regValue = exePath;
if (!exePath.empty() && exePath.find(' ') != std::string::npos) {
if (exePath.front() != '"' || exePath.back() != '"') {
regValue = "\"" + exePath + "\"";
}
}
// Ensure we close the key even if RegSetValueExA fails
result = RegSetValueExA(hKey, appName.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(regValue.c_str()),
static_cast<DWORD>(regValue.size() + 1));
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
static bool windows_disable(const std::string& appName) {
HKEY hKey = nullptr;
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_WRITE, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
result = RegDeleteValueA(hKey, appName.c_str());
RegCloseKey(hKey);
// Return true even if the value doesn't exist (already disabled)
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
}
static bool windows_exists(const std::string& appName) {
HKEY hKey = nullptr;
LONG result =
RegOpenKeyExA(HKEY_CURRENT_USER, WINDOWS_RUN_KEY, 0, KEY_READ, &hKey);
if (result != ERROR_SUCCESS) {
return false;
}
result = RegQueryValueExA(hKey, appName.c_str(), nullptr, nullptr, nullptr,
nullptr);
RegCloseKey(hKey);
return result == ERROR_SUCCESS;
}
#endif
// Linux
#if defined(__linux__)
static std::string linux_desktop_path(const std::string& appName) {
std::string home = get_home_dir();
if (home.empty()) {
return "";
}
return home + "/.config/autostart/" + appName + ".desktop";
}
static bool linux_enable(const std::string& appName,
const std::string& exePath) {
std::string home = get_home_dir();
if (home.empty()) {
return false;
}
std::filesystem::path dir =
std::filesystem::path(home) / ".config" / "autostart";
// Create directory if it doesn't exist
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
return false;
}
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
std::ofstream file(path);
if (!file.is_open()) {
return false;
}
file << "[Desktop Entry]\n";
file << "Type=Application\n";
file << "Exec=" << exePath << "\n";
file << "Hidden=false\n";
file << "NoDisplay=false\n";
file << "X-GNOME-Autostart-enabled=true\n";
file << "Terminal=false\n";
file << "StartupNotify=false\n";
file << "Name=" << appName << "\n";
file.close();
return file.good();
}
static bool linux_disable(const std::string& appName) {
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::remove(path, ec) && !ec;
}
static bool linux_exists(const std::string& appName) {
std::string path = linux_desktop_path(appName);
if (path.empty()) {
return false;
}
return file_exists(path);
}
#endif
// macOS
#ifdef __APPLE__
static std::string mac_plist_path(const std::string& appName) {
std::string home = get_home_dir();
if (home.empty()) {
return "";
}
return home + "/Library/LaunchAgents/" + appName + ".plist";
}
static bool mac_enable(const std::string& appName, const std::string& exePath) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
// Ensure LaunchAgents directory exists
std::filesystem::path dir =
std::filesystem::path(get_home_dir()) / "Library" / "LaunchAgents";
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
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>)"
<< appName << R"(</string>
<key>ProgramArguments</key>
<array>
<string>)"
<< exePath << R"(</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>)";
file.close();
return file.good();
}
static bool mac_disable(const std::string& appName) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::remove(path, ec) && !ec;
}
static bool mac_exists(const std::string& appName) {
std::string path = mac_plist_path(appName);
if (path.empty()) {
return false;
}
return file_exists(path);
}
#endif
bool EnableAutostart(const std::string& appName) {
std::string exePath = GetExecutablePath();
if (exePath.empty()) {
return false;
}
#ifdef _WIN32
return windows_enable(appName, exePath);
#elif __APPLE__
return mac_enable(appName, exePath);
#else
return linux_enable(appName, exePath);
#endif
}
bool DisableAutostart(const std::string& appName) {
#ifdef _WIN32
return windows_disable(appName);
#elif __APPLE__
return mac_disable(appName);
#else
return linux_disable(appName);
#endif
}
bool IsAutostartEnabled(const std::string& appName) {
#ifdef _WIN32
return windows_exists(appName);
#elif __APPLE__
return mac_exists(appName);
#else
return linux_exists(appName);
#endif
}
} // namespace crossdesk

21
src/autostart/autostart.h Normal file
View File

@@ -0,0 +1,21 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-18
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _AUTOSTART_H_
#define _AUTOSTART_H_
#include <string>
namespace crossdesk {
bool EnableAutostart(const std::string& appName);
bool DisableAutostart(const std::string& appName);
bool IsAutostartEnabled(const std::string& appName);
} // namespace crossdesk
#endif

View File

@@ -1,5 +1,8 @@
#include "config_center.h"
#include "autostart.h"
#include "rd_log.h"
namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path,
@@ -48,7 +51,9 @@ int ConfigCenter::Load() {
ini_.GetValue(section_, "cert_file_path", cert_file_path_.c_str());
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
@@ -71,6 +76,8 @@ int ConfigCenter::Save() {
static_cast<long>(signal_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
@@ -221,6 +228,41 @@ int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
return 0;
}
int ConfigCenter::SetAutostart(bool enable_autostart) {
enable_autostart_ = enable_autostart;
bool success = false;
if (enable_autostart) {
success = EnableAutostart("CrossDesk");
} else {
success = DisableAutostart("CrossDesk");
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
if (!success) {
LOG_ERROR("SetAutostart failed");
return -1;
}
return 0;
}
int ConfigCenter::SetDaemon(bool enable_daemon) {
enable_daemon_ = enable_daemon;
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
@@ -274,4 +316,8 @@ std::string ConfigCenter::GetDefaultCertFilePath() const {
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
} // namespace crossdesk

View File

@@ -40,6 +40,8 @@ class ConfigCenter {
int SetCertFilePath(const std::string& cert_file_path);
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
// read config
@@ -60,6 +62,8 @@ class ConfigCenter {
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
int Load();
int Save();
@@ -72,7 +76,7 @@ class ConfigCenter {
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_30;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
bool enable_turn_ = false;
@@ -86,6 +90,8 @@ class ConfigCenter {
std::string cert_file_path_default_ = "";
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
};
} // namespace crossdesk
#endif

View File

@@ -9,7 +9,11 @@
#include <stdio.h>
#include <nlohmann/json.hpp>
#include <string>
#include "display_info.h"
using json = nlohmann::json;
namespace crossdesk {
@@ -55,7 +59,7 @@ typedef struct {
int* bottom;
} HostInfo;
typedef struct {
struct RemoteAction {
ControlType type;
union {
Mouse m;
@@ -64,7 +68,111 @@ typedef struct {
bool a;
int d;
};
} RemoteAction;
// parse
std::string to_json() const { return ToJson(*this); }
bool from_json(const std::string& json_str) {
RemoteAction temp;
if (!FromJson(json_str, temp)) return false;
*this = temp;
return true;
}
static std::string ToJson(const RemoteAction& a) {
json j;
j["type"] = a.type;
switch (a.type) {
case ControlType::mouse:
j["mouse"] = {
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
break;
case ControlType::keyboard:
j["keyboard"] = {{"key_value", a.k.key_value}, {"flag", a.k.flag}};
break;
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
case ControlType::display_id:
j["display_id"] = a.d;
break;
case ControlType::host_infomation: {
json displays = json::array();
for (size_t idx = 0; idx < a.i.display_num; idx++) {
displays.push_back(
{{"name", a.i.display_list ? a.i.display_list[idx] : ""},
{"left", a.i.left ? a.i.left[idx] : 0},
{"top", a.i.top ? a.i.top[idx] : 0},
{"right", a.i.right ? a.i.right[idx] : 0},
{"bottom", a.i.bottom ? a.i.bottom[idx] : 0}});
}
j["host_info"] = {{"host_name", a.i.host_name},
{"display_num", a.i.display_num},
{"displays", displays}};
break;
}
}
return j.dump();
}
static bool FromJson(const std::string& json_str, RemoteAction& out) {
try {
json j = json::parse(json_str);
out.type = (ControlType)j.at("type").get<int>();
switch (out.type) {
case ControlType::mouse:
out.m.x = j.at("mouse").at("x").get<float>();
out.m.y = j.at("mouse").at("y").get<float>();
out.m.s = j.at("mouse").at("s").get<int>();
out.m.flag = (MouseFlag)j.at("mouse").at("flag").get<int>();
break;
case ControlType::keyboard:
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break;
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
out.d = j.at("display_id").get<int>();
break;
case ControlType::host_infomation: {
std::string host_name =
j.at("host_info").at("host_name").get<std::string>();
strncpy(out.i.host_name, host_name.c_str(), sizeof(out.i.host_name));
out.i.host_name[sizeof(out.i.host_name) - 1] = '\0';
out.i.host_name_size = host_name.size();
out.i.display_num = j.at("host_info").at("display_num").get<size_t>();
auto displays = j.at("host_info").at("displays");
out.i.display_list =
(char**)malloc(out.i.display_num * sizeof(char*));
out.i.left = (int*)malloc(out.i.display_num * sizeof(int));
out.i.top = (int*)malloc(out.i.display_num * sizeof(int));
out.i.right = (int*)malloc(out.i.display_num * sizeof(int));
out.i.bottom = (int*)malloc(out.i.display_num * sizeof(int));
for (size_t idx = 0; idx < out.i.display_num; idx++) {
std::string name = displays[idx].at("name").get<std::string>();
out.i.display_list[idx] = (char*)malloc(name.size() + 1);
strcpy(out.i.display_list[idx], name.c_str());
out.i.left[idx] = displays[idx].at("left").get<int>();
out.i.top[idx] = displays[idx].at("top").get<int>();
out.i.right[idx] = displays[idx].at("right").get<int>();
out.i.bottom[idx] = displays[idx].at("bottom").get<int>();
}
break;
}
}
return true;
} catch (const std::exception& e) {
printf("Failed to parse RemoteAction JSON: %s\n", e.what());
return false;
}
}
};
// int key_code, bool is_down
typedef void (*OnKeyAction)(int, bool, void*);

View File

@@ -51,7 +51,16 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
running_ = false;
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
XFlush(display_);
}
return 0;
}

View File

@@ -10,6 +10,10 @@ static void* g_user_ptr = nullptr;
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
if (!g_on_key_action) {
return event;
}
KeyboardCapturer* keyboard_capturer = (KeyboardCapturer*)userInfo;
if (!keyboard_capturer) {
LOG_ERROR("keyboard_capturer is nullptr");
@@ -120,8 +124,25 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
CFRelease(run_loop_source_);
CFRelease(event_tap_);
g_on_key_action = nullptr;
g_user_ptr = nullptr;
if (event_tap_) {
CGEventTapEnable(event_tap_, false);
}
if (run_loop_source_) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source_,
kCFRunLoopCommonModes);
CFRelease(run_loop_source_);
run_loop_source_ = nullptr;
}
if (event_tap_) {
CFRelease(event_tap_);
event_tap_ = nullptr;
}
return 0;
}

View File

@@ -39,7 +39,12 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
UnhookWindowsHookEx(keyboard_hook_);
if (keyboard_hook_) {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
UnhookWindowsHookEx(keyboard_hook_);
keyboard_hook_ = nullptr;
}
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,22 @@
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#else
#define SETTINGS_WINDOW_HEIGHT_CN 315
#define SETTINGS_WINDOW_HEIGHT_EN 315
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
@@ -47,6 +57,10 @@
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90

View File

@@ -158,14 +158,25 @@ static std::vector<std::string> no_such_id = {
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"};
static std::vector<std::string> new_version_available = {
reinterpret_cast<const char*>(u8"新版本可用"), "New Version Available"};
static std::vector<std::string> version = {
reinterpret_cast<const char*>(u8"版本"), "Version"};
static std::vector<std::string> access_website = {
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "};
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"),
"Confirm to delete this connection"};
#if _WIN32
static std::vector<std::string> enable_autostart = {
reinterpret_cast<const char*>(u8"开机自启:"), "Auto Start:"};
static std::vector<std::string> enable_daemon = {
reinterpret_cast<const char*>(u8"启用守护进程:"), "Enable Daemon:"};
static std::vector<std::string> takes_effect_after_restart = {
reinterpret_cast<const char*>(u8"重启后生效"),
"Takes effect after restart"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
"Minimize to system tray when exit:"};

View File

@@ -16,6 +16,7 @@
#include "platform.h"
#include "rd_log.h"
#include "screen_capturer_factory.h"
#include "version_checker.h"
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
@@ -259,6 +260,9 @@ int Render::LoadSettingsFromCacheFile() {
enable_turn_ = config_center_->IsEnableTurn();
enable_srtp_ = config_center_->IsEnableSrtp();
enable_self_hosted_ = config_center_->IsSelfHosted();
enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
language_button_value_last_ = language_button_value_;
video_quality_button_value_last_ = video_quality_button_value_;
@@ -267,6 +271,8 @@ int Render::LoadSettingsFromCacheFile() {
enable_turn_last_ = enable_turn_;
enable_srtp_last_ = enable_srtp_;
enable_self_hosted_last_ = enable_self_hosted_;
enable_autostart_last_ = enable_autostart_;
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
LOG_INFO("Load settings from cache file");
@@ -324,8 +330,9 @@ int Render::ScreenCapturerInit() {
int Render::StartScreenCapturer() {
if (screen_capturer_) {
LOG_INFO("Start screen capturer");
screen_capturer_->Start();
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor_);
screen_capturer_->Start(show_cursor_);
}
return 0;
@@ -495,6 +502,8 @@ int Render::CreateConnectionPeer() {
: false;
params_.enable_turn = config_center_->IsEnableTurn();
params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality());
params_.on_receive_video_buffer = nullptr;
params_.on_receive_audio_buffer = OnReceiveAudioBufferCb;
params_.on_receive_data_buffer = OnReceiveDataBufferCb;
@@ -566,6 +575,14 @@ void Render::UpdateInteractions() {
screen_capturer_is_started_ = false;
}
if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
StartSpeakerCapturer();
speaker_capturer_is_started_ = true;
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
StopSpeakerCapturer();
speaker_capturer_is_started_ = false;
}
if (start_mouse_controller_ && !mouse_controller_is_started_) {
StartMouseController();
mouse_controller_is_started_ = true;
@@ -896,6 +913,9 @@ int Render::DrawStreamWindow() {
}
int Render::Run() {
latest_version_ = CheckUpdate();
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
cert_path_ =
@@ -1055,9 +1075,9 @@ void Render::MainLoop() {
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::vector<char> serialized = SerializeRemoteAction(remote_action);
int ret = SendDataFrame(peer_, serialized.data(), serialized.size(),
data_label_.c_str());
std::string msg = remote_action.to_json();
int ret =
SendDataFrame(peer_, msg.data(), msg.size(), data_label_.c_str());
FreeRemoteAction(remote_action);
if (0 == ret) {
need_to_send_host_info_ = false;
@@ -1183,6 +1203,10 @@ void Render::CleanupPeers() {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
StopScreenCapturer();
StopSpeakerCapturer();
StopMouseController();
StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
@@ -1316,6 +1340,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
is_client_mode_ = false;
reload_recent_connections_ = true;
fullscreen_button_pressed_ = false;
start_keyboard_capturer_ = false;
just_created_ = false;
recent_connection_image_save_time_ = SDL_GetTicks();
} else {

View File

@@ -49,10 +49,10 @@ class Render {
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool mouse_control_button_pressed_ = false;
bool mouse_control_button_pressed_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = false;
bool control_mouse_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
@@ -307,11 +307,16 @@ class Render {
#endif
// main window properties
std::string latest_version_ = "";
bool update_available_ = false;
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = true;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool foucs_on_stream_window_ = false;
@@ -430,13 +435,17 @@ class Render {
KeyboardCapturer* keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
uint64_t last_frame_time_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
uint64_t new_version_icon_last_trigger_time_ = 0;
uint64_t new_version_icon_render_start_time_ = 0;
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 0;
int video_frame_rate_button_value_ = 0;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false;
bool enable_turn_ = false;
@@ -454,6 +463,10 @@ class Render {
bool enable_turn_last_ = false;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char signal_server_ip_self_[256] = "";
@@ -471,6 +484,9 @@ class Render {
client_properties_;
void CloseTab(decltype(client_properties_)::iterator& it);
/* ------ stream window property end ------ */
/* ------ server mode ------ */
std::unordered_map<std::string, ConnectionStatus> connection_status_;
};
} // namespace crossdesk
#endif

View File

@@ -1,3 +1,5 @@
#include <cmath>
#include "device_controller.h"
#include "localization.h"
#include "platform.h"
@@ -6,11 +8,6 @@
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
#ifdef CROSSDESK_DEBUG
#else
#define MOUSE_CONTROL 1
#endif
namespace crossdesk {
int Render::SendKeyCommand(int key_code, bool is_down) {
@@ -28,8 +25,9 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
client_properties_.end()) {
auto props = client_properties_[controlled_remote_id_];
if (props->connection_status_ == ConnectionStatus::Connected) {
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
}
@@ -94,8 +92,10 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
remote_action.m.flag = MouseFlag::move;
}
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= props->stream_render_rect_.x &&
last_mouse_event.button.x <= props->stream_render_rect_.x +
@@ -103,32 +103,44 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
last_mouse_event.button.y >= props->stream_render_rect_.y &&
last_mouse_event.button.y <= props->stream_render_rect_.y +
props->stream_render_rect_.h) {
int scroll_x = event.wheel.x;
int scroll_y = event.wheel.y;
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
if (scroll_x == 0) {
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = scroll_y;
} else if (scroll_y == 0) {
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = scroll_x;
remote_action.m.s = roundUp(scroll_x);
}
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
remote_action.m.x =
(float)(event.button.x - props->stream_render_rect_.x) / render_width;
(float)(last_mouse_event.button.x - props->stream_render_rect_.x) /
render_width;
remote_action.m.y =
(float)(event.button.y - props->stream_render_rect_.y) /
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height;
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
@@ -279,64 +291,54 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
return;
}
std::string json_str(data, size);
RemoteAction remote_action;
memcpy(&remote_action, data, size);
try {
remote_action.from_json(json_str);
} catch (const std::exception& e) {
LOG_ERROR("Failed to parse RemoteAction JSON: {}", e.what());
return;
}
std::string remote_id(user_id, user_id_size);
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// local
auto props = render->client_properties_.find(remote_id)->second;
RemoteAction host_info;
if (DeserializeRemoteAction(data, size, host_info)) {
if (ControlType::host_infomation == host_info.type &&
props->remote_host_name_.empty()) {
props->remote_host_name_ =
std::string(host_info.i.host_name, host_info.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < host_info.i.display_num; i++) {
props->display_info_list_.push_back(DisplayInfo(
std::string(host_info.i.display_list[i]), host_info.i.left[i],
host_info.i.top[i], host_info.i.right[i], host_info.i.bottom[i]));
LOG_INFO("Remote display [{}:{}], bound [({}, {}) ({}, {})]", i + 1,
props->display_info_list_[i].name,
props->display_info_list_[i].left,
props->display_info_list_[i].top,
props->display_info_list_[i].right,
props->display_info_list_[i].bottom);
}
}
} else {
if (remote_action.type == ControlType::host_infomation &&
props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
LOG_ERROR("No remote display detected");
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
FreeRemoteAction(host_info);
FreeRemoteAction(remote_action);
} else {
// remote
if (ControlType::mouse == remote_action.type && render->mouse_controller_) {
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
render->mouse_controller_->SendMouseCommand(remote_action,
render->selected_display_);
} else if (ControlType::audio_capture == remote_action.type) {
if (remote_action.a) {
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !render->start_speaker_capturer_)
render->StartSpeakerCapturer();
render->audio_capture_ = true;
} else {
else if (!remote_action.a && render->start_speaker_capturer_)
render->StopSpeakerCapturer();
render->audio_capture_ = false;
}
} else if (ControlType::keyboard == remote_action.type &&
} else if (remote_action.type == ControlType::keyboard &&
render->keyboard_capturer_) {
render->keyboard_capturer_->SendKeyboardCommand(
(int)remote_action.k.key_value,
remote_action.k.flag == KeyFlag::key_down);
} else if (ControlType::display_id == remote_action.type) {
if (render->screen_capturer_) {
render->selected_display_ = remote_action.d;
render->screen_capturer_->SwitchTo(remote_action.d);
}
} else if (remote_action.type == ControlType::display_id &&
render->screen_capturer_) {
render->selected_display_ = remote_action.d;
render->screen_capturer_->SwitchTo(remote_action.d);
}
}
}
@@ -409,7 +411,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
props->connection_status_ = status;
switch (status) {
case ConnectionStatus::Connected:
case ConnectionStatus::Connected: {
if (!render->need_to_create_stream_window_ &&
!render->client_properties_.empty()) {
render->need_to_create_stream_window_ = true;
@@ -420,14 +422,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
(int)render->stream_window_width_,
(int)(render->stream_window_height_ - render->title_bar_height_)};
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed:
render->password_validating_time_ = 0;
render->start_screen_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->control_mouse_ = false;
case ConnectionStatus::Closed: {
props->connection_established_ = false;
props->mouse_control_button_pressed_ = false;
if (props->dst_buffer_) {
@@ -436,8 +434,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
props->texture_width_);
}
render->CleanSubStreamWindowProperties(props);
break;
case ConnectionStatus::IncorrectPassword:
}
case ConnectionStatus::IncorrectPassword: {
render->password_validating_ = false;
render->password_validating_time_++;
if (render->connect_button_pressed_) {
@@ -447,37 +447,73 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
localization::connect[render->localization_language_index_];
}
break;
case ConnectionStatus::NoSuchTransmissionId:
}
case ConnectionStatus::NoSuchTransmissionId: {
if (render->connect_button_pressed_) {
props->connection_established_ = false;
render->connect_button_label_ =
localization::connect[render->localization_language_index_];
}
break;
}
default:
break;
}
} else {
render->is_client_mode_ = false;
render->show_connection_status_window_ = true;
render->connection_status_[remote_id] = status;
switch (status) {
case ConnectionStatus::Connected:
case ConnectionStatus::Connected: {
render->need_to_send_host_info_ = true;
render->start_screen_capturer_ = true;
render->start_mouse_controller_ = true;
break;
case ConnectionStatus::Closed:
render->start_screen_capturer_ = false;
render->start_speaker_capturer_ = true;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->need_to_send_host_info_ = false;
if (props) props->connection_established_ = false;
if (render->audio_capture_) {
render->StopSpeakerCapturer();
render->audio_capture_ = false;
#else
render->start_mouse_controller_ = true;
#endif
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
render->show_cursor_ = true;
}
break;
}
case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
})) {
render->start_screen_capturer_ = false;
render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->need_to_send_host_info_ = false;
if (props) props->connection_established_ = false;
if (render->audio_capture_) {
render->StopSpeakerCapturer();
render->audio_capture_ = false;
}
render->connection_status_.erase(remote_id);
}
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") == std::string::npos;
})) {
render->show_cursor_ = false;
}
break;
}
default:
break;
}

View File

@@ -68,8 +68,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
remote_action.type = ControlType::display_id;
remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) {
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
@@ -142,8 +143,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
RemoteAction remote_action;
remote_action.type = ControlType::audio_capture;
remote_action.a = props->audio_capture_button_pressed_;
SendDataFrame(props->peer_, (const char*)&remote_action,
sizeof(remote_action), props->data_label_.c_str());
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
if (!props->audio_capture_button_pressed_) {

View File

@@ -3,6 +3,7 @@
#include "render.h"
#define BUTTON_PADDING 36.0f
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
namespace crossdesk {
@@ -39,13 +40,59 @@ int Render::TitleBar(bool main_window) {
localization::settings[localization_language_index_].c_str())) {
show_settings_window_ = true;
}
if (ImGui::MenuItem(
localization::about[localization_language_index_].c_str())) {
show_new_version_icon_in_menu_ = false;
std::string about_str =
localization::about[localization_language_index_];
if (update_available_) {
auto now_time =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
if (now_time - new_version_icon_render_start_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
show_new_version_icon_ = false;
}
} else {
about_str = about_str + " ";
}
}
if (ImGui::MenuItem(about_str.c_str())) {
show_about_window_ = true;
}
if (update_available_ && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string new_version_available_str =
localization::new_version_available
[localization_language_index_] +
": " + latest_version_;
ImGui::Text("%s", new_version_available_str.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::EndMenu();
} else {
show_new_version_icon_in_menu_ = true;
}
float menu_bar_line_size = 15.0f;
draw_list->AddLine(ImVec2(bar_pos_x, bar_pos_y - 6),
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y - 6),
@@ -57,6 +104,33 @@ int Render::TitleBar(bool main_window) {
ImVec2(bar_pos_x + menu_bar_line_size, bar_pos_y + 6),
IM_COL32(0, 0, 0, 255));
if (update_available_ && show_new_version_icon_in_menu_) {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
// every 2 seconds
if (now_time - new_version_icon_last_trigger_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
show_new_version_icon_ = true;
new_version_icon_render_start_time_ = now_time;
new_version_icon_last_trigger_time_ = now_time;
}
// render for 1 second
if (show_new_version_icon_) {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetCursorPos(ImVec2(bar_pos_x + 10, bar_pos_y - 17));
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
ImGui::SetWindowFontScale(1.0f);
if (now_time - new_version_icon_render_start_time_ >=
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
show_new_version_icon_ = false;
}
}
}
{
SettingWindow();
SelfHostedServerWindow();

View File

@@ -1,3 +1,6 @@
#include <cstdlib>
#include <string>
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
@@ -5,16 +8,46 @@
namespace crossdesk {
void Hyperlink(const std::string& label, const std::string& url,
const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::SetCursorPosX(window_width * 0.1f);
ImGui::Text("%s", label.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.6f);
ImGui::TextUnformatted(url.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
#if defined(_WIN32)
std::string cmd = "start " + url;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
#else
std::string cmd = "xdg-open " + url;
#endif
system(cmd.c_str()); // open browser
}
}
}
int Render::AboutWindow() {
if (show_about_window_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
float about_window_height = latest_version_.empty()
? about_window_height_
: about_window_height_ + 20.0f;
ImGui::SetNextWindowPos(ImVec2(
(viewport->WorkSize.x - viewport->WorkPos.x - about_window_width_) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height_) /
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) /
2));
ImGui::SetNextWindowSize(ImVec2(about_window_width_, about_window_height_));
ImGui::SetNextWindowSize(ImVec2(about_window_width_, about_window_height));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
@@ -36,17 +69,30 @@ int Render::AboutWindow() {
#endif
std::string text = localization::version[localization_language_index_] +
": CrossDesk v" + version;
": CrossDesk " + version;
ImGui::SetCursorPosX(about_window_width_ * 0.1f);
ImGui::Text("%s", text.c_str());
if (update_available_) {
std::string latest_version =
localization::new_version_available[localization_language_index_] +
": " + latest_version_;
std::string access_website =
localization::access_website[localization_language_index_];
Hyperlink(latest_version, "https://crossdesk.cn", about_window_width_);
}
ImGui::Text("");
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
std::string license_text = "Licensed under GNU LGPL v3.";
ImGui::SetCursorPosX(about_window_width_ * 0.1f);
ImGui::Text("%s", copyright_text.c_str());
ImGui::SetCursorPosX(about_window_width_ * 0.1f);
ImGui::Text("%s", license_text.c_str());
ImGui::SetCursorPosX(about_window_width_ * 0.42f);
ImGui::SetCursorPosY(about_window_height_ * 0.75f);
ImGui::SetCursorPosY(about_window_height * 0.75f);
// OK
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
show_about_window_ = false;

View File

@@ -82,11 +82,11 @@ int Render::SettingWindow() {
{
const char* video_quality_items[] = {
localization::video_quality_high[localization_language_index_]
localization::video_quality_low[localization_language_index_]
.c_str(),
localization::video_quality_medium[localization_language_index_]
.c_str(),
localization::video_quality_low[localization_language_index_]
localization::video_quality_high[localization_language_index_]
.c_str()};
settings_items_offset += settings_items_padding;
@@ -158,6 +158,7 @@ int Render::SettingWindow() {
video_encode_format_items, IM_ARRAYSIZE(video_encode_format_items));
}
#if USE_CUDA && !defined(__aarch64__) && !defined(__arm__)
ImGui::Separator();
{
@@ -176,6 +177,7 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_hardware_video_codec",
&enable_hardware_video_codec_);
}
#endif
ImGui::Separator();
@@ -233,6 +235,53 @@ int Render::SettingWindow() {
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::Text("%s",
localization::enable_autostart[localization_language_index_]
.c_str());
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_AUTOSTART_PADDING_CN);
} else {
ImGui::SetCursorPosX(ENABLE_AUTOSTART_PADDING_EN);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::Text(
"%s",
localization::enable_daemon[localization_language_index_].c_str());
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_CN);
} else {
ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_EN);
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", localization::takes_effect_after_restart
[localization_language_index_]
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
#if _WIN32
ImGui::Separator();
@@ -347,6 +396,29 @@ int Render::SettingWindow() {
}
enable_self_hosted_last_ = enable_self_hosted_;
if (enable_autostart_) {
config_center_->SetAutostart(true);
} else {
config_center_->SetAutostart(false);
}
enable_autostart_last_ = enable_autostart_;
if (enable_daemon_) {
config_center_->SetDaemon(true);
} else {
config_center_->SetDaemon(false);
}
enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true);
} else {
config_center_->SetMinimizeToTray(false);
}
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
settings_window_pos_reset_ = true;
// Recreate peer instance

View File

@@ -36,9 +36,21 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
XRRCrtcInfo* crtc_info =
XRRGetCrtcInfo(display_, screen_res_, output_info->crtc);
display_info_list_.push_back(
DisplayInfo((void*)display_, output_info->name, true, crtc_info->x,
crtc_info->y, crtc_info->width, crtc_info->height));
std::string name(output_info->name);
if (name.empty()) {
name = "Display" + std::to_string(i + 1);
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
display_info_list_.push_back(DisplayInfo(
(void*)display_, name, true, crtc_info->x, crtc_info->y,
crtc_info->x + crtc_info->width, crtc_info->y + crtc_info->height));
XRRFreeCrtcInfo(crtc_info);
}
@@ -86,7 +98,7 @@ int ScreenCapturerX11::Destroy() {
return 0;
}
int ScreenCapturerX11::Start() {
int ScreenCapturerX11::Start(bool show_cursor) {
if (running_) return 0;
running_ = true;
paused_ = false;

View File

@@ -10,6 +10,7 @@
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xrandr.h>
#include <X11/extensions/Xfixes.h>
#include <atomic>
#include <cstring>
@@ -30,7 +31,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
@@ -54,6 +55,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_;
std::vector<DisplayInfo> display_info_list_;
@@ -61,6 +63,9 @@ class ScreenCapturerX11 : public ScreenCapturer {
// 缓冲区
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
// 鼠标光标相关
void DrawCursor(XImage* image, int x, int y);
};
} // namespace crossdesk
#endif

View File

@@ -28,8 +28,8 @@ int ScreenCapturerSck::Destroy() {
return 0;
}
int ScreenCapturerSck::Start() {
screen_capturer_sck_impl_->Start();
int ScreenCapturerSck::Start(bool show_cursor) {
screen_capturer_sck_impl_->Start(show_cursor);
return 0;
}

View File

@@ -26,7 +26,7 @@ class ScreenCapturerSck : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;

View File

@@ -57,7 +57,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
public:
int Init(const int fps, cb_desktop_data cb) override;
int Start() override;
int Start(bool show_cursor) override;
int SwitchTo(int monitor_index) override;
@@ -80,6 +80,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int width_ = 0;
int height_ = 0;
int fps_ = 60;
bool show_cursor_ = false;
public:
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
@@ -225,13 +226,17 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
CGRect bounds = CGDisplayBounds(display_id);
bool is_primary = CGDisplayIsMain(display_id);
std::string name;
name = GetDisplayName(display_id);
std::string name = GetDisplayName(display_id);
if (name.empty()) {
name = "Display " + std::to_string(unnamed_count++);
name = "Display" + std::to_string(unnamed_count++);
}
// clean display name, remove non-alphanumeric characters
name.erase(
std::remove_if(name.begin(), name.end(), [](unsigned char c) { return !std::isalnum(c); }),
name.end());
DisplayInfo info((void *)(uintptr_t)display_id, name, is_primary,
static_cast<int>(bounds.origin.x), static_cast<int>(bounds.origin.y),
static_cast<int>(bounds.origin.x + bounds.size.width),
@@ -246,7 +251,8 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
return 0;
}
int ScreenCapturerSckImpl::Start() {
int ScreenCapturerSckImpl::Start(bool show_cursor) {
show_cursor_ = show_cursor;
StartOrReconfigureCapturer();
return 0;
}
@@ -328,7 +334,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
excludingWindows:@[]];
SCStreamConfiguration *config = [[SCStreamConfiguration alloc] init];
config.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
config.showsCursor = false;
config.showsCursor = show_cursor_;
config.width = filter.contentRect.size.width * filter.pointPixelScale;
config.height = filter.contentRect.size.height * filter.pointPixelScale;
config.captureResolution = SCCaptureResolutionAutomatic;

View File

@@ -24,7 +24,7 @@ class ScreenCapturer {
public:
virtual int Init(const int fps, cb_desktop_data cb) = 0;
virtual int Destroy() = 0;
virtual int Start() = 0;
virtual int Start(bool show_cursor) = 0;
virtual int Stop() = 0;
virtual int Pause(int monitor_index) = 0;
virtual int Resume(int monitor_index) = 0;

View File

@@ -152,7 +152,7 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
int ScreenCapturerWgc::Destroy() { return 0; }
int ScreenCapturerWgc::Start() {
int ScreenCapturerWgc::Start(bool show_cursor) {
if (running_ == true) {
LOG_ERROR("Screen capturer already running");
return 0;
@@ -172,7 +172,7 @@ int ScreenCapturerWgc::Start() {
if (sessions_[i].running_) {
LOG_ERROR("Session {} is already running", i);
} else {
sessions_[i].session_->Start();
sessions_[i].session_->Start(show_cursor);
if (i != 0) {
sessions_[i].session_->Pause();

View File

@@ -24,7 +24,7 @@ class ScreenCapturerWgc : public ScreenCapturer,
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;

View File

@@ -29,7 +29,7 @@ class WgcSession {
virtual void RegisterObserver(wgc_session_observer* observer) = 0;
virtual int Start() = 0;
virtual int Start(bool show_cursor) = 0;
virtual int Stop() = 0;
virtual int Pause() = 0;

View File

@@ -55,7 +55,7 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
observer_ = observer;
}
int WgcSessionImpl::Start() {
int WgcSessionImpl::Start(bool show_cursor) {
std::lock_guard locker(lock_);
if (is_running_) return 0;
@@ -91,7 +91,7 @@ int WgcSessionImpl::Start() {
capture_session_.StartCapture();
capture_session_.IsCursorCaptureEnabled(false);
capture_session_.IsCursorCaptureEnabled(show_cursor);
error = 0;
} catch (winrt::hresult_error) {

View File

@@ -48,7 +48,7 @@ class WgcSessionImpl : public WgcSession {
void RegisterObserver(wgc_session_observer* observer) override;
int Start() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause() override;

View File

@@ -0,0 +1,165 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-11
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VERSION_CHECKER_H_
#define _VERSION_CHECKER_H_
#include <httplib.h>
#include <iostream>
#include <nlohmann/json.hpp>
#include <sstream>
#include <string>
#include <vector>
using json = nlohmann::json;
namespace crossdesk {
static std::string latest_release_date_ = "";
std::string ExtractNumericPart(const std::string& ver) {
size_t start = 0;
while (start < ver.size() && !std::isdigit(ver[start])) start++;
size_t end = start;
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
return ver.substr(start, end - start);
}
std::vector<int> SplitVersion(const std::string& ver) {
std::vector<int> nums;
std::istringstream ss(ver);
std::string token;
while (std::getline(ss, token, '.')) {
try {
nums.push_back(std::stoi(token));
} catch (...) {
nums.push_back(0);
}
}
return nums;
}
// extract date from version string (format: v1.2.3-20251113-abc
// or 1.2.3-20251113-abc)
std::string ExtractDateFromVersion(const std::string& version) {
size_t dash1 = version.find('-');
if (dash1 != std::string::npos) {
size_t dash2 = version.find('-', dash1 + 1);
if (dash2 != std::string::npos) {
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1);
bool is_date = true;
for (char c : date_part) {
if (!std::isdigit(c)) {
is_date = false;
break;
}
}
if (is_date) {
// convert YYYYMMDD to YYYY-MM-DD
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
date_part.substr(6, 2);
}
}
}
return "";
}
// compare two dates in YYYY-MM-DD format
bool IsNewerDate(const std::string& date1, const std::string& date2) {
if (date1.empty() || date2.empty()) return false;
// simple string comparison works for ISO date format (YYYY-MM-DD)
return date2 > date1;
}
bool IsNewerVersion(const std::string& current, const std::string& latest) {
auto v1 = SplitVersion(ExtractNumericPart(current));
auto v2 = SplitVersion(ExtractNumericPart(latest));
size_t len = std::max(v1.size(), v2.size());
v1.resize(len, 0);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (v2[i] > v1[i]) return true;
if (v2[i] < v1[i]) return false;
}
// if versions are equal, compare by release date
if (!latest_release_date_.empty()) {
// try to extract date from current version string
std::string current_date = ExtractDateFromVersion(current);
if (!current_date.empty()) {
return IsNewerDate(current_date, latest_release_date_);
} else {
return true;
}
}
return false;
}
bool IsNewerVersionWithDate(const std::string& current_version,
const std::string& current_date,
const std::string& latest_version,
const std::string& latest_date) {
// compare versions
auto v1 = SplitVersion(ExtractNumericPart(current_version));
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
size_t len = std::max(v1.size(), v2.size());
v1.resize(len, 0);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (v2[i] > v1[i]) return true;
if (v2[i] < v1[i]) return false;
}
// if versions are equal, compare by release date
if (!current_date.empty() && !latest_date.empty()) {
return IsNewerDate(current_date, latest_date);
}
// if dates are not available, versions are equal
return false;
}
std::string CheckUpdate() {
httplib::Client cli("https://version.crossdesk.cn");
cli.set_connection_timeout(5);
cli.set_read_timeout(5);
if (auto res = cli.Get("/version.json")) {
if (res->status == 200) {
try {
auto j = json::parse(res->body);
std::string latest = j["version"];
if (j.contains("releaseDate") && j["releaseDate"].is_string()) {
latest_release_date_ = j["releaseDate"];
} else {
latest_release_date_ = "";
}
return latest;
} catch (std::exception&) {
latest_release_date_ = "";
return "";
}
} else {
latest_release_date_ = "";
return "";
}
} else {
latest_release_date_ = "";
return "";
}
}
std::string GetLatestReleaseDate() { return latest_release_date_; }
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-11
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VERSION_CHECKER_H_
#define _VERSION_CHECKER_H_
#include <string>
namespace crossdesk {
std::string CheckUpdate();
bool IsNewerVersion(const std::string& current, const std::string& latest);
} // namespace crossdesk
#endif

View File

@@ -0,0 +1,39 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d39958a..ba21f3c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -117,8 +117,14 @@ if(BUILD_SHARED_LIBS AND WIN32 AND HTTPLIB_COMPILE)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
-if(CMAKE_SYSTEM_NAME MATCHES "Windows" AND ${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
- message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
+if(CMAKE_SYSTEM_NAME MATCHES "Windows")
+ if(CMAKE_SYSTEM_VERSION)
+ if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0")
+ message(SEND_ERROR "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.")
+ endif()
+ else()
+ message(WARNING "Cross-compiling for Windows, but CMAKE_SYSTEM_VERSION is not specified. Assuming target is Windows 10 or later.")
+ endif()
endif()
if(CMAKE_SIZEOF_VOID_P LESS 8)
message(WARNING "Pointer size ${CMAKE_SIZEOF_VOID_P} is not supported. Please use a 64-bit compiler.")
diff --git a/httplib.h b/httplib.h
index e15ba44..728504a 100644
--- a/httplib.h
+++ b/httplib.h
@@ -3083,8 +3083,13 @@ inline bool mmap::open(const char *path) {
auto wpath = u8string_to_wstring(path);
if (wpath.empty()) { return false; }
+#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM | WINAPI_PARTITION_GAMES) && (_WIN32_WINNT >= _WIN32_WINNT_WIN8)
hFile_ = ::CreateFile2(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ,
OPEN_EXISTING, NULL);
+#else
+ hFile_ = ::CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+#endif
if (hFile_ == INVALID_HANDLE_VALUE) { return false; }

117
thirdparty/cpp-httplib/xmake.lua vendored Normal file
View File

@@ -0,0 +1,117 @@
package("cpp-httplib")
set_kind("library", {headeronly = true})
set_homepage("https://github.com/yhirose/cpp-httplib")
set_description("A C++11 single-file header-only cross platform HTTP/HTTPS library.")
set_license("MIT")
set_urls("https://github.com/yhirose/cpp-httplib/archive/refs/tags/$(version).tar.gz",
"https://github.com/yhirose/cpp-httplib.git")
add_versions("v0.26.0", "a66f908f50ccb119769adce44fe1eac75f81b6ffab7c4ac0211bb663ffeb2688")
add_versions("v0.23.1", "410a1347ed6bcbcc4a19af8ed8ad3873fe9fa97731d52db845c4c78f3f9c31e6")
add_versions("v0.22.0", "fcfea48c8f2c386e7085ef8545c8a4875efa30fa6d5cf9dd31f03c6ad038da9d")
add_versions("v0.21.0", "99cfbce46981994d8f34ed21836510d7d408ffac91315bb22c9584a83e220e60")
add_versions("v0.20.1", "b74b1c2c150be2841eba80192f64d93e9a6711985b3ae8aaa1a9cec4863d1dd9")
add_versions("v0.20.0", "18064587e0cc6a0d5d56d619f4cbbcaba47aa5d84d86013abbd45d95c6653866")
add_versions("v0.19.0", "c9b9e0524666e1cd088f0874c57c1ce7c0eaa8552f9f4e15c755d5201fc8c608")
add_versions("v0.18.7", "b7b1e9e4e77565a5a9bc95e761d5df3e7c0e8ca37c90fd78b1b031bc6cb90fc1")
add_versions("v0.18.6", "8900747bba3dda8007f1876175be699036e09e4a25ceeab51196d9365bf1993a")
add_versions("v0.18.5", "731190e97acd63edce57cc3dacd496f57e7743bfc7933da7137cb3e93ec6c9a0")
add_versions("v0.18.3", "a0567bcd6c3fe5cef1b329b96245119047f876b49e06cc129a36a7a8dffe173e")
add_versions("v0.18.1", "405abd8170f2a446fc8612ac635d0db5947c0d2e156e32603403a4496255ff00")
add_versions("v0.17.3", "95bd6dba4241656c59d6f0854d408d14c220f7c71e673319ee27d30aee741aaa")
add_versions("v0.17.0", "dd3ba355b6aa74b7a0bff982ad0de7af6d9672fd83af30aa84eb707974d2a903")
add_versions("v0.16.3", "c1742fc7179aaae2a67ad9bba0740b7e9ffaf4f5e62feef53101ecdef1478716")
add_versions("v0.16.2", "75565bcdf12522929a26fb57a2c7f8cc0e175e27a9ecf51616075f3ea960da44")
add_versions("v0.15.3", "2121bbf38871bb2aafb5f7f2b9b94705366170909f434428352187cb0216124e")
add_versions("v0.15.2", "4afbcf4203249d2cbcb698e46e1f6fb61b479013a84844d6bb1c044e233cab6a")
add_versions("v0.15.1", "8d6a4a40ee8fd3f553b7e895882e60e674bd910883fc1857587dbbabee3cdb91")
add_versions("v0.15.0", "b658e625e283e2c81437a485a95f3acf8b1d32c53d8147b1ccecc8f630e1f7bb")
add_versions("v0.14.3", "dcf6486d9030937636d8a4f820ca9531808fd7edb283893dddbaa05f99357e63")
add_versions("v0.14.2", "dbcf5590e8ed35c6745c2ad659a5ebec92f05187d1506eec24449d6db95e5084")
add_versions("v0.14.1", "2d4fb5544da643e5d0a82585555d8b7502b4137eb321a4abbb075e21d2f00e96")
add_versions("v0.14.0", "3a92248ef8cf2c32ad07f910b8e3052ff2427022b2adb871cf326fb620d2438e")
add_versions("v0.12.6", "24bc594a9efcc08a5a6f3928e848d046d411a88b07bcd6f7f3851227a1f0133e")
add_versions("v0.12.1", "0e56c25c63e730ebd42e2beda6e7cb1b950131d8fc00d3158b1443a8d76f41ca")
add_versions("v0.9.2", "bfef2587a2aa31c85fb361df71c720be97076f8083e4f3881da8572f6a58054f")
add_versions("v0.8.5", "b353f3e7c124a08940d9425aeb7206183fa29857a8f720c162f8fd820cc18f0e")
add_patches("v0.26.0", path.join(os.scriptdir(), "patches/v0.26.0/fix-mingw.diff"), "f7b704e86abd8fd04217056e3ffb01427185e0bae72999246a3b8d13ba23c56a")
add_patches("v0.23.1", path.join(os.scriptdir(), "patches/v0.23.1/fix-mingw.diff"), "d2d8a4c16de3a00d9872526a187257c7ad344eba2a9f109d10b58eadce1c4059")
add_configs("ssl", { description = "Requires OpenSSL", default = false, type = "boolean"})
add_configs("zlib", { description = "Requires Zlib", default = false, type = "boolean"})
add_configs("brotli", { description = "Requires Brotli", default = false, type = "boolean"})
add_configs("exceptions", {description = "Enable the use of C++ exceptions", default = true, type = "boolean"})
add_deps("cmake")
if on_check then
on_check(function (package)
local pkg_ver = package:version()
if package:is_plat("windows") then
local winver = winos.version()
local vs = package:toolchain("msvc"):config("vs")
assert(winver and winver:gt("win8"), "package(httplib): Windows 8 or lower is neither supported nor tested.")
if pkg_ver and pkg_ver:ge("0.19.0") then
assert(vs and tonumber(vs) > 2015, "package(httplib >= 0.19.0): VS 2015 or lower is neither supported nor tested.")
elseif pkg_ver and pkg_ver:ge("0.11.0") then
assert(vs and tonumber(vs) > 2013, "package(httplib >= 0.11.0): VS 2013 or lower is neither supported nor tested.")
end
end
if pkg_ver and pkg_ver:ge("0.15.0") and package:is_plat("msys") then
wprint("package(httplib): MSYS2 (including MinGW) is not officially supported or tested by httplib.")
end
if package:is_plat("android") then
local ndk = package:toolchain("ndk")
local ndk_sdkver = ndk:config("ndk_sdkver")
assert(ndk_sdkver and tonumber(ndk_sdkver) >= 24, "package(httplib): need ndk api level >= 24 for android")
end
if pkg_ver and pkg_ver:ge("0.23.0") then
if package:check_sizeof("void*") == "4" then
raise("package(cpp-httplib >=0.23.0) does not support 32-bit")
end
end
end)
end
on_load(function (package)
if package:config("ssl") then
package:add("deps", "openssl3 3.3.2")
package:add("defines", "CPPHTTPLIB_OPENSSL_SUPPORT")
end
if package:config("zlib") then
package:add("deps", "zlib")
package:add("defines", "CPPHTTPLIB_ZLIB_SUPPORT")
end
if package:config("brotli") then
package:add("deps", "brotli")
package:add("defines", "CPPHTTPLIB_BROTLI_SUPPORT")
end
end)
on_install("!cygwin", function (package)
local configs = {"-DHTTPLIB_COMPILE=OFF"}
table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:is_debug() and "Debug" or "Release"))
table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
table.insert(configs, "-DHTTPLIB_REQUIRE_OPENSSL=" .. (package:config("ssl") and "ON" or "OFF"))
table.insert(configs, "-DHTTPLIB_REQUIRE_ZLIB=" .. (package:config("zlib") and "ON" or "OFF"))
table.insert(configs, "-DHTTPLIB_REQUIRE_BROTLI=" .. (package:config("brotli") and "ON" or "OFF"))
table.insert(configs, "-DHTTPLIB_NO_EXCEPTIONS=" .. (package:config("exceptions") and "OFF" or "ON"))
if package:config("ssl") then
local openssl = package:dep("openssl" .. (package:version():ge("0.15.0") and "3" or ""))
if not openssl:is_system() then
table.insert(configs, "-DOPENSSL_ROOT_DIR=" .. openssl:installdir())
end
end
import("package.tools.cmake").install(package, configs)
end)
on_test(function (package)
assert(package:check_cxxsnippets({test = [[
void test() {
httplib::Client cli("http://cpp-httplib-server.yhirose.repl.co");
}
]]}, {configs = {languages = "c++11"}, includes = "httplib.h"}))
end)

1
thirdparty/xmake.lua vendored Normal file
View File

@@ -0,0 +1 @@
includes("cpp-httplib")

View File

@@ -7,6 +7,12 @@ option("CROSSDESK_VERSION")
set_description("Set CROSSDESK_VERSION for build")
option_end()
option("USE_CUDA")
set_default(false)
set_showmenu(true)
set_description("Use CUDA for hardware codec acceleration")
option_end()
add_rules("mode.release", "mode.debug")
set_languages("c++17")
set_encodings("utf-8")
@@ -16,6 +22,8 @@ set_encodings("utf-8")
-- add_cxxflags("/W4", "/WX")
add_defines("UNICODE")
add_defines("USE_CUDA=" .. (is_config("USE_CUDA", true) and "1" or "0"))
if is_mode("debug") then
add_defines("CROSSDESK_DEBUG")
end
@@ -23,6 +31,8 @@ end
add_requires("spdlog 1.14.1", {system = false})
add_requires("imgui v1.91.5-docking", {configs = {sdl3 = true, sdl3_renderer = true}})
add_requires("openssl3 3.3.2", {system = false})
add_requires("nlohmann_json 3.11.3")
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21")
@@ -45,9 +55,9 @@ elseif is_os("macosx") then
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
end
add_packages("spdlog", "imgui")
add_packages("spdlog", "imgui", "nlohmann_json")
includes("submodules")
includes("submodules", "thirdparty")
target("rd_log")
set_kind("object")
@@ -131,9 +141,15 @@ target("thumbnail")
add_files("src/thumbnail/*.cpp")
add_includedirs("src/thumbnail", {public = true})
target("config_center")
target("autostart")
set_kind("object")
add_deps("rd_log")
add_files("src/autostart/*.cpp")
add_includedirs("src/autostart", {public = true})
target("config_center")
set_kind("object")
add_deps("rd_log", "autostart")
add_files("src/config_center/*.cpp")
add_includedirs("src/config_center", {public = true})
@@ -144,13 +160,21 @@ target("assets")
"src/gui/assets/icons",
"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("src/version_checker/*.cpp")
add_includedirs("src/version_checker", {public = true})
target("gui")
set_kind("object")
add_packages("libyuv")
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")
"device_controller", "thumbnail", "version_checker")
add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp",
"src/gui/windows/*.cpp")
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
@@ -163,4 +187,5 @@ target("gui")
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("src/app/main.cpp")
add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true})