Compare commits

..

1 Commits

Author SHA1 Message Date
dijunkun
eed525ef9a [fix] fix MiniRTC compilation error 2026-02-05 17:33:33 +08:00
90 changed files with 3569 additions and 10505 deletions

View File

@@ -15,28 +15,16 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
build-linux:
name: Build Linux (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
image: crossdesk/ubuntu20.04:latest
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64
runner: ubuntu-22.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh
# Linux amd64
build-linux-amd64:
name: Build on Ubuntu 22.04 amd64
runs-on: ubuntu-22.04
container:
image: ${{ matrix.image }}
image: crossdesk/ubuntu20.04:latest
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
@@ -44,21 +32,86 @@ jobs:
- name: Set legal Debian version
shell: bash
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="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
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@v5
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build CrossDesk
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ./scripts/linux/pkg_amd64.sh
./scripts/linux/pkg_amd64.sh ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}.deb
# Linux arm64
build-linux-arm64:
name: Build on Ubuntu 22.04 arm64
runs-on: ubuntu-22.04-arm
strategy:
matrix:
include:
- arch: arm64
image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
- name: Set legal Debian version
shell: bash
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="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
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
with:
submodules: recursive
@@ -70,13 +123,19 @@ jobs:
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
@@ -112,7 +171,7 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.xmake/packages
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
@@ -123,7 +182,7 @@ jobs:
run: brew install xmake
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -133,13 +192,19 @@ jobs:
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package CrossDesk app
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM}
- name: Upload build artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
@@ -169,7 +234,7 @@ jobs:
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: D:\xmake_global\.xmake\packages
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
@@ -221,7 +286,7 @@ jobs:
Copy-Item $source $target -Force
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -236,45 +301,37 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path certs
[System.IO.File]::WriteAllBytes('certs\crossdesk.cn_root.crt', [Convert]::FromBase64String('${{ secrets.CROSSDESK_CERT_BASE64 }}'))
- name: Package
shell: pwsh
run: |
cd "${{ github.workspace }}\scripts\windows"
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Package Portable
shell: pwsh
run: |
$portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux, build-macos, build-windows-x64]
[build-linux-amd64, build-linux-arm64, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
path: artifacts
@@ -302,7 +359,6 @@ jobs:
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
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
- name: List release files
run: ls -lh release/
@@ -360,10 +416,6 @@ jobs:
"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"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip"
},
"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"

View File

@@ -11,7 +11,7 @@ jobs:
update-version-json:
name: Update version.json with release information
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -25,7 +25,7 @@ jobs:
VERSION_ONLY="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
# Extract date from tag if available (format: v1.2.3-20251113-abc)
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
DATE_STR="${BASH_REMATCH[1]}"
@@ -45,7 +45,7 @@ jobs:
# Use jq to properly escape JSON
RELEASE_BODY="${{ github.event.release.body }}"
RELEASE_NAME="${{ github.event.release.name }}"
# Handle empty values
if [ -z "$RELEASE_BODY" ]; then
RELEASE_BODY=""
@@ -53,15 +53,15 @@ jobs:
if [ -z "$RELEASE_NAME" ]; then
RELEASE_NAME=""
fi
# Save to temporary files for proper handling
echo -n "$RELEASE_BODY" > /tmp/release_body.txt
echo -n "$RELEASE_NAME" > /tmp/release_name.txt
# Use jq to escape JSON strings
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
@@ -85,7 +85,7 @@ jobs:
else
DOWNLOADS_JSON=""
fi
# If downloads is empty, use default structure
if [ -z "$DOWNLOADS_JSON" ]; then
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
@@ -94,10 +94,6 @@ jobs:
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
@@ -118,7 +114,7 @@ jobs:
DOWNLOADS_EOF
)
fi
# Generate version.json using cat and heredoc
cat > version.json << EOF
{
@@ -130,7 +126,7 @@ jobs:
"downloads": ${DOWNLOADS_JSON}
}
EOF
cat version.json
- name: Upload version.json to server
@@ -141,4 +137,4 @@ jobs:
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}
remote_key: ${{ secrets.SERVER_KEY }}

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Xmake cache
.xmake/
build/
certs/
# MacOS Cache
.DS_Store

View File

@@ -181,7 +181,7 @@ sudo docker run -d \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
crossdesk/crossdesk-server:v1.1.3
```
上述命令中,用户需注意的参数如下:
@@ -208,13 +208,13 @@ sudo docker run -d \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
crossdesk/crossdesk-server:v1.1.3
```
**注意**
- **服务器需开放端口COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
@@ -232,30 +232,16 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
2. 点击点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="140" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**)。<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
Windows 平台使用**管理员权限**打开 PowerShell 执行
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
4. 后续如果自托管服务器被重置或因其他原因导致证书更换,可以点击`重置证书指纹`按钮重置客户端保存的证书指纹。<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
### Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -189,7 +189,7 @@ sudo docker run -d \
-e MAX_PORT=xxxxx \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
crossdesk/crossdesk-server:v1.1.3
```
The parameters you need to pay attention to are as follows:
@@ -216,13 +216,13 @@ sudo docker run -d \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
crossdesk/crossdesk-server:v1.1.3
```
**Notes**
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORTMAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you dont mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`.
@@ -243,31 +243,16 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
On Windows, open PowerShell with **administrator privileges** and execute:
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**).<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
4. If the self-hosted server is later reset or the certificate is replaced for any reason, you can click the `Reset Certificate Fingerprint` button to clear the certificate fingerprint saved on the client.<br><br>
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
### Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。

View File

@@ -15,18 +15,21 @@ 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"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -42,9 +45,7 @@ Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
libpipewire-0.3-0, xdg-desktop-portal,
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
libsndio7.0, libxcb-shm0, libpulse0
Recommends: nvidia-cuda-toolkit
Priority: optional
Section: utils
@@ -70,6 +71,7 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -83,9 +85,32 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
chmod +x "$DEBIAN_DIR/postinst"
dpkg-deb --build "$DEB_DIR"
@@ -95,4 +120,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"

View File

@@ -15,18 +15,21 @@ 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"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
cp build/linux/arm64/release/crossdesk "$BIN_DIR"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -42,9 +45,7 @@ Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
libpipewire-0.3-0, xdg-desktop-portal,
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
libsndio7.0, libxcb-shm0, libpulse0
Priority: optional
Section: utils
EOF
@@ -69,6 +70,7 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -82,6 +84,30 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
@@ -94,4 +120,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"

View File

@@ -11,6 +11,9 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -95,6 +98,11 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
# 复制证书文件
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -132,45 +140,21 @@ EOF
chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully."

View File

@@ -11,6 +11,9 @@ IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
@@ -95,6 +98,11 @@ IDENTIFIER="cn.crossdesk.app"
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
# 复制证书文件
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
# 清除应用的权限授权,以便重新授权
# 使用 tccutil 重置录屏权限和辅助功能权限
if command -v tccutil >/dev/null 2>&1; then
@@ -132,45 +140,21 @@ EOF
chmod +x build_pkg_scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
echo "PKG package created successfully."

View File

@@ -1,2 +0,0 @@
// Application icon (IDI_ICON1 = 1, which is the default app icon resource ID)
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"

View File

@@ -12,6 +12,9 @@
; Installer icon path
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
; Certificate path
!define CERT_FILE "${__FILEDIR__}\..\..\certs\crossdesk.cn_root.crt"
; Compression settings
SetCompressor /FINAL lzma
@@ -46,7 +49,7 @@ ShowInstDetails show
Section "MainSection"
; Check if CrossDesk is running
StrCpy $1 "CrossDesk.exe"
StrCpy $1 "crossdesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -72,7 +75,10 @@ installApp:
SetOverwrite ifnewer
; Main application executable path
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
File /oname=crossdesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Copy icon file to installation directory
File "${MUI_ICON}"
; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -82,23 +88,33 @@ installApp:
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\crossdesk.ico"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
SectionEnd
; After installation
Section -Post
ExecWait '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\mt.exe" -manifest "$INSTDIR\crossdesk.manifest" -outputresource:"$INSTDIR\crossdesk.exe";1'
SectionEnd
Section "Cert"
SetOutPath "$APPDATA\CrossDesk\certs"
File /r "${CERT_FILE}"
SectionEnd
Section -AdditionalIcons
; Desktop shortcut
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
; Start menu shortcut
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
SectionEnd
Section "Uninstall"
; Check if CrossDesk is running
StrCpy $1 "CrossDesk.exe"
StrCpy $1 "crossdesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -121,7 +137,7 @@ cancelUninstall:
uninstallApp:
; Delete main executable and uninstaller
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\crossdesk.exe"
Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory
@@ -144,5 +160,5 @@ SectionEnd
; ------ Functions ------
Function LaunchApp
Exec "$INSTDIR\CrossDesk.exe"
Exec "$INSTDIR\crossdesk.exe"
FunctionEnd

View File

@@ -34,16 +34,9 @@
#endif
#ifndef _WIN32
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
Daemon* Daemon::instance_ = nullptr;
#endif
namespace {
constexpr int kRestartDelayMs = 1000;
#ifndef _WIN32
constexpr int kWaitPollIntervalMs = 200;
#endif
} // namespace
// get executable file path
static std::string GetExecutablePath() {
#ifdef _WIN32
@@ -73,35 +66,33 @@ static std::string GetExecutablePath() {
return "";
}
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
void Daemon::stop() {
running_.store(false);
#ifndef _WIN32
stop_requested_ = 1;
#endif
}
bool Daemon::isRunning() const {
#ifndef _WIN32
return running_.load() && (stop_requested_ == 0);
Daemon::Daemon(const std::string& name)
: name_(name)
#ifdef _WIN32
,
running_(false)
#else
return running_.load();
,
running_(true)
#endif
{
}
void Daemon::stop() { running_ = false; }
bool Daemon::isRunning() const { return running_; }
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
running_.store(true);
running_ = true;
return runWithRestart(loop);
#elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI
stop_requested_ = 0;
running_.store(true);
running_ = true;
return runWithRestart(loop);
#else
// linux: Daemonize first, then run with restart monitoring
stop_requested_ = 0;
instance_ = this;
// check if running from terminal before fork
bool from_terminal =
@@ -143,13 +134,29 @@ bool Daemon::start(MainLoopFunc loop) {
}
// set up signal handlers
signal(SIGTERM, [](int) { stop_requested_ = 1; });
signal(SIGINT, [](int) { stop_requested_ = 1; });
signal(SIGTERM, [](int) {
if (instance_) instance_->stop();
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
running_.store(true);
// 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
}
@@ -197,7 +204,8 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
restart_count++;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
}
}
return true;
@@ -229,41 +237,27 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
continue;
}
while (isRunning()) {
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
if (wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result == WAIT_FAILED) {
std::cerr << "Failed waiting child process, error: " << GetLastError()
<< std::endl;
break;
}
}
if (!isRunning()) {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, 3000);
}
DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (!isRunning() || exit_code == 0) {
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(kRestartDelayMs));
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();
@@ -272,39 +266,21 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
_exit(1); // exec failed
} else if (pid > 0) {
int status = 0;
pid_t waited_pid = -1;
while (isRunning()) {
waited_pid = waitpid(pid, &status, WNOHANG);
if (waited_pid == pid) {
break;
}
if (waited_pid < 0 && errno != EINTR) {
break;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kWaitPollIntervalMs));
}
if (!isRunning() && waited_pid != pid) {
kill(pid, SIGTERM);
waited_pid = waitpid(pid, &status, 0);
}
pid_t waited_pid = waitpid(pid, &status, 0);
if (waited_pid < 0) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue;
}
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (!isRunning() || exit_code == 0) {
if (exit_code == 0) {
break; // normal exit
}
restart_count++;
@@ -312,9 +288,6 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else if (WIFSIGNALED(status)) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")"
@@ -325,10 +298,12 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
"(attempt "
<< restart_count << ")" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
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(kRestartDelayMs));
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
}
#endif

View File

@@ -7,11 +7,11 @@
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <atomic>
#include <csignal>
#include <functional>
#include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
@@ -28,10 +28,12 @@ class Daemon {
std::string name_;
bool runWithRestart(MainLoopFunc loop);
#ifndef _WIN32
static volatile std::sig_atomic_t stop_requested_;
#ifdef _WIN32
bool running_;
#else
static Daemon* instance_;
volatile bool running_;
#endif
std::atomic<bool> running_;
};
#endif
#endif

View File

@@ -35,8 +35,11 @@ int main(int argc, char* argv[]) {
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");
crossdesk::ConfigCenter config_center(cache_path + "/config.ini",
cert_path);
enable_daemon = config_center.IsEnableDaemon();
}

View File

@@ -1,8 +1,5 @@
#include "platform.h"
#include <cstdlib>
#include <cstring>
#include "rd_log.h"
#ifdef _WIN32
@@ -111,7 +108,7 @@ std::string GetHostName() {
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
LOG_ERROR("WSAStartup failed");
std::cerr << "WSAStartup failed." << std::endl;
return "";
}
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
@@ -128,25 +125,4 @@ std::string GetHostName() {
#endif
return hostname;
}
bool IsWaylandSession() {
#if defined(__linux__) && !defined(__APPLE__)
const char* session_type = std::getenv("XDG_SESSION_TYPE");
if (session_type) {
if (std::strcmp(session_type, "wayland") == 0 ||
std::strcmp(session_type, "Wayland") == 0) {
return true;
}
if (std::strcmp(session_type, "x11") == 0 ||
std::strcmp(session_type, "X11") == 0) {
return false;
}
}
const char* wayland_display = std::getenv("WAYLAND_DISPLAY");
return wayland_display && wayland_display[0] != '\0';
#else
return false;
#endif
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -13,7 +13,6 @@ namespace crossdesk {
std::string GetMac();
std::string GetHostName();
bool IsWaylandSession();
} // namespace crossdesk
#endif
#endif

View File

@@ -1,63 +0,0 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk

View File

@@ -1,20 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif

View File

@@ -1,279 +0,0 @@
#include "wayland_portal_shared.h"
#include <chrono>
#include <mutex>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <dbus/dbus.h>
#endif
#include "rd_log.h"
namespace crossdesk {
namespace {
std::mutex& SharedSessionMutex() {
static std::mutex mutex;
return mutex;
}
SharedWaylandPortalSessionInfo& SharedSessionInfo() {
static SharedWaylandPortalSessionInfo info;
return info;
}
bool& SharedSessionActive() {
static bool active = false;
return active;
}
int& SharedSessionRefs() {
static int refs = 0;
return refs;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalSessionInterface =
"org.freedesktop.portal.Session";
constexpr int kPortalCloseWaitMs = 100;
void LogCloseDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
struct SessionClosedState {
std::string session_handle;
bool received = false;
};
DBusHandlerResult HandleSessionClosedSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
(void)connection;
auto* state = static_cast<SessionClosedState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalSessionInterface, "Closed")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->session_handle != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
bool BeginSessionClosedWatch(DBusConnection* connection,
const std::string& session_handle,
SessionClosedState* state,
std::string* match_rule_out) {
if (!connection || session_handle.empty() || !state || !match_rule_out) {
return false;
}
state->session_handle = session_handle;
state->received = false;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalSessionInterface) +
"',member='Closed',path='" + session_handle + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogCloseDbusError("dbus_bus_add_match(Session.Closed)", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_add_filter(connection, HandleSessionClosedSignal, state,
nullptr);
*match_rule_out = match_rule;
return true;
}
void EndSessionClosedWatch(DBusConnection* connection, SessionClosedState* state,
const std::string& match_rule) {
if (!connection || !state || match_rule.empty()) {
return;
}
dbus_connection_remove_filter(connection, HandleSessionClosedSignal, state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
}
void WaitForSessionClosed(DBusConnection* connection, SessionClosedState* state,
int timeout_ms = kPortalCloseWaitMs) {
if (!connection || !state) {
return;
}
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!state->received && std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
}
#endif
} // namespace
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info) {
if (!info.connection || info.session_handle.empty() || info.stream_id == 0) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (SharedSessionActive()) {
const auto& active_info = SharedSessionInfo();
if (active_info.session_handle != info.session_handle &&
SharedSessionRefs() > 0) {
return false;
}
}
const bool same_session =
SharedSessionActive() &&
SharedSessionInfo().session_handle == info.session_handle;
SharedSessionInfo() = info;
SharedSessionActive() = true;
if (!same_session || SharedSessionRefs() <= 0) {
SharedSessionRefs() = 1;
}
return true;
}
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out) {
if (!out) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
const auto& info = SharedSessionInfo();
if (require_pointer && !info.pointer_granted) {
return false;
}
++SharedSessionRefs();
*out = info;
return true;
}
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out) {
if (connection_out) {
*connection_out = nullptr;
}
if (session_handle_out) {
session_handle_out->clear();
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
if (SharedSessionRefs() > 0) {
--SharedSessionRefs();
}
if (SharedSessionRefs() > 0) {
return true;
}
if (connection_out) {
*connection_out = SharedSessionInfo().connection;
}
if (session_handle_out) {
*session_handle_out = SharedSessionInfo().session_handle;
}
SharedSessionInfo() = SharedWaylandPortalSessionInfo{};
SharedSessionActive() = false;
SharedSessionRefs() = 0;
return true;
}
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!connection) {
return;
}
if (!session_handle.empty()) {
SessionClosedState close_state;
std::string close_match_rule;
const bool watching_closed = BeginSessionClosedWatch(
connection, session_handle, &close_state, &close_match_rule);
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, session_handle.c_str(), kPortalSessionInterface,
"Close");
if (message) {
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
connection, message, 1000, &error);
if (!reply && dbus_error_is_set(&error)) {
LogCloseDbusError(close_action, &error);
dbus_error_free(&error);
}
if (reply) {
dbus_message_unref(reply);
}
dbus_message_unref(message);
}
if (watching_closed) {
WaitForSessionClosed(connection, &close_state);
if (!close_state.received) {
LOG_WARN("Timed out waiting for portal session to close: {}",
session_handle);
LOG_WARN("Forcing local teardown without waiting for Session.Closed: {}",
session_handle);
EndSessionClosedWatch(connection, &close_state, close_match_rule);
} else {
EndSessionClosedWatch(connection, &close_state, close_match_rule);
LOG_INFO("Portal session closed: {}", session_handle);
}
}
}
dbus_connection_close(connection);
dbus_connection_unref(connection);
#else
(void)connection;
(void)session_handle;
(void)close_action;
#endif
}
} // namespace crossdesk

View File

@@ -1,37 +0,0 @@
/*
* Shared Wayland portal session state used by the Linux Wayland capturer and
* mouse controller so they can reuse one RemoteDesktop session.
*/
#ifndef _WAYLAND_PORTAL_SHARED_H_
#define _WAYLAND_PORTAL_SHARED_H_
#include <cstdint>
#include <string>
struct DBusConnection;
namespace crossdesk {
struct SharedWaylandPortalSessionInfo {
DBusConnection* connection = nullptr;
std::string session_handle;
uint32_t stream_id = 0;
int width = 0;
int height = 0;
bool pointer_granted = false;
};
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info);
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out);
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out);
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action);
} // namespace crossdesk
#endif

View File

@@ -5,8 +5,11 @@
namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ConfigCenter::ConfigCenter(const std::string& config_path,
const std::string& cert_file_path)
: config_path_(config_path),
cert_file_path_(cert_file_path),
cert_file_path_default_(cert_file_path) {
ini_.SetUnicode(true);
Load();
}
@@ -20,14 +23,8 @@ int ConfigCenter::Load() {
return -1;
}
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
}
language_ = static_cast<LANGUAGE>(
ini_.GetLongValue(section_, "language", static_cast<long>(language_)));
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
@@ -73,6 +70,71 @@ int ConfigCenter::Load() {
} else {
coturn_server_port_ = 0;
}
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
} else {
cert_file_path_ = "";
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr && strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
} else {
cert_fingerprint_ = "";
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
} else {
cert_fingerprint_server_host_ = "";
}
const char* default_cert_fingerprint_value =
ini_.GetValue(section_, "default_cert_fingerprint", nullptr);
if (default_cert_fingerprint_value != nullptr &&
strlen(default_cert_fingerprint_value) > 0) {
default_cert_fingerprint_ = default_cert_fingerprint_value;
} else {
default_cert_fingerprint_ = "";
}
const char* default_cert_fingerprint_server_host_value =
ini_.GetValue(section_, "default_cert_fingerprint_server_host", nullptr);
if (default_cert_fingerprint_server_host_value != nullptr &&
strlen(default_cert_fingerprint_server_host_value) > 0) {
default_cert_fingerprint_server_host_ =
default_cert_fingerprint_server_host_value;
} else {
default_cert_fingerprint_server_host_ = "";
}
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
!cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
if (!enable_self_hosted_ && !default_cert_fingerprint_.empty() &&
!default_cert_fingerprint_server_host_.empty() &&
signal_server_host_default_ != default_cert_fingerprint_server_host_) {
LOG_INFO(
"Default server IP changed from {} to {}, clearing old fingerprint",
default_cert_fingerprint_server_host_, signal_server_host_default_);
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
ini_.SaveFile(config_path_.c_str());
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -80,15 +142,6 @@ int ConfigCenter::Load() {
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
} else {
file_transfer_save_path_ = "";
}
return 0;
}
@@ -112,6 +165,19 @@ int ConfigCenter::Save() {
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
}
if (!default_cert_fingerprint_.empty()) {
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
@@ -119,9 +185,6 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -207,6 +270,15 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
}
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
signal_server_host != signal_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
signal_server_host_, signal_server_host);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
signal_server_host_ = signal_server_host;
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -238,6 +310,67 @@ int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
return 0;
}
int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
cert_file_path_ = cert_file_path;
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetCertFingerprint(const std::string& fingerprint) {
cert_fingerprint_ = fingerprint;
cert_fingerprint_server_host_ = signal_server_host_;
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetDefaultCertFingerprint(const std::string& fingerprint) {
default_cert_fingerprint_ = fingerprint;
default_cert_fingerprint_server_host_ = signal_server_host_default_;
ini_.SetValue(section_, "default_cert_fingerprint",
default_cert_fingerprint_.c_str());
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
default_cert_fingerprint_server_host_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearCertFingerprint() {
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::ClearDefaultCertFingerprint() {
default_cert_fingerprint_.clear();
default_cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "default_cert_fingerprint", false);
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -264,12 +397,45 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
const char* cert_file_path_value =
ini_.GetValue(section_, "cert_file_path", nullptr);
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
cert_file_path_ = cert_file_path_value;
}
const char* cert_fingerprint_value =
ini_.GetValue(section_, "cert_fingerprint", nullptr);
if (cert_fingerprint_value != nullptr &&
strlen(cert_fingerprint_value) > 0) {
cert_fingerprint_ = cert_fingerprint_value;
}
const char* cert_fingerprint_server_host_value =
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
if (cert_fingerprint_server_host_value != nullptr &&
strlen(cert_fingerprint_server_host_value) > 0) {
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
}
if (!cert_fingerprint_.empty() && !cert_fingerprint_server_host_.empty() &&
signal_server_host_ != cert_fingerprint_server_host_) {
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
cert_fingerprint_server_host_, signal_server_host_);
cert_fingerprint_.clear();
cert_fingerprint_server_host_.clear();
ini_.Delete(section_, "cert_fingerprint", false);
ini_.Delete(section_, "cert_fingerprint_server_host", false);
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
if (!cert_fingerprint_.empty()) {
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
ini_.SetValue(section_, "cert_fingerprint_server_host",
cert_fingerprint_server_host_.c_str());
}
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
@@ -357,6 +523,16 @@ int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
std::string ConfigCenter::GetCertFingerprint() const {
return cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultCertFingerprint() const {
return default_cert_fingerprint_;
}
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
@@ -369,6 +545,10 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
std::string ConfigCenter::GetDefaultCertFilePath() const {
return cert_file_path_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
@@ -376,19 +556,4 @@ bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
return file_transfer_save_path_;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -15,13 +15,15 @@ namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
public:
explicit ConfigCenter(const std::string& config_path = "config.ini");
explicit ConfigCenter(
const std::string& config_path = "config.ini",
const std::string& cert_file_path = "crossdesk.cn_root.crt");
~ConfigCenter();
// write config
@@ -35,11 +37,15 @@ class ConfigCenter {
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetCertFilePath(const std::string& cert_file_path);
int SetCertFingerprint(const std::string& fingerprint);
int SetDefaultCertFingerprint(const std::string& fingerprint);
int ClearCertFingerprint();
int ClearDefaultCertFingerprint();
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetFileTransferSavePath(const std::string& path);
// read config
@@ -53,14 +59,17 @@ class ConfigCenter {
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetCertFilePath() const;
std::string GetCertFingerprint() const;
std::string GetDefaultCertFingerprint() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
std::string GetFileTransferSavePath() const;
int Load();
int Save();
@@ -71,7 +80,7 @@ class ConfigCenter {
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM;
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;
@@ -83,11 +92,16 @@ class ConfigCenter {
int server_port_default_ = 9099;
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
std::string cert_file_path_ = "";
std::string cert_file_path_default_ = "";
std::string cert_fingerprint_ = "";
std::string cert_fingerprint_server_host_ = "";
std::string default_cert_fingerprint_ = "";
std::string default_cert_fingerprint_server_host_ = "";
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
std::string file_transfer_save_path_ = "";
};
} // namespace crossdesk
#endif
#endif

View File

@@ -1,8 +1,5 @@
#include "keyboard_capturer.h"
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h"
#include "rd_log.h"
@@ -13,7 +10,7 @@ static void* g_user_ptr = nullptr;
static int KeyboardEventHandler(Display* display, XEvent* event) {
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
KeySym keySym = XLookupKeysym(&event->xkey, 0);
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0);
int key_code = XKeysymToKeycode(display, keySym);
bool is_key_down = (event->xkey.type == KeyPress);
@@ -24,9 +21,7 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
return 0;
}
KeyboardCapturer::KeyboardCapturer()
: display_(nullptr), root_(0), running_(false) {
XInitThreads();
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Failed to open X display.");
@@ -34,87 +29,35 @@ KeyboardCapturer::KeyboardCapturer()
}
KeyboardCapturer::~KeyboardCapturer() {
Unhook();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
}
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
}
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
if (running_) {
return 0;
XSelectInput(display_, DefaultRootWindow(display_),
KeyPressMask | KeyReleaseMask);
while (running_) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
root_ = DefaultRootWindow(display_);
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
XFlush(display_);
running_ = true;
const int x11_fd = ConnectionNumber(display_);
event_thread_ = std::thread([this, x11_fd]() {
while (running_) {
while (running_ && XPending(display_) > 0) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
if (!running_) {
break;
}
struct pollfd pfd = {x11_fd, POLLIN, 0};
int poll_ret = poll(&pfd, 1, 50);
if (poll_ret < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERROR("poll for X11 events failed.");
running_ = false;
break;
}
if (poll_ret == 0) {
continue;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
LOG_ERROR("poll got invalid X11 event fd state.");
running_ = false;
break;
}
if ((pfd.revents & POLLIN) == 0) {
continue;
}
}
});
return 0;
}
int KeyboardCapturer::Unhook() {
running_ = false;
if (event_thread_.joinable()) {
event_thread_.join();
}
g_on_key_action = nullptr;
g_user_ptr = nullptr;
if (display_ && root_ != 0) {
XSelectInput(display_, root_, 0);
running_ = false;
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
XFlush(display_);
}

View File

@@ -11,9 +11,6 @@
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic>
#include <thread>
#include "device_controller.h"
namespace crossdesk {
@@ -31,8 +28,7 @@ class KeyboardCapturer : public DeviceController {
private:
Display* display_;
Window root_;
std::atomic<bool> running_;
std::thread event_thread_;
bool running_;
};
} // namespace crossdesk
#endif

View File

@@ -2,7 +2,6 @@
#include <X11/extensions/XTest.h>
#include "platform.h"
#include "rd_log.h"
namespace crossdesk {
@@ -13,17 +12,6 @@ MouseController::~MouseController() { Destroy(); }
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
display_info_list_ = display_info_list;
if (IsWaylandSession()) {
if (InitWaylandPortal()) {
use_wayland_portal_ = true;
LOG_INFO("Mouse controller initialized with Wayland portal backend");
return 0;
}
LOG_WARN(
"Wayland mouse control init failed, falling back to X11/XTest backend");
}
display_ = XOpenDisplay(NULL);
if (!display_) {
LOG_ERROR("Cannot connect to X server");
@@ -37,68 +25,26 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
&minor_version)) {
LOG_ERROR("XTest extension not available");
XCloseDisplay(display_);
display_ = nullptr;
return -2;
}
return 0;
}
void MouseController::UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) {
if (display_info_list.empty()) {
return;
}
display_info_list_ = display_info_list;
if (use_wayland_portal_) {
OnWaylandDisplayInfoListUpdated();
}
if (last_display_index_ < 0 ||
last_display_index_ >= static_cast<int>(display_info_list_.size())) {
last_display_index_ = -1;
last_norm_x_ = -1.0;
last_norm_y_ = -1.0;
}
}
int MouseController::Destroy() {
CleanupWaylandPortal();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
return 0;
}
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
if (remote_action.type != ControlType::mouse) {
return 0;
}
if (use_wayland_portal_) {
return SendWaylandMouseCommand(remote_action, display_index);
}
if (!display_) {
LOG_ERROR("X11 display not initialized");
return -1;
}
switch (remote_action.type) {
case mouse:
switch (remote_action.m.flag) {
case MouseFlag::move: {
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid display index: {}", display_index);
return -2;
}
case MouseFlag::move:
SetMousePosition(
static_cast<int>(remote_action.m.x *
display_info_list_[display_index].width +
@@ -107,7 +53,6 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
display_info_list_[display_index].height +
display_info_list_[display_index].top));
break;
}
case MouseFlag::left_down:
XTestFakeButtonEvent(display_, 1, True, CurrentTime);
XFlush(display_);
@@ -158,39 +103,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
}
void MouseController::SetMousePosition(int x, int y) {
if (!display_) {
return;
}
XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y);
XFlush(display_);
}
void MouseController::SimulateKeyDown(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, True, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateKeyUp(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, False, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateMouseWheel(int direction_button, int count) {
if (!display_) {
return;
}
for (int i = 0; i < count; ++i) {
XTestFakeButtonEvent(display_, direction_button, True, CurrentTime);
XTestFakeButtonEvent(display_, direction_button, False, CurrentTime);
}
XFlush(display_);
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -11,16 +11,10 @@
#include <X11/Xutil.h>
#include <unistd.h>
#include <functional>
#include <cstdint>
#include <string>
#include <vector>
#include "device_controller.h"
struct DBusConnection;
struct DBusMessageIter;
namespace crossdesk {
class MouseController : public DeviceController {
@@ -32,47 +26,18 @@ class MouseController : public DeviceController {
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(const std::vector<DisplayInfo>& display_info_list);
private:
void SimulateKeyDown(int kval);
void SimulateKeyUp(int kval);
void SetMousePosition(int x, int y);
void SimulateMouseWheel(int direction_button, int count);
bool InitWaylandPortal();
void CleanupWaylandPortal();
int SendWaylandMouseCommand(RemoteAction remote_action, int display_index);
void OnWaylandDisplayInfoListUpdated();
bool NotifyWaylandPointerMotion(double dx, double dy);
bool NotifyWaylandPointerMotionAbsolute(uint32_t stream, double x, double y);
bool NotifyWaylandPointerButton(int button, uint32_t state);
bool NotifyWaylandPointerAxisDiscrete(uint32_t axis, int32_t steps);
bool SendWaylandPortalVoidCall(const char* method_name,
const std::function<void(DBusMessageIter*)>&
append_args);
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
Display* display_ = nullptr;
Window root_ = 0;
std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0;
int screen_height_ = 0;
bool use_wayland_portal_ = false;
DBusConnection* dbus_connection_ = nullptr;
std::string wayland_session_handle_;
int last_display_index_ = -1;
double last_norm_x_ = -1.0;
double last_norm_y_ = -1.0;
bool logged_wayland_display_info_ = false;
uintptr_t last_logged_wayland_stream_ = 0;
int last_logged_wayland_width_ = 0;
int last_logged_wayland_height_ = 0;
WaylandAbsoluteMode wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown;
bool wayland_absolute_disabled_logged_ = false;
uint32_t wayland_absolute_stream_id_ = 0;
bool using_shared_wayland_session_ = false;
};
} // namespace crossdesk
#endif
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,156 +1,228 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <string>
#include <unordered_map>
#include <vector>
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include "localization_data.h"
#include <string>
#include <vector>
#if _WIN32
#include <Windows.h>
#endif
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
namespace detail {
namespace crossdesk {
inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
}
namespace localization {
using TranslationTable =
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>;
static std::vector<std::string> local_desktop = {
reinterpret_cast<const char*>(u8"本桌面"), "Local Desktop"};
static std::vector<std::string> local_id = {
reinterpret_cast<const char*>(u8"本机ID"), "Local ID"};
static std::vector<std::string> local_id_copied_to_clipboard = {
reinterpret_cast<const char*>(u8"已复制到剪贴板"), "Copied to clipboard"};
static std::vector<std::string> password = {
reinterpret_cast<const char*>(u8"密码"), "Password"};
static std::vector<std::string> max_password_len = {
reinterpret_cast<const char*>(u8"最大6个字符"), "Max 6 chars"};
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
{"en-US", row.en},
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
}
static std::vector<std::string> remote_desktop = {
reinterpret_cast<const char*>(u8"控制远程桌面"), "Control Remote Desktop"};
static std::vector<std::string> remote_id = {
reinterpret_cast<const char*>(u8"对端ID"), "Remote ID"};
static std::vector<std::string> connect = {
reinterpret_cast<const char*>(u8"连接"), "Connect"};
static std::vector<std::string> recent_connections = {
reinterpret_cast<const char*>(u8"近期连接"), "Recent Connections"};
static std::vector<std::string> disconnect = {
reinterpret_cast<const char*>(u8"断开连接"), "Disconnect"};
static std::vector<std::string> fullscreen = {
reinterpret_cast<const char*>(u8"全屏"), " Fullscreen"};
static std::vector<std::string> show_net_traffic_stats = {
reinterpret_cast<const char*>(u8"显示流量统计"), "Show Net Traffic Stats"};
static std::vector<std::string> hide_net_traffic_stats = {
reinterpret_cast<const char*>(u8"隐藏流量统计"), "Hide Net Traffic Stats"};
static std::vector<std::string> video = {
reinterpret_cast<const char*>(u8"视频"), "Video"};
static std::vector<std::string> audio = {
reinterpret_cast<const char*>(u8"音频"), "Audio"};
static std::vector<std::string> data = {reinterpret_cast<const char*>(u8"数据"),
"Data"};
static std::vector<std::string> total = {
reinterpret_cast<const char*>(u8"总计"), "Total"};
static std::vector<std::string> in = {reinterpret_cast<const char*>(u8"输入"),
"In"};
static std::vector<std::string> out = {reinterpret_cast<const char*>(u8"输出"),
"Out"};
static std::vector<std::string> loss_rate = {
reinterpret_cast<const char*>(u8"丢包率"), "Loss Rate"};
static std::vector<std::string> exit_fullscreen = {
reinterpret_cast<const char*>(u8"退出全屏"), "Exit fullscreen"};
static std::vector<std::string> control_mouse = {
reinterpret_cast<const char*>(u8"控制"), "Control"};
static std::vector<std::string> release_mouse = {
reinterpret_cast<const char*>(u8"释放"), "Release"};
static std::vector<std::string> audio_capture = {
reinterpret_cast<const char*>(u8"声音"), "Audio"};
static std::vector<std::string> mute = {
reinterpret_cast<const char*>(u8" 静音"), " Mute"};
static std::vector<std::string> settings = {
reinterpret_cast<const char*>(u8"设置"), "Settings"};
static std::vector<std::string> language = {
reinterpret_cast<const char*>(u8"语言:"), "Language:"};
static std::vector<std::string> language_zh = {
reinterpret_cast<const char*>(u8"中文"), "Chinese"};
static std::vector<std::string> language_en = {
reinterpret_cast<const char*>(u8"英文"), "English"};
static std::vector<std::string> video_quality = {
reinterpret_cast<const char*>(u8"视频质量:"), "Video Quality:"};
static std::vector<std::string> video_frame_rate = {
reinterpret_cast<const char*>(u8"画面采集帧率:"),
"Video Capture Frame Rate:"};
static std::vector<std::string> video_quality_high = {
reinterpret_cast<const char*>(u8""), "High"};
static std::vector<std::string> video_quality_medium = {
reinterpret_cast<const char*>(u8""), "Medium"};
static std::vector<std::string> video_quality_low = {
reinterpret_cast<const char*>(u8""), "Low"};
static std::vector<std::string> video_encode_format = {
reinterpret_cast<const char*>(u8"视频编码格式:"), "Video Encode Format:"};
static std::vector<std::string> av1 = {reinterpret_cast<const char*>(u8"AV1"),
"AV1"};
static std::vector<std::string> h264 = {
reinterpret_cast<const char*>(u8"H.264"), "H.264"};
static std::vector<std::string> enable_hardware_video_codec = {
reinterpret_cast<const char*>(u8"启用硬件编解码器:"),
"Enable Hardware Video Codec:"};
static std::vector<std::string> enable_turn = {
reinterpret_cast<const char*>(u8"启用中继服务:"), "Enable TURN Service:"};
static std::vector<std::string> enable_srtp = {
reinterpret_cast<const char*>(u8"启用SRTP:"), "Enable SRTP:"};
static std::vector<std::string> self_hosted_server_config = {
reinterpret_cast<const char*>(u8"自托管服务器配置"),
"Self-Hosted Server Config"};
static std::vector<std::string> self_hosted_server_settings = {
reinterpret_cast<const char*>(u8"自托管服务器设置"),
"Self-Hosted Server Settings"};
static std::vector<std::string> self_hosted_server_address = {
reinterpret_cast<const char*>(u8"服务器地址:"), "Server Address:"};
static std::vector<std::string> self_hosted_server_port = {
reinterpret_cast<const char*>(u8"信令服务端口:"), "Signal Service Port:"};
static std::vector<std::string> self_hosted_server_coturn_server_port = {
reinterpret_cast<const char*>(u8"中继服务端口:"), "Relay Service Port:"};
static std::vector<std::string> self_hosted_server_certificate_path = {
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
static std::vector<std::string> select_a_file = {
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
static std::vector<std::string> reset_cert_fingerprint = {
reinterpret_cast<const char*>(u8"重置证书指纹"),
"Reset Certificate Fingerprint"};
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
"OK"};
static std::vector<std::string> cancel = {
reinterpret_cast<const char*>(u8"取消"), "Cancel"};
inline TranslationTable BuildTranslationTable() {
TranslationTable table;
for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
static std::vector<std::string> new_password = {
reinterpret_cast<const char*>(u8"请输入六位密码:"),
"Please input a six-char password:"};
return table;
}
inline const TranslationTable& GetTranslationTable() {
static const TranslationTable table = BuildTranslationTable();
return table;
}
inline const std::string& GetTranslatedText(const std::string& key,
int language_index) {
static const std::string kEmptyText = "";
const auto& table = GetTranslationTable();
const auto key_it = table.find(key);
if (key_it == table.end()) {
return kEmptyText;
}
const auto& localized_values = key_it->second;
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
const auto exact_it = localized_values.find(language_code);
if (exact_it != localized_values.end()) {
return exact_it->second;
}
const auto english_it = localized_values.find("en-US");
if (english_it != localized_values.end()) {
return english_it->second;
}
const auto chinese_it = localized_values.find("zh-CN");
if (chinese_it != localized_values.end()) {
return chinese_it->second;
}
return kEmptyText;
}
} // namespace detail
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText("exit_program", normalized_index);
if (utf8_text.empty()) {
cached_text = L"Exit";
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = L"Exit";
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length - 1));
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
wide_length);
return cached_text.c_str();
}
#endif
} // namespace localization
} // namespace crossdesk
#endif
static std::vector<std::string> input_password = {
reinterpret_cast<const char*>(u8"请输入密码:"), "Please input password:"};
static std::vector<std::string> validate_password = {
reinterpret_cast<const char*>(u8"验证密码中..."), "Validate password ..."};
static std::vector<std::string> reinput_password = {
reinterpret_cast<const char*>(u8"请重新输入密码"),
"Please input password again"};
static std::vector<std::string> remember_password = {
reinterpret_cast<const char*>(u8"记住密码"), "Remember password"};
static std::vector<std::string> signal_connected = {
reinterpret_cast<const char*>(u8"已连接服务器"), "Connected"};
static std::vector<std::string> signal_disconnected = {
reinterpret_cast<const char*>(u8"未连接服务器"), "Disconnected"};
static std::vector<std::string> p2p_connected = {
reinterpret_cast<const char*>(u8"对等连接已建立"), "P2P Connected"};
static std::vector<std::string> p2p_disconnected = {
reinterpret_cast<const char*>(u8"对等连接已断开"), "P2P Disconnected"};
static std::vector<std::string> p2p_connecting = {
reinterpret_cast<const char*>(u8"正在建立对等连接..."),
"P2P Connecting ..."};
static std::vector<std::string> p2p_failed = {
reinterpret_cast<const char*>(u8"对等连接失败"), "P2P Failed"};
static std::vector<std::string> p2p_closed = {
reinterpret_cast<const char*>(u8"对等连接已关闭"), "P2P closed"};
static std::vector<std::string> no_such_id = {
reinterpret_cast<const char*>(u8"无此ID"), "No such ID"};
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"};
static std::vector<std::string> notification = {
reinterpret_cast<const char*>(u8"通知"), "Notification"};
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> release_date = {
reinterpret_cast<const char*>(u8"发布日期: "), "Release Date: "};
static std::vector<std::string> access_website = {
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "};
static std::vector<std::string> update = {
reinterpret_cast<const char*>(u8"更新"), "Update"};
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"),
"Confirm to delete this connection"};
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"};
static std::vector<std::string> select_file = {
reinterpret_cast<const char*>(u8"选择文件"), "Select File"};
static std::vector<std::string> file_transfer_progress = {
reinterpret_cast<const char*>(u8"文件传输进度"), "File Transfer Progress"};
static std::vector<std::string> queued = {
reinterpret_cast<const char*>(u8"队列中"), "Queued"};
static std::vector<std::string> sending = {
reinterpret_cast<const char*>(u8"正在传输"), "Sending"};
static std::vector<std::string> completed = {
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
static std::vector<std::string> failed = {
reinterpret_cast<const char*>(u8"失败"), "Failed"};
static std::vector<std::string> controller = {
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
static std::vector<std::string> file_transfer = {
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
static std::vector<std::string> connection_status = {
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
"Minimize to system tray when exit:"};
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
#endif
#ifdef __APPLE__
static std::vector<std::string> request_permissions = {
reinterpret_cast<const char*>(u8"权限请求"), "Request Permissions"};
static std::vector<std::string> screen_recording_permission = {
reinterpret_cast<const char*>(u8"屏幕录制权限"),
"Screen Recording Permission"};
static std::vector<std::string> accessibility_permission = {
reinterpret_cast<const char*>(u8"辅助功能权限"),
"Accessibility Permission"};
static std::vector<std::string> permission_required_message = {
reinterpret_cast<const char*>(u8"该应用需要授权以下权限:"),
"The application requires the following permissions:"};
#endif
} // namespace localization
} // namespace crossdesk
#endif

View File

@@ -1,166 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_DATA_H_
#define _LOCALIZATION_DATA_H_
namespace crossdesk {
namespace localization {
namespace detail {
struct TranslationRow {
const char* key;
const char* zh;
const char* en;
const char* ru;
};
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制", "Control", u8"Управление") \
X(release_mouse, u8"释放", "Release", u8"Освободить") \
X(audio_capture, u8"声音", "Audio", u8"Звук") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"高", "High", u8"Высокое") \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
X(video_quality_low, u8"低", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
};
} // namespace detail
} // namespace localization
} // namespace crossdesk
#endif

View File

@@ -1,36 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DEVICE_PRESENCE_H_
#define _DEVICE_PRESENCE_H_
#include <mutex>
#include <string>
#include <unordered_map>
class DevicePresence {
public:
void SetOnline(const std::string& device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string& device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
return cache_.count(device_id) > 0 && cache_.at(device_id);
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
#endif

View File

@@ -17,7 +17,7 @@ int Render::LocalWindow() {
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("LocalDesktopWindow",
@@ -42,11 +42,11 @@ int Render::LocalWindow() {
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::BeginChild(
"LocalDesktopPanel",
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
@@ -101,7 +101,7 @@ int Render::LocalWindow() {
ImGuiCol_WindowBg,
ImVec4(1.0f, 1.0f, 1.0f, 1.0f - (float)time_duration));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
@@ -177,11 +177,10 @@ int Render::LocalWindow() {
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding,
window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::Begin("ResetPasswordWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -300,4 +299,4 @@ int Render::LocalWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -9,7 +9,7 @@ int Render::RecentConnectionsWindow() {
ImGuiIO& io = ImGui::GetIO();
float recent_connection_window_width = io.DisplaySize.x;
float recent_connection_window_height =
io.DisplaySize.y * (0.455f - STATUS_BAR_HEIGHT);
io.DisplaySize.y * (0.46f - STATUS_BAR_HEIGHT);
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * 0.55f),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -17,7 +17,7 @@ int Render::RecentConnectionsWindow() {
ImGui::BeginChild(
"RecentConnectionsWindow",
ImVec2(recent_connection_window_width, recent_connection_window_height),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
@@ -64,7 +64,7 @@ int Render::ShowRecentConnections() {
ImGui::BeginChild(
"RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
@@ -122,8 +122,6 @@ int Render::ShowRecentConnections() {
it.second.remote_host_name = "unknown";
}
bool online = device_presence_.IsOnline(it.second.remote_id);
ImVec2 image_screen_pos = ImVec2(
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
@@ -134,29 +132,6 @@ int Render::ShowRecentConnections() {
ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height));
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
std::string display_host_name_with_presence =
it.second.remote_host_name + " " +
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_]);
ImGui::Text("%s", display_host_name_with_presence.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 circle_pos =
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
image_screen_pos.y + recent_connection_image_height * 0.12f);
ImU32 fill_color =
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
ImU32 border_color = IM_COL32(255, 255, 255, 255);
float dot_radius = recent_connection_image_height * 0.06f;
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
100);
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
// remote id display button
{
@@ -180,6 +155,14 @@ int Render::ShowRecentConnections() {
ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", it.second.remote_host_name.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
@@ -259,9 +242,6 @@ int Render::ShowRecentConnections() {
if (show_confirm_delete_connection_) {
ConfirmDeleteConnection();
}
if (show_offline_warning_window_) {
OfflineWarningWindow();
}
return 0;
}
@@ -273,10 +253,10 @@ int Render::ConfirmDeleteConnection() {
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -319,45 +299,4 @@ int Render::ConfirmDeleteConnection() {
ImGui::PopStyleVar();
return 0;
}
int Render::OfflineWarningWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("OfflineWarningWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
ImGui::SetCursorPosX(window_width * 0.43f);
ImGui::SetCursorPosY(window_height * 0.67f);
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_offline_warning_window_ = false;
}
auto text_width = ImGui::CalcTextSize(offline_warning_text_.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", offline_warning_text_.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -18,7 +18,7 @@ int Render::RemoteWindow() {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * TITLE_BAR_HEIGHT),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
ImGui::BeginChild("RemoteDesktopWindow",
@@ -48,7 +48,7 @@ int Render::RemoteWindow() {
ImGui::BeginChild(
"RemoteDesktopWindow_1",
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
@@ -165,21 +165,7 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
}
int Render::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
bool remember_password) {
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
@@ -209,10 +195,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 9.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_ = ConnectionStatus::Connecting;
show_connection_status_window_ = true;
props->control_window_max_height_ = title_bar_height_ * 6.0f;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
@@ -270,4 +253,4 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -7,8 +7,6 @@
#include <X11/Xlib.h>
#endif
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -16,6 +14,7 @@
#include <string>
#include <thread>
#include "OPPOSans_Regular.h"
#include "clipboard.h"
#include "device_controller_factory.h"
#include "fa_regular_400.h"
@@ -37,32 +36,6 @@
namespace crossdesk {
namespace {
const ImWchar* GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) {
ImGuiIO& io = ImGui::GetIO();
ImFontGlyphRangesBuilder builder;
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
ImVector<ImWchar> built_ranges;
builder.BuildRanges(&built_ranges);
glyph_ranges.assign(built_ranges.Data,
built_ranges.Data + built_ranges.Size);
}
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
}
bool CanReadFontFile(const char* font_path) {
if (!font_path) {
return false;
}
std::ifstream font_file(font_path, std::ios::binary);
return font_file.good();
}
#if defined(__linux__) && !defined(__APPLE__)
inline bool X11GetDisplayAndWindow(SDL_Window* window, Display** display_out,
::Window* x11_window_out) {
@@ -325,7 +298,7 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window,
float mouse_grab_padding = render->title_bar_button_width_ * 0.16f;
if (area->y < render->title_bar_button_width_ &&
area->y > mouse_grab_padding &&
area->x < window_width - render->title_bar_button_width_ * 3.0f &&
area->x < window_width - render->title_bar_button_width_ * 4.0f &&
area->x > mouse_grab_padding) {
return SDL_HITTEST_DRAGGABLE;
}
@@ -506,8 +479,7 @@ int Render::LoadSettingsFromCacheFile() {
thumbnail_ = std::make_shared<Thumbnail>(cache_path_ + "/thumbnails/",
aes128_key_, aes128_iv_);
language_button_value_ = localization::detail::ClampLanguageIndex(
(int)config_center_->GetLanguage());
language_button_value_ = (int)config_center_->GetLanguage();
video_quality_button_value_ = (int)config_center_->GetVideoQuality();
video_frame_rate_button_value_ = (int)config_center_->GetVideoFrameRate();
video_encode_format_button_value_ =
@@ -520,16 +492,6 @@ int Render::LoadSettingsFromCacheFile() {
enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
// File transfer save path
{
std::string saved_path = config_center_->GetFileTransferSavePath();
strncpy(file_transfer_save_path_buf_, saved_path.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
file_transfer_save_path_last_ = saved_path;
}
language_button_value_last_ = language_button_value_;
video_quality_button_value_last_ = video_quality_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_;
@@ -581,9 +543,8 @@ int Render::ScreenCapturerInit() {
if (0 == screen_capturer_init_ret) {
LOG_INFO("Init screen capturer success");
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
if (display_info_list_.empty()) {
display_info_list_ = screen_capturer_->GetDisplayInfoList();
}
return 0;
} else {
@@ -596,22 +557,10 @@ int Render::ScreenCapturerInit() {
}
int Render::StartScreenCapturer() {
if (!screen_capturer_) {
LOG_INFO("Screen capturer instance missing, recreating before start");
if (0 != ScreenCapturerInit()) {
LOG_ERROR("Recreate screen capturer failed");
return -1;
}
}
if (screen_capturer_) {
LOG_INFO("Start screen capturer, show cursor: {}", show_cursor_);
const int ret = screen_capturer_->Start(show_cursor_);
if (ret != 0) {
LOG_ERROR("Start screen capturer failed: {}", ret);
return ret;
}
screen_capturer_->Start(show_cursor_);
}
return 0;
@@ -664,42 +613,14 @@ int Render::StartMouseController() {
LOG_INFO("Device controller factory is nullptr");
return -1;
}
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
if (!screen_capturer_) {
return 1;
}
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (latest_display_info.empty() ||
latest_display_info[0].handle == nullptr) {
return 1;
}
}
if (screen_capturer_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
}
#endif
mouse_controller_ = (MouseController*)device_controller_factory_->Create(
DeviceControllerFactory::Device::Mouse);
if (!mouse_controller_) {
LOG_ERROR("Create mouse controller failed");
return -1;
}
int mouse_controller_init_ret = mouse_controller_->Init(display_info_list_);
if (0 != mouse_controller_init_ret) {
LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
return mouse_controller_init_ret;
}
return 0;
@@ -751,11 +672,13 @@ int Render::CreateConnectionPeer() {
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
std::string tls_cert_fingerprint;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
tls_cert_fingerprint = config_center_->GetCertFingerprint();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
bool use_cached_id = false;
@@ -816,6 +739,7 @@ int Render::CreateConnectionPeer() {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
tls_cert_fingerprint = config_center_->GetDefaultCertFingerprint();
params_.user_id = client_id_with_password_;
}
@@ -839,6 +763,7 @@ int Render::CreateConnectionPeer() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
// peer config
strncpy((char*)params_.signal_server_ip, signal_server_ip.c_str(),
@@ -859,6 +784,30 @@ int Render::CreateConnectionPeer() {
strncpy((char*)params_.turn_server_password, "crossdeskpw",
sizeof(params_.turn_server_password) - 1);
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
strncpy(params_.tls_cert_fingerprint, tls_cert_fingerprint.c_str(),
sizeof(params_.tls_cert_fingerprint) - 1);
params_.tls_cert_fingerprint[sizeof(params_.tls_cert_fingerprint) - 1] = '\0';
if (config_center_->IsSelfHosted()) {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetCertFingerprint(fingerprint);
LOG_INFO("Saved self-hosted certificate fingerprint: {}", fingerprint);
}
};
params_.fingerprint_user_data = this;
} else {
params_.on_cert_fingerprint = [](const char* fingerprint, void* user_data) {
Render* render = static_cast<Render*>(user_data);
if (render && render->config_center_) {
render->config_center_->SetDefaultCertFingerprint(fingerprint);
LOG_INFO("Saved default server certificate fingerprint: {}",
fingerprint);
}
};
params_.fingerprint_user_data = this;
}
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
@@ -879,7 +828,6 @@ int Render::CreateConnectionPeer() {
params_.on_receive_video_frame = OnReceiveVideoBufferCb;
params_.on_signal_status = OnSignalStatusCb;
params_.on_signal_message = OnSignalMessageCb;
params_.on_connection_status = OnConnectionStatusCb;
params_.on_net_status_report = OnNetStatusReport;
@@ -917,38 +865,11 @@ int Render::AudioDeviceInit() {
desired_out.format = SDL_AUDIO_S16;
desired_out.channels = 1;
auto open_stream = [&]() -> bool {
output_stream_ = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
return output_stream_ != nullptr;
};
if (!open_stream()) {
#if defined(__linux__) && !defined(__APPLE__)
LOG_WARN(
"Failed to open output stream with driver [{}]: {}",
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER") : "(default)",
SDL_GetError());
setenv("SDL_AUDIODRIVER", "dummy", 1);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
SDL_GetError());
return -1;
}
if (!open_stream()) {
LOG_ERROR("Failed to open output stream with dummy driver: {}",
SDL_GetError());
return -1;
}
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
#else
output_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&desired_out, nullptr, nullptr);
if (!output_stream_) {
LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
return -1;
#endif
}
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
@@ -966,24 +887,9 @@ int Render::AudioDeviceDestroy() {
}
void Render::UpdateInteractions() {
#if defined(__linux__) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
is_wayland_session && !start_screen_capturer_ &&
screen_capturer_is_started_ && !start_mouse_controller_ &&
mouse_controller_is_started_;
if (stop_wayland_mouse_before_screen) {
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
"cleanly release the shared portal session");
StopMouseController();
mouse_controller_is_started_ = false;
}
#endif
if (start_screen_capturer_ && !screen_capturer_is_started_) {
if (0 == StartScreenCapturer()) {
screen_capturer_is_started_ = true;
}
StartScreenCapturer();
screen_capturer_is_started_ = true;
} else if (!start_screen_capturer_ && screen_capturer_is_started_) {
StopScreenCapturer();
screen_capturer_is_started_ = false;
@@ -998,25 +904,14 @@ void Render::UpdateInteractions() {
}
if (start_mouse_controller_ && !mouse_controller_is_started_) {
if (0 == StartMouseController()) {
mouse_controller_is_started_ = true;
}
StartMouseController();
mouse_controller_is_started_ = true;
} else if (!start_mouse_controller_ && mouse_controller_is_started_) {
StopMouseController();
mouse_controller_is_started_ = false;
}
#if defined(__linux__) && !defined(__APPLE__)
if (screen_capturer_is_started_ && screen_capturer_ && mouse_controller_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
}
}
#endif
if (start_keyboard_capturer_ && focus_on_stream_window_) {
if (start_keyboard_capturer_ && foucs_on_stream_window_) {
if (!keyboard_capturer_is_started_) {
StartKeyboardCapturer();
keyboard_capturer_is_started_ = true;
@@ -1037,10 +932,9 @@ int Render::CreateMainWindow() {
ImGui::SetCurrentContext(main_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Main Window", (int)main_window_width_,
(int)main_window_height_,
"Remote Desk", (int)main_window_width_, (int)main_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
SDL_WINDOW_HIDDEN,
&main_window_, &main_renderer_)) {
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
return -1;
@@ -1060,7 +954,6 @@ int Render::CreateMainWindow() {
(int)(server_window_width_default_ * dpi_scale_);
server_window_normal_height_ =
(int)(server_window_height_default_ * dpi_scale_);
window_rounding_ = window_rounding_default_ * dpi_scale_;
SDL_SetWindowSize(main_window_, (int)main_window_width_,
(int)main_window_height_);
@@ -1071,8 +964,6 @@ int Render::CreateMainWindow() {
// for window region action
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&main_windows_system_chinese_font_);
ImGuiStyle& style = ImGui::GetStyle();
@@ -1129,10 +1020,9 @@ int Render::CreateStreamWindow() {
ImGui::SetCurrentContext(stream_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Stream Window", (int)stream_window_width_,
"Stream window", (int)stream_window_width_,
(int)stream_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS,
&stream_window_, &stream_renderer_)) {
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
SDL_GetError());
@@ -1146,8 +1036,6 @@ int Render::CreateStreamWindow() {
// for window region action
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&stream_windows_system_chinese_font_);
ImGuiStyle& style = ImGui::GetStyle();
@@ -1182,18 +1070,13 @@ int Render::DestroyStreamWindow() {
if (stream_renderer_) {
SDL_DestroyRenderer(stream_renderer_);
stream_renderer_ = nullptr;
}
if (stream_window_) {
SDL_DestroyWindow(stream_window_);
stream_window_ = nullptr;
}
stream_window_created_ = false;
focus_on_stream_window_ = false;
stream_window_grabbed_ = false;
control_mouse_ = false;
return 0;
}
@@ -1208,12 +1091,12 @@ int Render::CreateServerWindow() {
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Server Window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
if (!SDL_CreateWindowAndRenderer("Server window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY |
SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
SDL_GetError());
return -1;
@@ -1295,16 +1178,13 @@ int Render::DestroyServerWindow() {
if (server_renderer_) {
SDL_DestroyRenderer(server_renderer_);
server_renderer_ = nullptr;
}
if (server_window_) {
SDL_DestroyWindow(server_window_);
server_window_ = nullptr;
}
server_window_created_ = false;
server_window_inited_ = false;
return 0;
}
@@ -1317,68 +1197,78 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) {
io.IniFilename = NULL; // disable imgui.ini
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
// Load Fonts
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = false;
io.Fonts->AddFontFromMemoryTTF(OPPOSans_Regular_ttf, OPPOSans_Regular_ttf_len,
font_size, &config,
io.Fonts->GetGlyphRangesChineseFull());
config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, 30.0f,
&config, icon_ranges);
// Load system Chinese font as fallback
config.MergeMode = false;
config.FontDataOwnedByAtlas = false;
if (system_chinese_font_out) {
*system_chinese_font_out = nullptr;
}
ImFont* ui_font = nullptr;
const ImWchar* multilingual_ranges = GetMultilingualGlyphRanges();
#if defined(_WIN32)
const char* base_font_paths[] = {
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/simsun.ttc", nullptr};
// Windows: Try Microsoft YaHei (微软雅黑) first, then SimSun (宋体)
const char* font_paths[] = {"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/simsun.ttc", nullptr};
#elif defined(__APPLE__)
const char* base_font_paths[] = {
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/System/Library/Fonts/SFNS.ttf", nullptr};
// macOS: Try PingFang SC first, then STHeiti
const char* font_paths[] = {"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/STHeiti Medium.ttc",
nullptr};
#else
const char* base_font_paths[] = {
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
// Linux: Try common Chinese fonts
const char* font_paths[] = {
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
nullptr};
"/usr/share/fonts/truetype/arphic/uming.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", nullptr};
#endif
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
if (!CanReadFontFile(base_font_paths[i])) {
continue;
}
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
&config, multilingual_ranges);
if (ui_font != nullptr) {
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
for (int i = 0; font_paths[i] != nullptr; i++) {
std::ifstream font_file(font_paths[i], std::ios::binary);
if (font_file.good()) {
font_file.close();
if (!system_chinese_font_out) {
break;
}
*system_chinese_font_out =
io.Fonts->AddFontFromFileTTF(font_paths[i], font_size, &config,
io.Fonts->GetGlyphRangesChineseFull());
if (*system_chinese_font_out != nullptr) {
// Merge FontAwesome icons into the Chinese font
config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &config, icon_ranges);
config.MergeMode = false;
LOG_INFO("Loaded system Chinese font with icons: {}", font_paths[i]);
break;
}
}
}
if (!ui_font) {
ui_font = io.Fonts->AddFontDefault(&config);
}
if (!ui_font) {
LOG_WARN("Failed to initialize base UI font");
ImGui::StyleColorsLight();
return 0;
}
ImFontConfig icon_config = config;
icon_config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &icon_config, icon_ranges);
io.FontDefault = ui_font;
if (system_chinese_font_out) {
*system_chinese_font_out = ui_font;
// If no system font found, use default font
if (system_chinese_font_out && *system_chinese_font_out == nullptr) {
*system_chinese_font_out = io.Fonts->AddFontDefault(&config);
// Merge FontAwesome icons into the default font
config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &config, icon_ranges);
config.MergeMode = false;
LOG_WARN("System Chinese font not found, using default font with icons");
}
ImGui::StyleColorsLight();
@@ -1418,8 +1308,6 @@ int Render::DrawMainWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
ImGuiCond_Always);
@@ -1428,7 +1316,6 @@ int Render::DrawMainWindow() {
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
TitleBar(true);
@@ -1449,7 +1336,6 @@ int Render::DrawMainWindow() {
ImGui::Render();
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
SDL_RenderClear(main_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
SDL_RenderPresent(main_renderer_);
@@ -1499,7 +1385,6 @@ int Render::DrawStreamWindow() {
ImGui::Render();
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
SDL_RenderClear(stream_renderer_);
// std::shared_lock lock(client_properties_mutex_);
@@ -1507,8 +1392,10 @@ int Render::DrawStreamWindow() {
auto props = it.second;
if (props->tab_selected_) {
SDL_FRect render_rect_f = {
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
static_cast<float>(props->stream_render_rect_.x),
static_cast<float>(props->stream_render_rect_.y),
static_cast<float>(props->stream_render_rect_.w),
static_cast<float>(props->stream_render_rect_.h)};
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
&render_rect_f);
}
@@ -1524,20 +1411,24 @@ int Render::DrawServerWindow() {
LOG_ERROR("Server context is null");
return -1;
}
if (server_window_) {
int w = 0;
int h = 0;
SDL_GetWindowSize(server_window_, &w, &h);
if (w > 0 && h > 0) {
server_window_width_ = (float)w;
server_window_height_ = (float)h;
}
}
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO& io = ImGui::GetIO();
server_window_width_ = io.DisplaySize.x;
server_window_height_ = io.DisplaySize.y;
ServerWindow();
ImGui::Render();
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
SDL_SetRenderDrawColor(server_renderer_, 255, 255, 255, 255);
SDL_RenderClear(server_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
SDL_RenderPresent(server_renderer_);
@@ -1549,10 +1440,10 @@ int Render::Run() {
if (!latest_version_info_.empty() &&
latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
latest_version_ = 'v' + latest_version_info_["version"].get<std::string>();
latest_version_ = latest_version_info_["version"];
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
release_notes_ = latest_version_info_["releaseNotes"];
} else {
release_notes_ = "";
}
@@ -1567,11 +1458,13 @@ int Render::Run() {
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
cert_path_ =
(path_manager_->GetCertPath() / "crossdesk.cn_root.crt").string();
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = path_manager_->GetLogPath().string();
cache_path_ = path_manager_->GetCachePath().string();
config_center_ =
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini", cert_path_);
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
@@ -1585,6 +1478,8 @@ int Render::Run() {
} else {
signal_server_port_self_[0] = '\0';
}
strncpy(cert_file_path_, cert_path_.c_str(), sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
} else {
std::cerr << "Failed to create PathManager" << std::endl;
return -1;
@@ -1613,27 +1508,16 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
void Render::InitializeSettings() {
LoadSettingsFromCacheFile();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
if (localization_language_index_ == 0) {
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (localization_language_index_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
} else {
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_;
localization_language_index_ = language_button_value_;
if (localization_language_index_ != 0 && localization_language_index_ != 1) {
localization_language_index_ = 0;
LOG_ERROR("Invalid language index: [{}], use [0] by default",
localization_language_index_);
}
}
void Render::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
if (!getenv("SDL_AUDIODRIVER")) {
// Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency.
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
}
#endif
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("Error: {}", SDL_GetError());
return;
@@ -1670,8 +1554,7 @@ void Render::InitializeModules() {
std::shared_lock lock(client_properties_mutex_);
int ret = -1;
for (const auto& [remote_id, props] : client_properties_) {
if (props && props->peer_ && props->connection_established_ &&
props->enable_mouse_control_) {
if (props && props->peer_ && props->connection_established_) {
ret = SendReliableDataFrame(props->peer_, data, size,
props->clipboard_label_.c_str());
if (ret != 0) {
@@ -1725,8 +1608,6 @@ void Render::MainLoop() {
UpdateLabels();
HandleRecentConnections();
HandleConnectionStatusChange();
HandlePendingPresenceProbe();
HandleStreamWindow();
HandleServerWindow();
@@ -1766,124 +1647,10 @@ void Render::HandleRecentConnections() {
LOG_INFO("Load recent connection thumbnails");
}
reload_recent_connections_ = false;
recent_connection_ids_.clear();
for (const auto& conn : recent_connections_) {
recent_connection_ids_.push_back(conn.first);
}
need_to_send_recent_connections_ = true;
}
}
}
void Render::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
if (!recent_connection_ids_.empty()) {
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto& id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
size_t pos = std::string::npos;
if (pos_y != std::string::npos &&
(pos_n == std::string::npos || pos_y < pos_n)) {
pos = pos_y;
} else if (pos_n != std::string::npos) {
pos = pos_n;
}
if (pos != std::string::npos) {
pure_id = pure_id.substr(0, pos);
}
j["devices"].push_back(pure_id);
}
auto s = j.dump();
SendSignalMessage(peer_, s.data(), s.size());
}
}
need_to_send_recent_connections_ = false;
}
void Render::HandlePendingPresenceProbe() {
bool has_action = false;
bool should_connect = false;
bool remember_password = false;
std::string remote_id;
std::string password;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (!pending_presence_probe_ || !pending_presence_result_ready_) {
return;
}
has_action = true;
should_connect = pending_presence_online_;
remote_id = pending_presence_remote_id_;
password = pending_presence_password_;
remember_password = pending_presence_remember_password_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
if (!has_action) {
return;
}
if (should_connect) {
ConnectTo(remote_id, password.c_str(), remember_password, true);
return;
}
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
}
int Render::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) {
if (!signal_connected_ || !peer_) {
return -1;
}
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = true;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_ = remote_id;
pending_presence_password_ = password ? password : "";
pending_presence_remember_password_ = remember_password;
}
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array({remote_id});
auto s = j.dump();
int ret = SendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
return ret;
}
void Render::HandleStreamWindow() {
if (need_to_create_stream_window_) {
CreateStreamWindow();
@@ -1916,12 +1683,6 @@ void Render::HandleServerWindow() {
void Render::Cleanup() {
Clipboard::StopMonitoring();
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
if (screen_capturer_) {
screen_capturer_->Destroy();
delete screen_capturer_;
@@ -1934,6 +1695,12 @@ void Render::Cleanup() {
speaker_capturer_ = nullptr;
}
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
if (keyboard_capturer_) {
delete keyboard_capturer_;
keyboard_capturer_ = nullptr;
@@ -2015,9 +1782,9 @@ void Render::CleanupPeers() {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
StopMouseController();
StopScreenCapturer();
StopSpeakerCapturer();
StopMouseController();
StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
@@ -2295,36 +2062,25 @@ void Render::UpdateRenderRect() {
float render_area_height = props->render_window_height_;
props->stream_render_rect_last_ = props->stream_render_rect_;
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
render_area_width, render_area_height};
if (render_area_width < render_area_height * video_ratio) {
rect_f.x = props->render_window_x_;
rect_f.y = std::abs(render_area_height -
render_area_width * video_ratio_reverse) /
2.0f +
props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_width * video_ratio_reverse;
props->stream_render_rect_ = {
(int)props->render_window_x_,
(int)(abs(render_area_height -
render_area_width * video_ratio_reverse) /
2 +
(int)props->render_window_y_),
(int)render_area_width,
(int)(render_area_width * video_ratio_reverse)};
} else if (render_area_width > render_area_height * video_ratio) {
rect_f.x =
std::abs(render_area_width - render_area_height * video_ratio) / 2.0f +
props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_height * video_ratio;
rect_f.h = render_area_height;
props->stream_render_rect_ = {
(int)abs(render_area_width - render_area_height * video_ratio) / 2,
(int)props->render_window_y_, (int)(render_area_height * video_ratio),
(int)render_area_height};
} else {
rect_f.x = props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_height;
props->stream_render_rect_ = {
(int)props->render_window_x_, (int)props->render_window_y_,
(int)render_area_width, (int)render_area_height};
}
props->stream_render_rect_f_ = rect_f;
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
static_cast<int>(std::lround(rect_f.y)),
static_cast<int>(std::lround(rect_f.w)),
static_cast<int>(std::lround(rect_f.h))};
}
}
@@ -2442,7 +2198,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_WINDOW_FOCUS_GAINED:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
focus_on_stream_window_ = true;
foucs_on_stream_window_ = true;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = true;
@@ -2452,7 +2208,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_WINDOW_FOCUS_LOST:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
focus_on_stream_window_ = false;
foucs_on_stream_window_ = false;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = false;
@@ -2465,23 +2221,11 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL: {
Uint32 mouse_window_id = 0;
if (event.type == SDL_EVENT_MOUSE_MOTION) {
mouse_window_id = event.motion.windowID;
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
mouse_window_id = event.button.windowID;
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
mouse_window_id = event.wheel.windowID;
}
if (focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == mouse_window_id) {
case SDL_EVENT_MOUSE_WHEEL:
if (foucs_on_stream_window_) {
ProcessMouseEvent(event);
}
break;
}
default:
if (event.type == STREAM_REFRESH_EVENT) {
@@ -2701,4 +2445,4 @@ void Render::ProcessFileDropEvent(const SDL_Event& event) {
// Handle the dropped file on server window as needed
}
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -25,7 +25,6 @@
#include "IconsFontAwesome6.h"
#include "config_center.h"
#include "device_controller_factory.h"
#include "device_presence.h"
#include "imgui.h"
#include "imgui_impl_sdl3.h"
#include "imgui_impl_sdlrenderer3.h"
@@ -95,7 +94,7 @@ class Render {
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_control_button_pressed_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
@@ -160,7 +159,6 @@ class Render {
SDL_Texture* stream_texture_ = nullptr;
uint8_t* argb_buffer_ = nullptr;
int argb_buffer_size_ = 0;
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
SDL_Rect stream_render_rect_;
SDL_Rect stream_render_rect_last_;
ImVec2 control_window_pos_;
@@ -194,8 +192,6 @@ class Render {
void UpdateLabels();
void UpdateInteractions();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleStreamWindow();
void HandleServerWindow();
void Cleanup();
@@ -230,6 +226,7 @@ class Render {
int RecentConnectionsWindow();
int SettingWindow();
int SelfHostedServerWindow();
int ShowSimpleFileBrowser();
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
int AboutWindow();
@@ -237,7 +234,6 @@ class Render {
bool ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props);
int ShowRecentConnections();
bool OpenUrl(const std::string& url);
void Hyperlink(const std::string& label, const std::string& url,
const float window_width);
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
@@ -245,9 +241,7 @@ class Render {
private:
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string& remote_id,
const char* password, bool remember_password);
bool remember_password);
int CreateMainWindow();
int DestroyMainWindow();
int CreateStreamWindow();
@@ -262,12 +256,9 @@ class Render {
int DrawStreamWindow();
int DrawServerWindow();
int ConfirmDeleteConnection();
int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props);
void DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props);
#ifdef __APPLE__
int RequestPermissionWindow();
bool CheckScreenRecordingPermission();
@@ -296,9 +287,6 @@ class Render {
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void OnSignalMessageCb(const char* message, size_t size,
void* user_data);
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
size_t user_id_size, void* user_data);
@@ -393,6 +381,7 @@ class Render {
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string cert_path_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
@@ -415,12 +404,9 @@ class Render {
// recent connections
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresence device_presence_;
bool need_to_send_recent_connections_ = true;
// main window render
SDL_Window* main_window_ = nullptr;
@@ -446,11 +432,11 @@ class Render {
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool start_keyboard_capturer_ = true;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool foucs_on_stream_window_ = false;
bool main_window_minimized_ = false;
uint32_t last_main_minimize_request_tick_ = 0;
uint32_t last_stream_minimize_request_tick_ = 0;
@@ -550,8 +536,6 @@ class Render {
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
// server window collapsed mode
bool server_window_collapsed_ = false;
@@ -587,11 +571,9 @@ class Render {
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = "";
std::string offline_warning_text_ = "";
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
@@ -642,15 +624,16 @@ class Render {
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_quality_button_value_ = 0;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_hardware_video_codec_ = false;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
char cert_file_path_[256] = "";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
@@ -666,11 +649,10 @@ class Render {
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_ = "";
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
std::string tls_cert_path_self_ = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = "";
@@ -694,14 +676,7 @@ class Render {
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = "";
std::string selected_server_remote_hostname_ = "";
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::string pending_presence_remote_id_ = "";
std::string pending_presence_password_ = "";
bool pending_presence_remember_password_ = false;
FileTransferState file_transfer_;
};
} // namespace crossdesk
#endif
#endif

View File

@@ -1,4 +1,3 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
@@ -7,7 +6,6 @@
#include <fstream>
#include <limits>
#include <unordered_map>
#include <unordered_set>
#include "clipboard.h"
#include "device_controller.h"
@@ -22,65 +20,6 @@
namespace crossdesk {
void Render::OnSignalMessageCb(const char* message, size_t size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render || !message || size == 0) {
return;
}
std::string s(message, size);
auto j = nlohmann::json::parse(s, nullptr, false);
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
return;
}
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto& dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
if (!dev.contains("id") || !dev["id"].is_string()) {
continue;
}
if (!dev.contains("online") || !dev["online"].is_boolean()) {
continue;
}
std::string id = dev["id"].get<std::string>();
bool online = dev["online"].get<bool>();
render->device_presence_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
render->pending_presence_probe_mutex_);
if (render->pending_presence_probe_ &&
render->pending_presence_remote_id_ == id) {
render->pending_presence_result_ready_ = true;
render->pending_presence_online_ = online;
}
}
}
}
} else if (type == "presence_update") {
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
j["online"].is_boolean()) {
std::string id = j["id"].get<std::string>();
bool online = j["online"].get<bool>();
if (!id.empty()) {
render->device_presence_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
render->pending_presence_probe_mutex_);
if (render->pending_presence_probe_ &&
render->pending_presence_remote_id_ == id) {
render->pending_presence_result_ready_ = true;
render->pending_presence_online_ = online;
}
}
}
}
}
}
int Render::SendKeyCommand(int key_code, bool is_down) {
RemoteAction remote_action;
remote_action.type = ControlType::keyboard;
@@ -91,16 +30,17 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
}
remote_action.k.key_value = key_code;
std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_
: controlled_remote_id_;
if (!target_id.empty()) {
if (client_properties_.find(target_id) != client_properties_.end()) {
auto props = client_properties_[target_id];
if (props->connection_status_ == ConnectionStatus::Connected &&
props->peer_) {
if (!controlled_remote_id_.empty()) {
// std::shared_lock lock(client_properties_mutex_);
if (client_properties_.find(controlled_remote_id_) !=
client_properties_.end()) {
auto props = client_properties_[controlled_remote_id_];
if (props->connection_status_ == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
if (props->peer_) {
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
}
}
@@ -110,67 +50,10 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
int Render::ProcessMouseEvent(const SDL_Event& event) {
controlled_remote_id_ = "";
int video_width, video_height = 0;
int render_width, render_height = 0;
float ratio_x, ratio_y = 0;
RemoteAction remote_action;
float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y;
auto normalize_cursor_to_window_space = [&](float* x, float* y) {
if (!x || !y || !stream_window_) {
return;
}
int window_width = 0;
int window_height = 0;
int pixel_width = 0;
int pixel_height = 0;
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
pixel_height <= 0) {
return;
}
if ((window_width != pixel_width || window_height != pixel_height) &&
(*x > static_cast<float>(window_width) + 1.0f ||
*y > static_cast<float>(window_height) + 1.0f)) {
const float scale_x =
static_cast<float>(window_width) / static_cast<float>(pixel_width);
const float scale_y =
static_cast<float>(window_height) / static_cast<float>(pixel_height);
*x *= scale_x;
*y *= scale_y;
static bool logged_pixel_to_window_conversion = false;
if (!logged_pixel_to_window_conversion) {
LOG_INFO(
"Mouse coordinate space converted from pixels to window units: "
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
window_width, window_height, pixel_width, pixel_height, scale_x,
scale_y);
logged_pixel_to_window_conversion = true;
}
}
};
if (event.type == SDL_EVENT_MOUSE_MOTION) {
cursor_x = event.motion.x;
cursor_y = event.motion.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
cursor_x = event.button.x;
cursor_y = event.button.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
cursor_x = last_mouse_event.motion.x;
cursor_y = last_mouse_event.motion.y;
}
const bool is_pointer_position_event =
(event.type == SDL_EVENT_MOUSE_MOTION ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
// std::shared_lock lock(client_properties_mutex_);
for (auto& it : client_properties_) {
@@ -179,24 +62,23 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
continue;
}
const SDL_FRect render_rect = props->stream_render_rect_f_;
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
continue;
}
if (is_pointer_position_event && cursor_x >= render_rect.x &&
cursor_x <= render_rect.x + render_rect.w && cursor_y >= render_rect.y &&
cursor_y <= render_rect.y + render_rect.h) {
if (event.button.x >= props->stream_render_rect_.x &&
event.button.x <=
props->stream_render_rect_.x + props->stream_render_rect_.w &&
event.button.y >= props->stream_render_rect_.y &&
event.button.y <=
props->stream_render_rect_.y + props->stream_render_rect_.h) {
controlled_remote_id_ = it.first;
last_mouse_event.motion.x = cursor_x;
last_mouse_event.motion.y = cursor_y;
last_mouse_event.button.x = cursor_x;
last_mouse_event.button.y = cursor_y;
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
last_mouse_event.button.x = event.button.x;
last_mouse_event.button.y = event.button.y;
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
remote_action.m.x =
(float)(event.button.x - props->stream_render_rect_.x) / render_width;
remote_action.m.y =
(float)(event.button.y - props->stream_render_rect_.y) /
render_height;
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
remote_action.type = ControlType::mouse;
@@ -222,18 +104,21 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
}
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
break;
remote_action.m.flag = MouseFlag::move;
}
std::string msg = remote_action.to_json();
if (props->peer_) {
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 >= render_rect.x &&
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
last_mouse_event.button.y >= render_rect.y &&
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
last_mouse_event.button.x >= props->stream_render_rect_.x &&
last_mouse_event.button.x <= props->stream_render_rect_.x +
props->stream_render_rect_.w &&
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) {
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
@@ -260,18 +145,17 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
remote_action.m.s = roundUp(scroll_x);
}
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
(std::max)(render_rect.w, 1.0f);
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
(std::max)(render_rect.h, 1.0f);
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
remote_action.m.x =
(float)(last_mouse_event.button.x - props->stream_render_rect_.x) /
render_width;
remote_action.m.y =
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height;
if (props->control_bar_hovered_) {
continue;
}
std::string msg = remote_action.to_json();
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
@@ -437,14 +321,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
// Update output directory from config
std::string configured_path =
render->config_center_->GetFileTransferSavePath();
if (!configured_path.empty()) {
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
} else if (receiver.OutputDir().empty()) {
receiver = FileReceiver(); // re-init with default desktop path
}
receiver.SetOnSendAck([render,
remote_user_id](const FileTransferAck& ack) -> int {
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
@@ -468,13 +344,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
return;
} else if (source_id == render->clipboard_label_) {
if (size > 0) {
std::string remote_user_id(user_id, user_id_size);
auto props =
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
if (props && !props->enable_mouse_control_) {
return;
}
std::string clipboard_text(data, size);
if (!Clipboard::SetText(clipboard_text)) {
LOG_ERROR("Failed to set clipboard content from remote");
@@ -571,9 +440,9 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
const double bps =
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
if (bps > 0.0) {
const double capped =
(std::min)(bps, static_cast<double>(
(std::numeric_limits<uint32_t>::max)()));
const double capped = (std::min)(
bps,
static_cast<double>((std::numeric_limits<uint32_t>::max)()));
estimated_rate_bps = static_cast<uint32_t>(capped);
}
}
@@ -689,6 +558,13 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
remote_action.i.host_name, remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]",
render->connection_host_names_[remote_id]);
for (int i = 0; i < remote_action.i.display_num; i++) {
render->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(remote_action);
}
} else {
@@ -728,7 +604,6 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
render->signal_connected_ = true;
render->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id);
} else if (SignalStatus::SignalFailed == status) {
render->signal_connected_ = false;
@@ -738,6 +613,10 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
render->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", client_id);
render->config_center_->ClearCertFingerprint();
}
} else {
if (client_id.rfind("C-", 0) != 0) {
@@ -765,6 +644,9 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalFingerprintMismatch == status) {
props->signal_connected_ = false;
LOG_ERROR("[{}] signal server fingerprint mismatch", remote_id);
}
}
}
@@ -838,17 +720,13 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
0, (int)render->title_bar_height_,
(int)render->stream_window_width_,
(int)(render->stream_window_height_ - render->title_bar_height_)};
props->stream_render_rect_f_ = {
0.0f, render->title_bar_height_, render->stream_window_width_,
render->stream_window_height_ - render->title_bar_height_};
render->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
props->connection_established_ = false;
props->enable_mouse_control_ = false;
props->mouse_control_button_pressed_ = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
@@ -866,8 +744,6 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
event.user.data1 = props.get();
SDL_PushEvent(&event);
render->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
@@ -947,7 +823,12 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true;
render->remote_client_id_ = remote_id;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = 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;
@@ -957,8 +838,6 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
@@ -968,19 +847,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
})) {
render->need_to_destroy_server_window_ = true;
render->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on
// subsequent reconnects.
render->start_screen_capturer_ = true;
LOG_INFO("Keeping Wayland screen capturer running after "
"disconnect to preserve reconnect stability");
} else {
render->start_screen_capturer_ = false;
}
#else
render->start_screen_capturer_ = false;
#endif
render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
@@ -992,10 +859,6 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
}
render->connection_status_.erase(remote_id);
render->connection_host_names_.erase(remote_id);
if (render->screen_capturer_) {
render->screen_capturer_->ResetToInitialMonitor();
}
}
if (std::all_of(render->connection_status_.begin(),
@@ -1144,4 +1007,4 @@ void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -138,10 +138,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float line_padding = title_bar_height_ * 0.12f;
float line_thickness = title_bar_height_ * 0.07f;
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
if (props->control_bar_expand_) {
ImGui::SetCursorPosX(props->is_control_bar_in_left_
? props->control_window_width_ * 0.03f
? props->control_window_width_ * 1.03f
: props->control_window_width_ * 0.17f);
ImDrawList* draw_list = ImGui::GetWindowDrawList();
@@ -198,21 +198,24 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float mouse_y = ImGui::GetCursorScreenPos().y;
float disable_mouse_x = mouse_x + line_padding;
float disable_mouse_y = mouse_y + line_padding;
std::string mouse = ICON_FA_COMPUTER_MOUSE;
std::string mouse = props->mouse_control_button_pressed_
? ICON_FA_COMPUTER_MOUSE
: ICON_FA_COMPUTER_MOUSE;
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(mouse.c_str(), ImVec2(button_width, button_height))) {
if (props->connection_established_) {
start_keyboard_capturer_ = !start_keyboard_capturer_;
props->control_mouse_ = !props->control_mouse_;
props->enable_mouse_control_ = !props->enable_mouse_control_;
props->mouse_control_button_pressed_ =
!props->mouse_control_button_pressed_;
props->mouse_control_button_label_ =
props->enable_mouse_control_
props->mouse_control_button_pressed_
? localization::release_mouse[localization_language_index_]
: localization::control_mouse[localization_language_index_];
}
}
if (!props->enable_mouse_control_) {
if (!props->mouse_control_button_pressed_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
ImVec2(mouse_x + button_width - line_padding,
mouse_y + button_height - line_padding),
@@ -356,11 +359,13 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SameLine();
}
float expand_button_pos_x = props->control_bar_expand_
? (props->is_control_bar_in_left_
? props->control_window_width_ * 0.917f
: props->control_window_width_ * 0.03f)
: props->control_window_width_ * 0.11f;
float expand_button_pos_x =
props->control_bar_expand_ ? (props->is_control_bar_in_left_
? props->control_window_width_ * 1.917f
: props->control_window_width_ * 0.03f)
: (props->is_control_bar_in_left_
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.23f);
ImGui::SetCursorPosX(expand_button_pos_x);
@@ -392,7 +397,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
}
int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
ImGui::SetCursorPos(ImVec2(props->is_control_bar_in_left_
? props->control_window_width_ * 1.02f
: props->control_window_width_ * 0.02f,
props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f);
if (ImGui::BeginTable("NetTrafficStats", 4, ImGuiTableFlags_BordersH,
@@ -453,33 +460,12 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
LossRateDisplay(props->net_traffic_stats_.total_inbound_stats.loss_rate);
ImGui::TableNextColumn();
ImGui::Text("FPS:");
ImGui::Text("FPS");
ImGui::TableNextColumn();
ImGui::Text("%d", props->fps_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text("%s:",
localization::resolution[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text("%dx%d", props->video_width_, props->video_height_);
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::TableNextColumn();
ImGui::Text(
"%s:",
localization::connection_mode[localization_language_index_].c_str());
ImGui::TableNextColumn();
ImGui::Text(
"%s",
props->traversal_mode_ == 0
? localization::connection_mode_direct[localization_language_index_]
.c_str()
: localization::connection_mode_relay[localization_language_index_]
.c_str());
ImGui::EndTable();
}
ImGui::SetWindowFontScale(1.0f);

View File

@@ -9,27 +9,25 @@ int Render::StatusBar() {
float status_bar_width = io.DisplaySize.x;
float status_bar_height = io.DisplaySize.y * STATUS_BAR_HEIGHT;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
ImGui::SetNextWindowPos(ImVec2(0, io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::BeginChild("StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Borders,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor(2);
ImGui::BeginChild(
"StatusBar", ImVec2(status_bar_width, status_bar_height),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoBringToFrontOnFocus);
ImVec2 dot_pos = ImVec2(status_bar_width * 0.025f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.5f));
ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
ImColor(signal_connected_ ? 0.0f : 1.0f,
signal_connected_ ? 1.0f : 0.0f, 0.0f),
100);
draw_list->AddCircle(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100);
ImGui::SetWindowFontScale(0.6f);
draw_list->AddText(
@@ -42,7 +40,8 @@ int Render::StatusBar() {
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor();
ImGui::EndChild();
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -2,7 +2,6 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
@@ -24,7 +23,7 @@ int Render::TitleBar(bool main_window) {
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
title_bar_width = io.DisplaySize.x;
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * TITLE_BAR_HEIGHT;
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
@@ -46,12 +45,12 @@ int Render::TitleBar(bool main_window) {
title_bar_button_height = title_bar_button_height_;
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
ImVec2(title_bar_width, title_bar_height_padding),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
@@ -286,20 +285,8 @@ int Render::TitleBar(bool main_window) {
float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK;
bool close_button_clicked = false;
if (main_window) {
close_button_clicked = RoundedCornerButton(
close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height), 8.5f,
ImDrawFlags_RoundCornersTopRight, true, IM_COL32(0, 0, 0, 0),
IM_COL32(250, 0, 0, 255), IM_COL32(255, 0, 0, 128));
} else {
close_button_clicked =
ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height));
}
if (close_button_clicked) {
if (ImGui::Button(close_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) {
#if _WIN32
if (enable_minimize_to_tray_) {
tray_->MinimizeToTray();
@@ -312,7 +299,6 @@ int Render::TitleBar(bool main_window) {
}
#endif
}
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
xmark_pos_y - xmark_size / 2 + 0.75f),
ImVec2(xmark_pos_x + xmark_size / 2 - 1.5f,
@@ -329,4 +315,4 @@ int Render::TitleBar(bool main_window) {
ImGui::EndChild();
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
GetCursorPos(&pt);
HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 1001,
localization::GetExitProgramLabel(language_index_));
localization::exit_program[language_index_]);
SetForegroundWindow(hwnd_message_only_);
int cmd =
@@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
}
return true;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -1,10 +1,6 @@
#include <cstdlib>
#include <string>
#if defined(_WIN32)
#include <windows.h>
#endif
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
@@ -12,44 +8,11 @@
namespace crossdesk {
bool Render::OpenUrl(const std::string& url) {
#if defined(_WIN32)
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len <= 0) {
return false;
}
std::wstring wide_url(static_cast<size_t>(wide_len), L'\0');
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, &wide_url[0], wide_len);
if (!wide_url.empty() && wide_url.back() == L'\0') {
wide_url.pop_back();
}
std::wstring cmd = L"cmd.exe /c start \"\" \"" + wide_url + L"\"";
STARTUPINFOW startup_info = {sizeof(startup_info)};
PROCESS_INFORMATION process_info = {};
if (!CreateProcessW(nullptr, &cmd[0], nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup_info,
&process_info)) {
return false;
}
CloseHandle(process_info.hThread);
CloseHandle(process_info.hProcess);
return true;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
return system(cmd.c_str()) == 0;
#else
std::string cmd = "xdg-open " + url;
return system(cmd.c_str()) == 0;
#endif
}
void Render::Hyperlink(const std::string& label, const std::string& url,
const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::TextUnformatted(label.c_str());
ImGui::SetCursorPosX(window_width * 0.1f);
ImGui::Text("%s", label.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
@@ -60,7 +23,14 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
OpenUrl(url);
#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
}
}
}
@@ -70,7 +40,7 @@ int Render::AboutWindow() {
float about_window_width = title_bar_button_width_ * 7.5f;
float about_window_height = latest_version_.empty()
? title_bar_button_width_ * 4.0f
: title_bar_button_width_ * 4.9f;
: title_bar_button_width_ * 4.6f;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(
@@ -82,8 +52,8 @@ int Render::AboutWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::Begin(
localization::about[localization_language_index_].c_str(), nullptr,
@@ -104,24 +74,17 @@ int Render::AboutWindow() {
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", text.c_str());
if (0) {
std::string new_version_available =
if (update_available_) {
std::string latest_version =
localization::new_version_available[localization_language_index_] +
": ";
ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", new_version_available.c_str());
": " + latest_version_;
std::string access_website =
localization::access_website[localization_language_index_];
ImGui::SetCursorPosX((about_window_width -
ImGui::CalcTextSize(latest_version_.c_str()).x) /
2.0f);
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
ImGui::Spacing();
} else {
ImGui::Text("%s", "");
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);
@@ -130,7 +93,7 @@ int Render::AboutWindow() {
ImGui::Text("%s", license_text.c_str());
ImGui::SetCursorPosX(about_window_width * 0.445f);
ImGui::SetCursorPosY(about_window_height * 0.8f);
ImGui::SetCursorPosY(about_window_height * 0.75f);
// OK
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
show_about_window_ = false;

View File

@@ -15,10 +15,10 @@ bool Render::ConnectionStatusWindow(
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::Begin("ConnectionStatusWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
@@ -36,18 +36,6 @@ bool Render::ConnectionStatusWindow(
text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == props->connection_status_) {
text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);

View File

@@ -42,74 +42,50 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
}
}
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 1.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1, 1, 1, 1));
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, window_rounding_ * 1.5f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
float y_boundary = fullscreen_button_pressed_ ? 0.0f : title_bar_height_;
float container_x = 0.0f;
float container_y = y_boundary;
float container_w = stream_window_width_;
float container_h = stream_window_height_ - y_boundary;
ImGui::SetNextWindowSize(
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(container_w, container_h), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(container_x, container_y), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, title_bar_height_), ImGuiCond_Once);
float pos_x = 0;
float pos_y = 0;
float y_boundary = fullscreen_button_pressed_ ? 0 : title_bar_height_;
std::string container_window_title =
props->remote_id_ + "ControlContainerWindow";
ImGui::Begin(container_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoBackground);
ImGui::PopStyleVar();
ImVec2 container_pos = ImGui::GetWindowPos();
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
props->control_bar_hovered_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
ImVec2 delta = ImGui::GetIO().MouseDelta;
pos_x = current_x_rel + delta.x;
pos_y = current_y_rel + delta.y;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
} else if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0.0f;
float new_cursor_pos_y = 0.0f;
if (props->reset_control_bar_pos_) {
float new_cursor_pos_x = 0;
float new_cursor_pos_y = 0;
// set control window pos
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
} else if (props->control_window_pos_.y < y_boundary) {
pos_y = y_boundary;
} else {
pos_y = current_y_rel;
pos_y = props->control_window_pos_.y;
}
if (props->is_control_bar_in_left_) {
pos_x = 0.0f;
pos_x = 0;
} else {
pos_x = container_w - props->control_window_width_;
pos_x = stream_window_width_ - props->control_window_width_;
}
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
if (0 != props->mouse_diff_control_bar_pos_x_ &&
0 != props->mouse_diff_control_bar_pos_y_) {
// set cursor pos
new_cursor_pos_x =
container_pos.x + pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y =
container_pos.y + pos_y + props->mouse_diff_control_bar_pos_y_;
new_cursor_pos_x = pos_x + props->mouse_diff_control_bar_pos_x_;
new_cursor_pos_y = pos_y + props->mouse_diff_control_bar_pos_y_;
SDL_WarpMouseInWindow(stream_window_, (int)new_cursor_pos_x,
(int)new_cursor_pos_y);
@@ -118,14 +94,12 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
} else if (!props->reset_control_bar_pos_ &&
ImGui::IsMouseReleased(ImGuiMouseButton_Left) ||
props->control_window_width_is_changing_) {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
if (current_x_rel <= container_w * 0.5f) {
pos_x = 0.0f;
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
if (props->control_window_pos_.x <= stream_window_width_ * 0.5f) {
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
} else {
pos_y = current_y_rel;
pos_y = props->control_window_pos_.y;
}
if (props->control_bar_expand_) {
@@ -144,53 +118,47 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
}
}
props->is_control_bar_in_left_ = true;
} else if (current_x_rel > container_w * 0.5f) {
pos_x = container_w - props->control_window_width_;
pos_y = (current_y_rel >= 0.0f &&
current_y_rel <= container_h - props->control_window_height_)
? current_y_rel
: (current_y_rel < 0.0f
? 0.0f
: (container_h - props->control_window_height_));
} else if (props->control_window_pos_.x > stream_window_width_ * 0.5f) {
pos_x = 0;
pos_y =
(props->control_window_pos_.y >= y_boundary &&
props->control_window_pos_.y <=
stream_window_height_ - props->control_window_height_)
? props->control_window_pos_.y
: (props->control_window_pos_.y <
(fullscreen_button_pressed_ ? 0 : title_bar_height_)
? (fullscreen_button_pressed_ ? 0 : title_bar_height_)
: (stream_window_height_ - props->control_window_height_));
if (props->control_bar_expand_) {
if (props->control_window_width_ >= props->control_window_max_width_) {
props->control_window_width_ = props->control_window_max_width_;
props->control_window_width_is_changing_ = false;
pos_x = container_w - props->control_window_max_width_;
pos_x = stream_window_width_ - props->control_window_max_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = container_w - props->control_window_width_;
pos_x = stream_window_width_ - props->control_window_width_;
}
} else {
if (props->control_window_width_ <= props->control_window_min_width_) {
props->control_window_width_ = props->control_window_min_width_;
props->control_window_width_is_changing_ = false;
pos_x = container_w - props->control_window_min_width_;
pos_x = stream_window_width_ - props->control_window_min_width_;
} else {
props->control_window_width_is_changing_ = true;
pos_x = container_w - props->control_window_width_;
pos_x = stream_window_width_ - props->control_window_width_;
}
}
props->is_control_bar_in_left_ = false;
}
if (current_y_rel + props->control_window_height_ > container_h) {
pos_y = container_h - props->control_window_height_;
} else if (current_y_rel < 0.0f) {
pos_y = 0.0f;
if (props->control_window_pos_.y + props->control_window_height_ >
stream_window_height_) {
pos_y = stream_window_height_ - props->control_window_height_;
} else if (props->control_window_pos_.y < y_boundary) {
pos_y = y_boundary;
}
} else {
float current_x_rel = props->control_window_pos_.x - container_pos.x;
float current_y_rel = props->control_window_pos_.y - container_pos.y;
pos_x = current_x_rel;
pos_y = current_y_rel;
if (pos_x < 0.0f) pos_x = 0.0f;
if (pos_y < 0.0f) pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h)
pos_y = container_h - props->control_window_height_;
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
}
if (props->control_bar_expand_ && props->control_window_height_is_changing_) {
@@ -212,20 +180,10 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
}
std::string control_window_title = props->remote_id_ + "ControlWindow";
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
float child_cursor_x = pos_x;
float child_cursor_y = pos_y;
ImGui::SetCursorPos(ImVec2(child_cursor_x, child_cursor_y));
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(
control_child_window_title.c_str(),
ImVec2(props->control_window_width_, props->control_window_height_),
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
ImGui::Begin(control_window_title.c_str(), nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar();
props->control_window_pos_ = ImGui::GetWindowPos();
SDL_GetMouseState(&props->mouse_pos_x_, &props->mouse_pos_y_);
@@ -234,27 +192,31 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
props->mouse_diff_control_bar_pos_y_ =
props->mouse_pos_y_ - props->control_window_pos_.y;
if (props->control_window_pos_.y < container_pos.y ||
props->control_window_pos_.y + props->control_window_height_ >
(container_pos.y + container_h) ||
props->control_window_pos_.x < container_pos.x ||
props->control_window_pos_.x + props->control_window_width_ >
(container_pos.x + container_w)) {
ImGui::ClearActiveID();
props->reset_control_bar_pos_ = true;
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
static bool a, b, c, d, e;
ImGui::SetNextWindowPos(
ImVec2(props->is_control_bar_in_left_
? props->control_window_pos_.x - props->control_window_width_
: props->control_window_pos_.x,
props->control_window_pos_.y),
ImGuiCond_Always);
std::string control_child_window_title =
props->remote_id_ + "ControlChildWindow";
ImGui::BeginChild(control_child_window_title.c_str(),
ImVec2(props->control_window_width_ * 2.0f,
props->control_window_height_),
ImGuiChildFlags_Border, ImGuiWindowFlags_NoDecoration);
ImGui::PopStyleColor();
ControlBar(props);
props->control_bar_hovered_ = ImGui::IsWindowHovered();
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleVar(4);
ImGui::PopStyleColor();
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -94,10 +94,10 @@ int Render::FileTransferWindow(
ImGui::PushFont(stream_windows_system_chinese_font_);
}
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.3f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
@@ -130,7 +130,7 @@ int Render::FileTransferWindow(
ImGui::SetWindowFontScale(0.5f);
ImGui::BeginChild(
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
@@ -242,4 +242,3 @@ int Render::FileTransferWindow(
}
} // namespace crossdesk

View File

@@ -2,7 +2,6 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h"
namespace crossdesk {
@@ -16,28 +15,28 @@ int Render::SettingWindow() {
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.07f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.1f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.8f));
#endif
} else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.07f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
#else
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.1f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.8f));
#endif
}
@@ -50,8 +49,8 @@ int Render::SettingWindow() {
int settings_items_offset = 0;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(localization::settings[localization_language_index_].c_str(),
nullptr,
@@ -60,9 +59,9 @@ int Render::SettingWindow() {
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{
const auto& supported_languages = localization::GetSupportedLanguages();
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
const char* language_items[] = {
localization::language_zh[localization_language_index_].c_str(),
localization::language_en[localization_language_index_].c_str()};
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
@@ -77,23 +76,13 @@ int Render::SettingWindow() {
}
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo(
"##language",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
language_button_value_)]
.display_name
.c_str())) {
if (ImGui::BeginCombo("##language",
language_items[language_button_value_])) {
ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) {
for (int i = 0; i < IM_ARRAYSIZE(language_items); i++) {
bool selected = (i == language_button_value_);
if (ImGui::Selectable(
supported_languages[i].display_name.c_str(), selected))
if (ImGui::Selectable(language_items[i], selected))
language_button_value_ = i;
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
@@ -341,14 +330,10 @@ int Render::SettingWindow() {
ImGui::EndTooltip();
}
}
#if _WIN32
ImGui::Separator();
{
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
@@ -364,67 +349,8 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_);
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
}
#endif
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text(
"%s",
localization::file_transfer_save_path[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
}
std::string display_path =
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: localization::default_desktop[localization_language_index_];
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.95f, 0.95f, 0.95f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.9f, 0.9f, 0.9f, 1.0f));
ImGui::PushFont(main_windows_system_chinese_font_);
if (ImGui::Button(display_path.c_str(),
ImVec2(title_bar_button_width_ * 2.0f, 0))) {
const char* folder =
tinyfd_selectFolderDialog(localization::file_transfer_save_path
[localization_language_index_]
.c_str(),
strlen(file_transfer_save_path_buf_) > 0
? file_transfer_save_path_buf_
: nullptr);
if (folder) {
strncpy(file_transfer_save_path_buf_, folder,
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) -
1] = '\0';
}
}
if (ImGui::IsItemHovered() &&
strlen(file_transfer_save_path_buf_) > 0) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", file_transfer_save_path_buf_);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
ImGui::PopFont();
ImGui::PopStyleColor(3);
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
@@ -448,24 +374,16 @@ int Render::SettingWindow() {
show_self_hosted_server_config_window_ = false;
// Language
language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_);
if (language_button_value_ == 0) {
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (language_button_value_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE);
} else {
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH);
}
config_center_->SetLanguage(localization_language_);
language_button_value_last_ = language_button_value_;
localization_language_ = (ConfigCenter::LANGUAGE)language_button_value_;
localization_language_index_ = language_button_value_;
LOG_INFO("Set localization language: {}",
localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex(
localization_language_index_)]
.code
.c_str());
localization_language_index_ == 0 ? "zh" : "en");
// Video quality
if (video_quality_button_value_ == 0) {
@@ -551,10 +469,6 @@ int Render::SettingWindow() {
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
// File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
file_transfer_save_path_last_ = file_transfer_save_path_buf_;
settings_window_pos_reset_ = true;
// Recreate peer instance
@@ -602,13 +516,6 @@ int Render::SettingWindow() {
enable_turn_ = enable_turn_last_;
}
// Restore file transfer save path
strncpy(file_transfer_save_path_buf_,
file_transfer_save_path_last_.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
file_transfer_save_path_buf_[sizeof(file_transfer_save_path_buf_) - 1] =
'\0';
settings_window_pos_reset_ = true;
}
ImGui::SetWindowFontScale(0.5f);
@@ -620,4 +527,4 @@ int Render::SettingWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -14,16 +14,15 @@ int Render::MainWindow() {
ImGui::SetNextWindowPos(ImVec2(0.0f, io.DisplaySize.y * (TITLE_BAR_HEIGHT)),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild(
"DeskWindow",
ImVec2(local_remote_window_width, local_remote_window_height),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor(2);
ImGui::PopStyleColor();
LocalWindow();
@@ -57,4 +56,4 @@ int Render::MainWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -127,8 +127,8 @@ int Render::RequestPermissionWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin(

View File

@@ -28,6 +28,98 @@ std::vector<std::string> GetRootEntries() {
return roots;
}
int Render::ShowSimpleFileBrowser() {
std::string display_text;
if (selected_current_file_path_.empty()) {
selected_current_file_path_ = std::filesystem::current_path().string();
}
if (!tls_cert_path_self_.empty()) {
display_text =
std::filesystem::path(tls_cert_path_self_).filename().string();
} else if (selected_current_file_path_ != "Root") {
display_text =
std::filesystem::path(selected_current_file_path_).filename().string();
if (display_text.empty()) {
display_text = selected_current_file_path_;
}
}
if (display_text.empty()) {
display_text =
localization::select_a_file[localization_language_index_].c_str();
}
if (show_file_browser_) {
ImGui::PushItemFlag(ImGuiItemFlags_AutoClosePopups, false);
float fixed_width = title_bar_button_width_ * 3.8f;
ImGui::SetNextItemWidth(fixed_width);
ImGui::SetNextWindowSizeConstraints(ImVec2(fixed_width, 0),
ImVec2(fixed_width, 100.0f));
if (ImGui::BeginCombo("##select_a_file", display_text.c_str(), 0)) {
ImGui::SetWindowFontScale(0.5f);
bool file_selected = false;
auto roots = GetRootEntries();
if (selected_current_file_path_ == "Root" ||
!std::filesystem::exists(selected_current_file_path_) ||
!std::filesystem::is_directory(selected_current_file_path_)) {
for (const auto& root : roots) {
if (ImGui::Selectable(root.c_str())) {
selected_current_file_path_ = root;
tls_cert_path_self_.clear();
}
}
} else {
std::filesystem::path p(selected_current_file_path_);
if (ImGui::Selectable("..")) {
if (std::find(roots.begin(), roots.end(),
selected_current_file_path_) != roots.end()) {
selected_current_file_path_ = "Root";
} else if (p.has_parent_path()) {
selected_current_file_path_ = p.parent_path().string();
} else {
selected_current_file_path_ = "Root";
}
tls_cert_path_self_.clear();
}
try {
for (const auto& entry : std::filesystem::directory_iterator(
selected_current_file_path_)) {
std::string name = entry.path().filename().string();
if (entry.is_directory()) {
if (ImGui::Selectable(name.c_str())) {
selected_current_file_path_ = entry.path().string();
tls_cert_path_self_.clear();
}
} else {
if (ImGui::Selectable(name.c_str())) {
tls_cert_path_self_ = entry.path().string();
file_selected = true;
show_file_browser_ = false;
}
}
}
} catch (const std::exception& e) {
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Error: %s", e.what());
}
}
ImGui::EndCombo();
}
ImGui::PopItemFlag();
} else {
show_file_browser_ = true;
}
return 0;
}
int Render::SelfHostedServerWindow() {
ImGuiIO& io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) {
@@ -36,12 +128,12 @@ int Render::SelfHostedServerWindow() {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.41f));
} else {
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.41f));
}
self_hosted_server_config_window_pos_reset_ = false;
@@ -51,8 +143,8 @@ int Render::SelfHostedServerWindow() {
{
ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(localization::self_hosted_server_settings
[localization_language_index_]
@@ -120,6 +212,35 @@ int Render::SelfHostedServerWindow() {
IM_ARRAYSIZE(coturn_server_port_self_));
}
ImGui::Separator();
// {
// ImGui::AlignTextToFramePadding();
// ImGui::Text(
// "%s",
// localization::reset_cert_fingerprint[localization_language_index_]
// .c_str());
// ImGui::SameLine();
// if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
// ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
// } else {
// ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
// }
// ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
// ShowSimpleFileBrowser();
// }
{
ImGui::AlignTextToFramePadding();
if (ImGui::Button(localization::reset_cert_fingerprint
[localization_language_index_]
.c_str())) {
config_center_->ClearCertFingerprint();
LOG_INFO("Certificate fingerprint cleared by user");
}
}
if (stream_window_inited_) {
ImGui::EndDisabled();
}
@@ -142,6 +263,7 @@ int Render::SelfHostedServerWindow() {
config_center_->SetServerHost(signal_server_ip_self_);
config_center_->SetServerPort(atoi(signal_server_port_self_));
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
config_center_->SetCertFilePath(tls_cert_path_self_);
strncpy(signal_server_ip_, signal_server_ip_self_,
sizeof(signal_server_ip_) - 1);
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
@@ -151,6 +273,9 @@ int Render::SelfHostedServerWindow() {
strncpy(coturn_server_port_, coturn_server_port_self_,
sizeof(coturn_server_port_) - 1);
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
strncpy(cert_file_path_, tls_cert_path_self_.c_str(),
sizeof(cert_file_path_) - 1);
cert_file_path_[sizeof(cert_file_path_) - 1] = '\0';
self_hosted_server_config_window_pos_reset_ = true;
}
@@ -181,6 +306,7 @@ int Render::SelfHostedServerWindow() {
} else {
coturn_server_port_self_[0] = '\0';
}
tls_cert_path_self_ = config_center_->GetCertFilePath();
}
ImGui::SetWindowFontScale(1.0f);

View File

@@ -8,7 +8,6 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h"
namespace crossdesk {
@@ -49,24 +48,22 @@ int Render::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::Begin("##server_window", nullptr,
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
server_window_title_bar_height_ = title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::BeginChild(
"ServerTitleBar",
ImVec2(server_window_width_, server_window_title_bar_height_),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
@@ -86,13 +83,9 @@ int Render::ServerWindow() {
const char* icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle";
bool toggle_clicked = RoundedCornerButton(
toggle_label.c_str(),
ImVec2(server_title_bar_button_width, server_title_bar_button_height),
8.5f, ImDrawFlags_RoundCornersTopLeft, true, IM_COL32(0, 0, 0, 0),
IM_COL32(0, 0, 0, 25), IM_COL32(255, 255, 255, 255));
if (toggle_clicked) {
if (ImGui::Button(toggle_label.c_str(),
ImVec2(server_title_bar_button_width,
server_title_bar_button_height))) {
if (server_window_) {
int w = 0;
int h = 0;
@@ -121,7 +114,7 @@ int Render::ServerWindow() {
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
RemoteClientInfoWindow();
@@ -140,7 +133,7 @@ int Render::RemoteClientInfoWindow() {
ImGui::BeginChild(
"RemoteClientInfoWindow",
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
ImGuiChildFlags_Borders,
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
@@ -148,38 +141,33 @@ int Render::RemoteClientInfoWindow() {
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries;
remote_entries.reserve(connection_status_.size());
for (const auto& kv : connection_status_) {
const auto host_it = connection_host_names_.find(kv.first);
const std::string display_name =
(host_it != connection_host_names_.end() && !host_it->second.empty())
? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
std::vector<std::string> remote_hostnames;
remote_hostnames.reserve(connection_host_names_.size());
for (const auto& kv : connection_host_names_) {
remote_hostnames.push_back(kv.second);
}
auto find_display_name_by_remote_id =
[&remote_entries](const std::string& remote_id) -> std::string {
for (const auto& entry : remote_entries) {
if (entry.first == remote_id) {
return entry.second;
auto find_remote_id_by_hostname =
[this](const std::string& hostname) -> std::string {
for (const auto& kv : connection_host_names_) {
if (kv.second == hostname) {
return kv.first;
}
}
return {};
};
if (!selected_server_remote_id_.empty() &&
find_display_name_by_remote_id(selected_server_remote_id_).empty()) {
selected_server_remote_id_.clear();
selected_server_remote_hostname_.clear();
if (!selected_server_remote_hostname_.empty()) {
if (std::find(remote_hostnames.begin(), remote_hostnames.end(),
selected_server_remote_hostname_) == remote_hostnames.end()) {
selected_server_remote_hostname_.clear();
selected_server_remote_id_.clear();
}
}
if (selected_server_remote_id_.empty() && !remote_entries.empty()) {
selected_server_remote_id_ = remote_entries.front().first;
}
if (!selected_server_remote_id_.empty()) {
selected_server_remote_hostname_ =
find_display_name_by_remote_id(selected_server_remote_id_);
if (selected_server_remote_hostname_.empty() && !remote_hostnames.empty()) {
selected_server_remote_hostname_ = remote_hostnames.front();
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
ImGui::SetWindowFontScale(font_scale);
@@ -201,12 +189,13 @@ int Render::RemoteClientInfoWindow() {
ImGui::AlignTextToFramePadding();
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
for (int i = 0; i < static_cast<int>(remote_entries.size()); i++) {
for (int i = 0; i < static_cast<int>(remote_hostnames.size()); i++) {
const bool selected =
(remote_entries[i].first == selected_server_remote_id_);
if (ImGui::Selectable(remote_entries[i].second.c_str(), selected)) {
selected_server_remote_id_ = remote_entries[i].first;
selected_server_remote_hostname_ = remote_entries[i].second;
(remote_hostnames[i] == selected_server_remote_hostname_);
if (ImGui::Selectable(remote_hostnames[i].c_str(), selected)) {
selected_server_remote_hostname_ = remote_hostnames[i];
selected_server_remote_id_ =
find_remote_id_by_hostname(selected_server_remote_hostname_);
}
if (selected) {
ImGui::SetItemDefaultFocus();
@@ -362,7 +351,7 @@ int Render::RemoteClientInfoWindow() {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f);
ImGui::SetWindowFontScale(font_scale);
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
close_connection_button_height))) {
@@ -376,4 +365,4 @@ int Render::RemoteClientInfoWindow() {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -31,34 +31,6 @@ void Render::DrawConnectionStatusText(
}
}
void Render::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) {
return;
}
bool has_valid_frame = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
has_valid_frame = props->stream_texture_ != nullptr &&
props->video_width_ > 0 && props->video_height_ > 0 &&
props->front_frame_ && !props->front_frame_->empty();
}
if (has_valid_frame) {
return;
}
const std::string& text =
localization::receiving_screen[localization_language_index_];
ImVec2 size = ImGui::GetWindowSize();
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
ImGui::SetCursorPos(
ImVec2((size.x - text_size.x) * 0.5f, (size.y - text_size.y) * 0.5f));
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
}
void Render::CloseTab(decltype(client_properties_)::iterator& it) {
// std::unique_lock lock(client_properties_mutex_);
if (it != client_properties_.end()) {
@@ -145,9 +117,7 @@ int Render::StreamWindow() {
ImGui::SetWindowFontScale(0.6f);
ImGui::SetNextWindowSize(
ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImVec2(stream_window_width_, stream_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
@@ -168,12 +138,10 @@ int Render::StreamWindow() {
UpdateRenderRect();
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
DrawReceivingScreenText(props);
focused_remote_id_ = props->remote_id_;
if (!props->peer_) {
@@ -183,12 +151,12 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
// Ensure we flush pending STREAM_REFRESH_EVENT events and
// clean up peer resources before erasing the entry, otherwise
// SDL events may still hold raw pointers to freed
// SubStreamWindowProperties (including video_frame_mutex_),
// leading to std::system_error when locking.
CloseTab(erase_it);
erase_it = client_properties_.erase(erase_it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
}
}
// lock.lock();
@@ -249,9 +217,7 @@ int Render::StreamWindow() {
if (props->tab_selected_) {
ImGui::SetNextWindowSize(
ImVec2(stream_window_width_,
stream_window_height_ -
(fullscreen_button_pressed_ ? 0 : title_bar_height_)),
ImVec2(stream_window_width_, stream_window_height_),
ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
@@ -270,12 +236,10 @@ int Render::StreamWindow() {
UpdateRenderRect();
ControlWindow(props);
// Show file transfer window if needed
FileTransferWindow(props);
DrawReceivingScreenText(props);
ImGui::End();
if (!props->peer_) {
@@ -287,7 +251,12 @@ int Render::StreamWindow() {
// std::unique_lock unique_lock(client_properties_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) {
CloseTab(erase_it);
client_properties_.erase(erase_it);
if (client_properties_.empty()) {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
}
}
// lock.lock();

View File

@@ -1,4 +1,5 @@
#include <algorithm>
#include <cstdlib>
#include <string>
#include "layout.h"
@@ -76,8 +77,8 @@ int Render::UpdateNotificationWindow() {
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
@@ -103,7 +104,6 @@ int Render::UpdateNotificationWindow() {
localization::access_website[localization_language_index_] +
"https://crossdesk.cn";
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
Hyperlink(download_text, "https://crossdesk.cn",
update_notification_window_width);
ImGui::SetWindowFontScale(1.0f);
@@ -121,7 +121,7 @@ int Render::UpdateNotificationWindow() {
ImGui::BeginChild(
"ScrollableContent",
ImVec2(update_notification_window_width * 0.9f, scrollable_height),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGuiChildFlags_Border, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f);
// set text wrap position to current available width (accounts for
// scrollbar)
@@ -184,7 +184,14 @@ int Render::UpdateNotificationWindow() {
localization::update[localization_language_index_].c_str())) {
// open download page
std::string url = "https://crossdesk.cn";
OpenUrl(url);
#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());
show_update_notification_window_ = false;
}

View File

@@ -62,4 +62,4 @@ std::shared_ptr<spdlog::logger> get_logger() {
return g_logger;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -40,6 +40,20 @@ std::filesystem::path PathManager::GetLogPath() {
#endif
}
std::filesystem::path PathManager::GetCertPath() {
#ifdef _WIN32
// %APPDATA%\AppName\Certs
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name_ / "certs";
#elif __APPLE__
// $HOME/Library/Application Support/AppName/certs
return GetHome() + "/Library/Application Support/" + app_name_ + "/certs";
#else
// $XDG_CONFIG_HOME/AppName/certs
return GetEnvOrDefault("XDG_CONFIG_HOME", GetHome() + "/.config") /
app_name_ / "certs";
#endif
}
bool PathManager::CreateDirectories(const std::filesystem::path& p) {
std::error_code ec;
bool created = std::filesystem::create_directories(p, ec);

View File

@@ -26,6 +26,8 @@ class PathManager {
std::filesystem::path GetLogPath();
std::filesystem::path GetCertPath();
bool CreateDirectories(const std::filesystem::path& p);
private:

View File

@@ -1,573 +0,0 @@
#include "screen_capturer_drm.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
defined(__has_include) && __has_include(<xf86drm.h>) && \
__has_include(<xf86drmMode.h>)
#define CROSSDESK_DRM_BUILD_ENABLED 1
#include <xf86drm.h>
#include <xf86drmMode.h>
#elif defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM && \
defined(__has_include) && __has_include(<libdrm/xf86drm.h>) && \
__has_include(<libdrm/xf86drmMode.h>)
#define CROSSDESK_DRM_BUILD_ENABLED 1
#include <libdrm/xf86drm.h>
#include <libdrm/xf86drmMode.h>
#else
#define CROSSDESK_DRM_BUILD_ENABLED 0
#endif
#if CROSSDESK_DRM_BUILD_ENABLED
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <thread>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr int kMaxDrmCards = 16;
const char* ConnectorTypeName(uint32_t type) {
switch (type) {
case DRM_MODE_CONNECTOR_VGA:
return "VGA";
case DRM_MODE_CONNECTOR_DVII:
return "DVI-I";
case DRM_MODE_CONNECTOR_DVID:
return "DVI-D";
case DRM_MODE_CONNECTOR_DVIA:
return "DVI-A";
case DRM_MODE_CONNECTOR_HDMIA:
return "HDMI-A";
case DRM_MODE_CONNECTOR_HDMIB:
return "HDMI-B";
case DRM_MODE_CONNECTOR_DisplayPort:
return "DP";
case DRM_MODE_CONNECTOR_eDP:
return "eDP";
case DRM_MODE_CONNECTOR_LVDS:
return "LVDS";
#ifdef DRM_MODE_CONNECTOR_VIRTUAL
case DRM_MODE_CONNECTOR_VIRTUAL:
return "Virtual";
#endif
default:
return "Display";
}
}
} // namespace
ScreenCapturerDrm::ScreenCapturerDrm() {}
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
int ScreenCapturerDrm::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!cb) {
LOG_ERROR("DRM screen capturer callback is null");
return -1;
}
fps_ = std::max(1, fps);
callback_ = cb;
monitor_index_ = 0;
initial_monitor_index_ = 0;
consecutive_failures_ = 0;
display_info_list_.clear();
outputs_.clear();
y_plane_.clear();
uv_plane_.clear();
if (!DiscoverOutputs()) {
LOG_ERROR("DRM screen capturer could not find active outputs");
callback_ = nullptr;
CloseDevices();
return -1;
}
return 0;
}
int ScreenCapturerDrm::Destroy() {
Stop();
callback_ = nullptr;
display_info_list_.clear();
outputs_.clear();
y_plane_.clear();
uv_plane_.clear();
CloseDevices();
return 0;
}
int ScreenCapturerDrm::Start(bool show_cursor) {
if (running_) {
return 0;
}
if (outputs_.empty()) {
LOG_ERROR("DRM screen capturer has no output to capture");
return -1;
}
show_cursor_ = show_cursor;
paused_ = false;
int probe_index = monitor_index_.load();
if (probe_index < 0 || probe_index >= static_cast<int>(outputs_.size())) {
probe_index = 0;
}
if (!CaptureOutputFrame(outputs_[probe_index], false)) {
LOG_ERROR("DRM start probe failed on output {}", outputs_[probe_index].name);
return -1;
}
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDrm::Stop() {
if (!running_) {
return 0;
}
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
return 0;
}
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDrm::SwitchTo(int monitor_index) {
if (monitor_index < 0 ||
monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid DRM monitor index: {}", monitor_index);
return -1;
}
monitor_index_ = monitor_index;
return 0;
}
int ScreenCapturerDrm::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
return display_info_list_;
}
bool ScreenCapturerDrm::DiscoverOutputs() {
for (int card_index = 0; card_index < kMaxDrmCards; ++card_index) {
const std::string card_path = "/dev/dri/card" + std::to_string(card_index);
const int fd = open(card_path.c_str(), O_RDWR | O_CLOEXEC);
if (fd < 0) {
continue;
}
drmModeRes* resources = drmModeGetResources(fd);
if (!resources) {
close(fd);
continue;
}
DrmDevice device;
device.fd = fd;
device.path = card_path;
devices_.push_back(device);
const int device_slot = static_cast<int>(devices_.size()) - 1;
const size_t output_count_before = outputs_.size();
for (int i = 0; i < resources->count_connectors; ++i) {
drmModeConnector* connector =
drmModeGetConnector(fd, resources->connectors[i]);
if (!connector) {
continue;
}
if (connector->connection != DRM_MODE_CONNECTED ||
connector->count_modes <= 0) {
drmModeFreeConnector(connector);
continue;
}
uint32_t crtc_id = 0;
if (connector->encoder_id != 0) {
drmModeEncoder* encoder = drmModeGetEncoder(fd, connector->encoder_id);
if (encoder) {
crtc_id = encoder->crtc_id;
drmModeFreeEncoder(encoder);
}
}
if (crtc_id == 0) {
for (int enc_idx = 0; enc_idx < connector->count_encoders; ++enc_idx) {
drmModeEncoder* encoder =
drmModeGetEncoder(fd, connector->encoders[enc_idx]);
if (!encoder) {
continue;
}
if (encoder->crtc_id != 0) {
crtc_id = encoder->crtc_id;
drmModeFreeEncoder(encoder);
break;
}
drmModeFreeEncoder(encoder);
}
}
if (crtc_id == 0) {
drmModeFreeConnector(connector);
continue;
}
drmModeCrtc* crtc = drmModeGetCrtc(fd, crtc_id);
if (!crtc || !crtc->mode_valid || crtc->width <= 0 || crtc->height <= 0) {
if (crtc) {
drmModeFreeCrtc(crtc);
}
drmModeFreeConnector(connector);
continue;
}
DrmOutput output;
output.device_index = device_slot;
output.connector_id = connector->connector_id;
output.crtc_id = crtc_id;
output.left = crtc->x;
output.top = crtc->y;
output.width = static_cast<int>(crtc->width);
output.height = static_cast<int>(crtc->height);
output.name = std::string(ConnectorTypeName(connector->connector_type)) +
std::to_string(connector->connector_type_id);
outputs_.push_back(output);
display_info_list_.push_back(
DisplayInfo(output.name, output.left, output.top,
output.left + output.width, output.top + output.height));
LOG_INFO("DRM output found: {} on {}, {}x{} @ ({}, {})", output.name,
card_path, output.width, output.height, output.left, output.top);
drmModeFreeCrtc(crtc);
drmModeFreeConnector(connector);
}
drmModeFreeResources(resources);
if (outputs_.size() == output_count_before) {
close(fd);
devices_.pop_back();
}
}
if (outputs_.empty()) {
return false;
}
LOG_INFO("DRM screen capturer discovered {} output(s)", outputs_.size());
return true;
}
void ScreenCapturerDrm::CloseDevices() {
for (auto& device : devices_) {
if (device.fd >= 0) {
close(device.fd);
device.fd = -1;
}
}
devices_.clear();
}
void ScreenCapturerDrm::CaptureLoop() {
using clock = std::chrono::steady_clock;
const auto frame_interval =
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
while (running_) {
const auto frame_start = clock::now();
if (!paused_) {
int index = monitor_index_.load();
if (index >= 0 && index < static_cast<int>(outputs_.size())) {
const bool ok = CaptureOutputFrame(outputs_[index], true);
if (!ok) {
++consecutive_failures_;
if (consecutive_failures_ == 1 || consecutive_failures_ % 60 == 0) {
LOG_WARN("DRM capture failed (consecutive={})",
consecutive_failures_);
}
} else {
consecutive_failures_ = 0;
}
}
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
clock::now() - frame_start);
if (elapsed < frame_interval) {
std::this_thread::sleep_for(frame_interval - elapsed);
}
}
}
bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
bool emit_callback) {
if (output.device_index < 0 ||
output.device_index >= static_cast<int>(devices_.size())) {
return false;
}
const int fd = devices_[output.device_index].fd;
if (fd < 0) {
return false;
}
drmModeCrtc* crtc = drmModeGetCrtc(fd, output.crtc_id);
if (!crtc) {
return false;
}
const uint32_t fb_id = crtc->buffer_id;
drmModeFreeCrtc(crtc);
if (fb_id == 0) {
return false;
}
drmModeFB* fb = drmModeGetFB(fd, fb_id);
if (!fb) {
return false;
}
const uint32_t handle = fb->handle;
const uint32_t pitch = fb->pitch;
const int src_width = static_cast<int>(fb->width);
const int src_height = static_cast<int>(fb->height);
const int bpp = static_cast<int>(fb->bpp);
drmModeFreeFB(fb);
if (handle == 0 || pitch == 0 || src_width <= 1 || src_height <= 1) {
return false;
}
if (bpp != 32) {
LOG_WARN("DRM capture unsupported bpp: {}", bpp);
return false;
}
const size_t map_size =
static_cast<size_t>(pitch) * static_cast<size_t>(src_height);
uint8_t* mapped_ptr = nullptr;
size_t mapped_size = 0;
int prime_fd = -1;
if (!MapFramebuffer(fd, handle, map_size, &mapped_ptr, &mapped_size,
&prime_fd)) {
return false;
}
int capture_width = std::min(src_width, output.width);
int capture_height = std::min(src_height, output.height);
if (capture_width <= 0 || capture_height <= 0) {
capture_width = src_width;
capture_height = src_height;
}
capture_width &= ~1;
capture_height &= ~1;
if (capture_width <= 1 || capture_height <= 1) {
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return false;
}
const size_t y_size =
static_cast<size_t>(capture_width) * static_cast<size_t>(capture_height);
const size_t uv_size = y_size / 2;
if (y_plane_.size() != y_size) {
y_plane_.resize(y_size);
}
if (uv_plane_.size() != uv_size) {
uv_plane_.resize(uv_size);
}
const int convert_ret =
libyuv::ARGBToNV12(mapped_ptr, static_cast<int>(pitch), y_plane_.data(),
capture_width, uv_plane_.data(), capture_width,
capture_width, capture_height);
if (convert_ret != 0) {
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return false;
}
std::vector<uint8_t> nv12;
nv12.reserve(y_plane_.size() + uv_plane_.size());
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (emit_callback && callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
capture_height, output.name.c_str());
}
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
return true;
}
bool ScreenCapturerDrm::MapFramebuffer(int fd, uint32_t handle, size_t map_size,
uint8_t** mapped_ptr,
size_t* mapped_size,
int* prime_fd) const {
if (!mapped_ptr || !mapped_size || !prime_fd || map_size == 0) {
return false;
}
*mapped_ptr = nullptr;
*mapped_size = 0;
*prime_fd = -1;
drm_mode_map_dumb map_arg{};
map_arg.handle = handle;
if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg) == 0) {
void* mapped = mmap(nullptr, map_size, PROT_READ, MAP_SHARED, fd,
static_cast<off_t>(map_arg.offset));
if (mapped != MAP_FAILED) {
*mapped_ptr = static_cast<uint8_t*>(mapped);
*mapped_size = map_size;
return true;
}
}
int dma_fd = -1;
if (drmPrimeHandleToFD(fd, handle, DRM_CLOEXEC, &dma_fd) == 0) {
size_t dma_map_size = map_size;
const off_t fd_size = lseek(dma_fd, 0, SEEK_END);
if (fd_size > 0) {
dma_map_size = std::min(map_size, static_cast<size_t>(fd_size));
}
void* mapped =
mmap(nullptr, dma_map_size, PROT_READ, MAP_SHARED, dma_fd, 0);
if (mapped != MAP_FAILED) {
*mapped_ptr = static_cast<uint8_t*>(mapped);
*mapped_size = dma_map_size;
*prime_fd = dma_fd;
return true;
}
close(dma_fd);
}
return false;
}
void ScreenCapturerDrm::UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
int prime_fd) const {
if (mapped_ptr && mapped_size > 0) {
munmap(mapped_ptr, mapped_size);
}
if (prime_fd >= 0) {
close(prime_fd);
}
}
} // namespace crossdesk
#else
#include "rd_log.h"
namespace crossdesk {
ScreenCapturerDrm::ScreenCapturerDrm() {}
ScreenCapturerDrm::~ScreenCapturerDrm() { Destroy(); }
int ScreenCapturerDrm::Init([[maybe_unused]] const int fps, cb_desktop_data cb) {
Destroy();
callback_ = cb;
LOG_WARN("DRM screen capturer disabled: libdrm headers not available");
return -1;
}
int ScreenCapturerDrm::Destroy() {
Stop();
callback_ = nullptr;
display_info_list_.clear();
outputs_.clear();
return 0;
}
int ScreenCapturerDrm::Start([[maybe_unused]] bool show_cursor) { return -1; }
int ScreenCapturerDrm::Stop() {
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
return 0;
}
int ScreenCapturerDrm::Pause([[maybe_unused]] int monitor_index) { return 0; }
int ScreenCapturerDrm::Resume([[maybe_unused]] int monitor_index) { return 0; }
int ScreenCapturerDrm::SwitchTo([[maybe_unused]] int monitor_index) {
return -1;
}
int ScreenCapturerDrm::ResetToInitialMonitor() { return 0; }
std::vector<DisplayInfo> ScreenCapturerDrm::GetDisplayInfoList() {
return display_info_list_;
}
bool ScreenCapturerDrm::DiscoverOutputs() { return false; }
void ScreenCapturerDrm::CloseDevices() {}
void ScreenCapturerDrm::CaptureLoop() {}
bool ScreenCapturerDrm::CaptureOutputFrame(
[[maybe_unused]] const DrmOutput& output,
[[maybe_unused]] bool emit_callback) {
return false;
}
bool ScreenCapturerDrm::MapFramebuffer([[maybe_unused]] int fd,
[[maybe_unused]] uint32_t handle,
[[maybe_unused]] size_t map_size,
[[maybe_unused]] uint8_t** mapped_ptr,
[[maybe_unused]] size_t* mapped_size,
[[maybe_unused]] int* prime_fd) const {
return false;
}
void ScreenCapturerDrm::UnmapFramebuffer([[maybe_unused]] uint8_t* mapped_ptr,
[[maybe_unused]] size_t mapped_size,
[[maybe_unused]] int prime_fd) const {}
} // namespace crossdesk
#endif

View File

@@ -1,87 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DRM_H_
#define _SCREEN_CAPTURER_DRM_H_
#include <atomic>
#include <cstdint>
#include <string>
#include <thread>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDrm : public ScreenCapturer {
public:
ScreenCapturerDrm();
~ScreenCapturerDrm();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
struct DrmDevice {
int fd = -1;
std::string path;
};
struct DrmOutput {
int device_index = -1;
uint32_t connector_id = 0;
uint32_t crtc_id = 0;
std::string name;
int left = 0;
int top = 0;
int width = 0;
int height = 0;
};
private:
bool DiscoverOutputs();
void CloseDevices();
void CaptureLoop();
bool CaptureOutputFrame(const DrmOutput& output, bool emit_callback = true);
bool MapFramebuffer(int fd, uint32_t handle, size_t map_size,
uint8_t** mapped_ptr, size_t* mapped_size,
int* prime_fd) const;
void UnmapFramebuffer(uint8_t* mapped_ptr, size_t mapped_size,
int prime_fd) const;
private:
std::vector<DrmDevice> devices_;
std::vector<DrmOutput> outputs_;
std::vector<DisplayInfo> display_info_list_;
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_;
int consecutive_failures_ = 0;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
} // namespace crossdesk
#endif

View File

@@ -1,506 +0,0 @@
#include "screen_capturer_linux.h"
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include "platform.h"
#include "rd_log.h"
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
#include "screen_capturer_drm.h"
#endif
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include "screen_capturer_wayland.h"
#endif
#include "screen_capturer_x11.h"
namespace crossdesk {
namespace {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
constexpr bool kDrmBuildEnabled = true;
#else
constexpr bool kDrmBuildEnabled = false;
#endif
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
constexpr bool kWaylandBuildEnabled = true;
#else
constexpr bool kWaylandBuildEnabled = false;
#endif
} // namespace
ScreenCapturerLinux::ScreenCapturerLinux() {}
ScreenCapturerLinux::~ScreenCapturerLinux() { Destroy(); }
int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!cb) {
LOG_ERROR("Linux screen capturer callback is null");
return -1;
}
fps_ = fps;
callback_orig_ = std::move(cb);
callback_ = [this](unsigned char* data, int size, int width, int height,
const char* display_name) {
const std::string mapped_name = MapDisplayName(display_name);
if (callback_orig_) {
callback_orig_(data, size, width, height, mapped_name.c_str());
}
};
const char* force_backend = getenv("CROSSDESK_SCREEN_BACKEND");
if (force_backend && force_backend[0] != '\0') {
if (strcmp(force_backend, "drm") == 0) {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
LOG_INFO("Linux screen capturer forced backend: DRM");
return InitDrm();
#else
LOG_ERROR(
"Linux screen capturer forced backend DRM is disabled at build time");
return -1;
#endif
}
if (strcmp(force_backend, "x11") == 0) {
LOG_INFO("Linux screen capturer forced backend: X11");
return InitX11();
}
if (strcmp(force_backend, "wayland") == 0) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
LOG_INFO("Linux screen capturer forced backend: Wayland");
return InitWayland();
#else
LOG_ERROR(
"Linux screen capturer forced backend Wayland is disabled at build "
"time");
return -1;
#endif
}
LOG_WARN("Unknown CROSSDESK_SCREEN_BACKEND={}, using auto strategy",
force_backend);
}
const bool wayland_session = IsWaylandSession();
if (wayland_session) {
if (kDrmBuildEnabled) {
LOG_INFO("Wayland session detected, prefer DRM -> X11 -> Wayland");
if (InitDrm() == 0) {
return 0;
}
} else {
LOG_INFO("Wayland session detected, DRM disabled, prefer X11 -> Wayland");
}
if (InitX11() == 0) {
return 0;
}
if (kDrmBuildEnabled) {
LOG_WARN(
"DRM and X11 init failed in Wayland session, trying Wayland portal");
} else {
LOG_WARN("X11 init failed in Wayland session, trying Wayland portal");
}
if (kWaylandBuildEnabled) {
return InitWayland();
}
LOG_ERROR("Wayland session detected but Wayland backend is disabled");
return -1;
}
if (InitX11() == 0) {
return 0;
}
if (kDrmBuildEnabled) {
LOG_WARN("X11 init failed, trying DRM fallback");
return InitDrm();
}
LOG_ERROR("X11 init failed and DRM backend is disabled");
return -1;
}
int ScreenCapturerLinux::Destroy() {
if (impl_) {
impl_->Destroy();
impl_.reset();
}
backend_ = BackendType::kNone;
callback_ = nullptr;
callback_orig_ = nullptr;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
canonical_displays_.clear();
label_alias_.clear();
}
return 0;
}
int ScreenCapturerLinux::Start(bool show_cursor) {
if (!impl_) {
LOG_ERROR("Linux screen capturer backend is not initialized");
return -1;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (backend_ == BackendType::kWayland) {
const int refresh_ret = RefreshWaylandBackend();
if (refresh_ret != 0) {
LOG_WARN("Linux screen capturer Wayland backend refresh failed: {}",
refresh_ret);
}
}
#endif
const int ret = impl_->Start(show_cursor);
if (ret == 0) {
return 0;
}
const char* backend_name = "None";
if (backend_ == BackendType::kX11) {
backend_name = "X11";
} else if (backend_ == BackendType::kDrm) {
backend_name = "DRM";
} else if (backend_ == BackendType::kWayland) {
backend_name = "Wayland";
}
LOG_WARN("Linux screen capturer backend {} start failed: {}",
backend_name, ret);
if (backend_ == BackendType::kX11 && kDrmBuildEnabled &&
TryFallbackToDrm(show_cursor)) {
return 0;
}
if (backend_ == BackendType::kX11 && kWaylandBuildEnabled &&
TryFallbackToWayland(show_cursor)) {
return 0;
}
if (backend_ == BackendType::kDrm && kDrmBuildEnabled) {
if (TryFallbackToX11(show_cursor)) {
return 0;
}
if (kWaylandBuildEnabled && TryFallbackToWayland(show_cursor)) {
return 0;
}
}
if (backend_ == BackendType::kWayland && kWaylandBuildEnabled) {
if (kDrmBuildEnabled && TryFallbackToDrm(show_cursor)) {
return 0;
}
if (TryFallbackToX11(show_cursor)) {
return 0;
}
}
return ret;
}
int ScreenCapturerLinux::Stop() {
if (!impl_) {
return 0;
}
const int ret = impl_->Stop();
UpdateAliasesFromBackend(impl_.get());
return ret;
}
int ScreenCapturerLinux::Pause(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->Pause(monitor_index);
}
int ScreenCapturerLinux::Resume(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->Resume(monitor_index);
}
int ScreenCapturerLinux::SwitchTo(int monitor_index) {
if (!impl_) {
return -1;
}
return impl_->SwitchTo(monitor_index);
}
int ScreenCapturerLinux::ResetToInitialMonitor() {
if (!impl_) {
return -1;
}
return impl_->ResetToInitialMonitor();
}
std::vector<DisplayInfo> ScreenCapturerLinux::GetDisplayInfoList() {
if (!impl_) {
return std::vector<DisplayInfo>();
}
// Wayland backend may update display geometry/stream handle asynchronously
// after Start(). Refresh aliases every time to keep canonical displays fresh.
UpdateAliasesFromBackend(impl_.get());
std::lock_guard<std::mutex> lock(alias_mutex_);
if (!canonical_displays_.empty()) {
return canonical_displays_;
}
return impl_->GetDisplayInfoList();
}
int ScreenCapturerLinux::InitX11() {
auto backend = std::make_unique<ScreenCapturerX11>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer X11 init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kX11;
LOG_INFO("Linux screen capturer backend selected: X11");
return 0;
}
int ScreenCapturerLinux::InitDrm() {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
auto backend = std::make_unique<ScreenCapturerDrm>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer DRM init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kDrm;
LOG_INFO("Linux screen capturer backend selected: DRM");
return 0;
#else
LOG_WARN("Linux screen capturer DRM backend is disabled at build time");
return -1;
#endif
}
int ScreenCapturerLinux::InitWayland() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto backend = std::make_unique<ScreenCapturerWayland>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
LOG_WARN("Linux screen capturer Wayland init failed: {}", ret);
return ret;
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer backend selected: Wayland");
return 0;
#else
LOG_WARN("Linux screen capturer Wayland backend is disabled at build time");
return -1;
#endif
}
int ScreenCapturerLinux::RefreshWaylandBackend() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto backend = std::make_unique<ScreenCapturerWayland>();
const int ret = backend->Init(fps_, callback_);
if (ret != 0) {
backend->Destroy();
return ret;
}
if (impl_) {
impl_->Destroy();
}
UpdateAliasesFromBackend(backend.get());
impl_ = std::move(backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer Wayland backend refreshed before start");
return 0;
#else
return -1;
#endif
}
bool ScreenCapturerLinux::TryFallbackToDrm(bool show_cursor) {
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
auto drm_backend = std::make_unique<ScreenCapturerDrm>();
int ret = drm_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback DRM init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(drm_backend.get());
ret = drm_backend->Start(show_cursor);
if (ret != 0) {
drm_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback DRM start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(drm_backend);
backend_ = BackendType::kDrm;
LOG_INFO("Linux screen capturer fallback switched to DRM");
return true;
#else
(void)show_cursor;
LOG_WARN("Linux screen capturer DRM fallback is disabled at build time");
return false;
#endif
}
bool ScreenCapturerLinux::TryFallbackToX11(bool show_cursor) {
auto x11_backend = std::make_unique<ScreenCapturerX11>();
int ret = x11_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback X11 init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(x11_backend.get());
ret = x11_backend->Start(show_cursor);
if (ret != 0) {
x11_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback X11 start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(x11_backend);
backend_ = BackendType::kX11;
LOG_INFO("Linux screen capturer fallback switched to X11");
return true;
}
bool ScreenCapturerLinux::TryFallbackToWayland(bool show_cursor) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
auto wayland_backend = std::make_unique<ScreenCapturerWayland>();
int ret = wayland_backend->Init(fps_, callback_);
if (ret != 0) {
LOG_ERROR("Linux screen capturer fallback Wayland init failed: {}", ret);
return false;
}
UpdateAliasesFromBackend(wayland_backend.get());
ret = wayland_backend->Start(show_cursor);
if (ret != 0) {
wayland_backend->Destroy();
LOG_ERROR("Linux screen capturer fallback Wayland start failed: {}", ret);
return false;
}
if (impl_) {
impl_->Stop();
impl_->Destroy();
}
impl_ = std::move(wayland_backend);
backend_ = BackendType::kWayland;
LOG_INFO("Linux screen capturer fallback switched to Wayland");
return true;
#else
(void)show_cursor;
LOG_WARN("Linux screen capturer Wayland fallback is disabled at build time");
return false;
#endif
}
void ScreenCapturerLinux::UpdateAliasesFromBackend(ScreenCapturer* backend) {
if (!backend) {
return;
}
const auto backend_displays = backend->GetDisplayInfoList();
if (backend_displays.empty()) {
return;
}
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
if (canonical_displays_.empty()) {
canonical_displays_ = backend_displays;
for (const auto& display : backend_displays) {
label_alias_[display.name] = display.name;
}
return;
}
if (canonical_displays_.size() < backend_displays.size()) {
for (size_t i = canonical_displays_.size(); i < backend_displays.size();
++i) {
canonical_displays_.push_back(backend_displays[i]);
}
}
for (size_t i = 0; i < backend_displays.size(); ++i) {
const std::string mapped_name = i < canonical_displays_.size()
? canonical_displays_[i].name
: backend_displays[i].name;
label_alias_[backend_displays[i].name] = mapped_name;
if (i < canonical_displays_.size()) {
// Keep original stable names, but refresh geometry from active backend.
canonical_displays_[i].handle = backend_displays[i].handle;
canonical_displays_[i].is_primary = backend_displays[i].is_primary;
canonical_displays_[i].left = backend_displays[i].left;
canonical_displays_[i].top = backend_displays[i].top;
canonical_displays_[i].right = backend_displays[i].right;
canonical_displays_[i].bottom = backend_displays[i].bottom;
canonical_displays_[i].width = backend_displays[i].width;
canonical_displays_[i].height = backend_displays[i].height;
}
}
}
std::string ScreenCapturerLinux::MapDisplayName(const char* display_name) const {
std::string input_name = display_name ? display_name : "";
if (input_name.empty()) {
return input_name;
}
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(input_name);
if (it != label_alias_.end()) {
return it->second;
}
if (canonical_displays_.size() == 1) {
return canonical_displays_[0].name;
}
return input_name;
}
} // namespace crossdesk

View File

@@ -1,66 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_LINUX_H_
#define _SCREEN_CAPTURER_LINUX_H_
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerLinux : public ScreenCapturer {
public:
ScreenCapturerLinux();
~ScreenCapturerLinux();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
enum class BackendType { kNone, kX11, kDrm, kWayland };
private:
int InitX11();
int InitDrm();
int InitWayland();
int RefreshWaylandBackend();
bool TryFallbackToDrm(bool show_cursor);
bool TryFallbackToX11(bool show_cursor);
bool TryFallbackToWayland(bool show_cursor);
void UpdateAliasesFromBackend(ScreenCapturer* backend);
std::string MapDisplayName(const char* display_name) const;
private:
std::unique_ptr<ScreenCapturer> impl_;
BackendType backend_ = BackendType::kNone;
int fps_ = 60;
cb_desktop_data callback_;
cb_desktop_data callback_orig_;
std::vector<DisplayInfo> canonical_displays_;
mutable std::mutex alias_mutex_;
std::unordered_map<std::string, std::string> label_alias_;
};
} // namespace crossdesk
#endif

View File

@@ -1,242 +0,0 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#if !CROSSDESK_WAYLAND_BUILD_ENABLED
#error "Wayland capturer requires USE_WAYLAND=true and Wayland development headers"
#endif
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <thread>
#include "platform.h"
#include "rd_log.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
namespace {
int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
struct PipeWireRecoveryConfig {
ScreenCapturerWayland::PipeWireConnectMode mode;
bool relaxed_connect = false;
};
constexpr auto kPipeWireCloseSettleDelay = std::chrono::milliseconds(200);
} // namespace
ScreenCapturerWayland::ScreenCapturerWayland() {}
ScreenCapturerWayland::~ScreenCapturerWayland() { Destroy(); }
int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
Destroy();
if (!IsWaylandSession()) {
LOG_ERROR("Wayland screen capturer requires a Wayland session");
return -1;
}
if (!cb) {
LOG_ERROR("Wayland screen capturer callback is null");
return -1;
}
if (!CheckPortalAvailability()) {
LOG_ERROR("xdg-desktop-portal screencast service is unavailable");
return -1;
}
fps_ = fps;
callback_ = cb;
pointer_granted_ = false;
shared_session_registered_ = false;
display_info_list_.clear();
display_info_list_.push_back(
DisplayInfo(display_name_, 0, 0, kFallbackWidth, kFallbackHeight));
monitor_index_ = 0;
initial_monitor_index_ = 0;
frame_width_ = kFallbackWidth;
frame_height_ = kFallbackHeight;
frame_stride_ = kFallbackWidth * 4;
logical_width_ = kFallbackWidth;
logical_height_ = kFallbackHeight;
y_plane_.resize(kFallbackWidth * kFallbackHeight);
uv_plane_.resize((kFallbackWidth / 2) * (kFallbackHeight / 2) * 2);
return 0;
}
int ScreenCapturerWayland::Destroy() {
Stop();
y_plane_.clear();
uv_plane_.clear();
display_info_list_.clear();
callback_ = nullptr;
return 0;
}
int ScreenCapturerWayland::Start(bool show_cursor) {
if (running_) {
return 0;
}
show_cursor_ = show_cursor;
paused_ = false;
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(0);
pipewire_last_frame_ms_.store(0);
running_ = true;
thread_ = std::thread([this]() { Run(); });
return 0;
}
int ScreenCapturerWayland::Stop() {
running_ = false;
if (thread_.joinable()) {
thread_.join();
}
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
return 0;
}
int ScreenCapturerWayland::Pause([[maybe_unused]] int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerWayland::Resume([[maybe_unused]] int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerWayland::SwitchTo(int monitor_index) {
if (monitor_index != 0) {
LOG_WARN("Wayland screencast currently supports one logical display");
return -1;
}
monitor_index_ = 0;
return 0;
}
int ScreenCapturerWayland::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerWayland::GetDisplayInfoList() {
return display_info_list_;
}
void ScreenCapturerWayland::Run() {
static constexpr PipeWireRecoveryConfig kRecoveryConfigs[] = {
{PipeWireConnectMode::kTargetObject, false},
{PipeWireConnectMode::kAny, true},
{PipeWireConnectMode::kNodeId, false},
{PipeWireConnectMode::kNodeId, true},
};
int recovery_index = 0;
auto setup_pipewire = [this, &recovery_index]() -> bool {
const auto& config = kRecoveryConfigs[recovery_index];
return OpenPipeWireRemote() &&
SetupPipeWireStream(config.relaxed_connect, config.mode);
};
auto setup_pipeline = [this, &setup_pipewire]() -> bool {
return ConnectSessionBus() && CreatePortalSession() &&
SelectPortalDevices() && SelectPortalSource() &&
StartPortalSession() && setup_pipewire();
};
if (!setup_pipeline()) {
running_ = false;
CleanupPipeWire();
ClosePortalSession();
CleanupDbus();
return;
}
while (running_) {
if (!paused_) {
const int64_t now = NowMs();
const int64_t stream_start = pipewire_stream_start_ms_.load();
const int64_t last_frame = pipewire_last_frame_ms_.load();
const bool format_ready = pipewire_format_ready_.load();
const bool format_timeout =
stream_start > 0 && !format_ready && (now - stream_start) > 1200;
const bool first_frame_timeout =
stream_start > 0 && format_ready && last_frame == 0 &&
(now - stream_start) > 4000;
const bool frame_stall = last_frame > 0 && (now - last_frame) > 5000;
if (format_timeout || first_frame_timeout || frame_stall) {
if (recovery_index + 1 >=
static_cast<int>(sizeof(kRecoveryConfigs) /
sizeof(kRecoveryConfigs[0]))) {
LOG_ERROR(
"Wayland capture stalled and recovery limit reached, "
"format_ready={}, stream_start={}, last_frame={}, attempts={}",
format_ready, stream_start, last_frame, recovery_index);
running_ = false;
break;
}
++recovery_index;
const char* reason = format_timeout
? "format-timeout"
: (first_frame_timeout ? "first-frame-timeout"
: "frame-stall");
const auto& config = kRecoveryConfigs[recovery_index];
LOG_WARN(
"Wayland capture stalled ({}) - retrying PipeWire only, "
"attempt {}/{}, mode={}, relaxed_connect={}",
reason, recovery_index,
static_cast<int>(sizeof(kRecoveryConfigs) /
sizeof(kRecoveryConfigs[0])) -
1,
config.mode == PipeWireConnectMode::kTargetObject
? "target-object"
: (config.mode == PipeWireConnectMode::kNodeId ? "node-id"
: "any"),
config.relaxed_connect);
CleanupPipeWire();
if (!setup_pipewire()) {
LOG_ERROR("Wayland PipeWire-only recovery failed at attempt {}",
recovery_index);
running_ = false;
break;
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
CleanupPipeWire();
if (!session_handle_.empty()) {
std::this_thread::sleep_for(kPipeWireCloseSettleDelay);
}
ClosePortalSession();
CleanupDbus();
}
} // namespace crossdesk

View File

@@ -1,110 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WAYLAND_H_
#define _SCREEN_CAPTURER_WAYLAND_H_
struct DBusConnection;
struct pw_context;
struct pw_core;
struct pw_stream;
struct pw_thread_loop;
#include <atomic>
#include <cstdint>
#include <string>
#include <thread>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerWayland : public ScreenCapturer {
public:
enum class PipeWireConnectMode { kTargetObject, kNodeId, kAny };
public:
ScreenCapturerWayland();
~ScreenCapturerWayland();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
bool CheckPortalAvailability() const;
bool ConnectSessionBus();
bool CreatePortalSession();
bool SelectPortalDevices();
bool SelectPortalSource();
bool StartPortalSession();
bool OpenPipeWireRemote();
bool SetupPipeWireStream(bool relaxed_connect, PipeWireConnectMode mode);
void Run();
void CleanupPipeWire();
void CleanupDbus();
void ClosePortalSession();
void HandlePipeWireBuffer();
void UpdateDisplayGeometry(int width, int height);
private:
static constexpr int kFallbackWidth = 1920;
static constexpr int kFallbackHeight = 1080;
std::thread thread_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
std::atomic<bool> pipewire_format_ready_{false};
std::atomic<int64_t> pipewire_stream_start_ms_{0};
std::atomic<int64_t> pipewire_last_frame_ms_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
DBusConnection* dbus_connection_ = nullptr;
std::string session_handle_;
std::string display_name_ = "WAYLAND0";
uint32_t pipewire_node_id_ = 0;
int pipewire_fd_ = -1;
pw_thread_loop* pw_thread_loop_ = nullptr;
pw_context* pw_context_ = nullptr;
pw_core* pw_core_ = nullptr;
pw_stream* pw_stream_ = nullptr;
void* stream_listener_ = nullptr;
bool pipewire_initialized_ = false;
bool pipewire_thread_loop_started_ = false;
bool pointer_granted_ = false;
bool shared_session_registered_ = false;
uint32_t spa_video_format_ = 0;
int frame_width_ = 0;
int frame_height_ = 0;
int frame_stride_ = 0;
int logical_width_ = 0;
int logical_height_ = 0;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
} // namespace crossdesk
#endif

View File

@@ -1,46 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-22
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WAYLAND_BUILD_H_
#define _SCREEN_CAPTURER_WAYLAND_BUILD_H_
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#define CROSSDESK_WAYLAND_BUILD_ENABLED 1
#include <dbus/dbus.h>
#include <pipewire/keys.h>
#include <pipewire/pipewire.h>
#include <pipewire/stream.h>
#include <pipewire/thread-loop.h>
#include <spa/param/param.h>
#include <spa/param/format-utils.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/video/raw.h>
#include <spa/buffer/meta.h>
#include <spa/utils/result.h>
#if defined(__has_include)
#if __has_include(<spa/param/buffers.h>)
#include <spa/param/buffers.h>
#endif
#endif
#define CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS 1u
#define CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS 2u
#define CROSSDESK_SPA_PARAM_BUFFERS_SIZE 3u
#define CROSSDESK_SPA_PARAM_BUFFERS_STRIDE 4u
#define CROSSDESK_SPA_PARAM_META_TYPE 1u
#define CROSSDESK_SPA_PARAM_META_SIZE 2u
#else
#define CROSSDESK_WAYLAND_BUILD_ENABLED 0
#endif
#endif

View File

@@ -1,630 +0,0 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#if CROSSDESK_WAYLAND_BUILD_ENABLED
#include <chrono>
#include <cstdint>
#include <thread>
#include <unistd.h>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
const char* PipeWireFormatName(uint32_t spa_format) {
switch (spa_format) {
case SPA_VIDEO_FORMAT_BGRx:
return "BGRx";
case SPA_VIDEO_FORMAT_BGRA:
return "BGRA";
#ifdef SPA_VIDEO_FORMAT_RGBx
case SPA_VIDEO_FORMAT_RGBx:
return "RGBx";
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
case SPA_VIDEO_FORMAT_RGBA:
return "RGBA";
#endif
default:
return "unsupported";
}
}
const char* PipeWireConnectModeName(
ScreenCapturerWayland::PipeWireConnectMode mode) {
switch (mode) {
case ScreenCapturerWayland::PipeWireConnectMode::kTargetObject:
return "target-object";
case ScreenCapturerWayland::PipeWireConnectMode::kNodeId:
return "node-id";
case ScreenCapturerWayland::PipeWireConnectMode::kAny:
return "any";
default:
return "unknown";
}
}
int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
struct PipeWireTargetLookupState {
pw_thread_loop* loop = nullptr;
uint32_t target_node_id = 0;
int sync_seq = -1;
bool done = false;
bool found = false;
std::string object_serial;
};
std::string LookupPipeWireTargetObjectSerial(pw_core* core,
pw_thread_loop* loop,
uint32_t node_id) {
if (!core || !loop || node_id == 0) {
return "";
}
PipeWireTargetLookupState state;
state.loop = loop;
state.target_node_id = node_id;
pw_registry* registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
if (!registry) {
return "";
}
spa_hook registry_listener{};
spa_hook core_listener{};
pw_registry_events registry_events{};
registry_events.version = PW_VERSION_REGISTRY_EVENTS;
registry_events.global =
[](void* userdata, uint32_t id, uint32_t permissions, const char* type,
uint32_t version, const spa_dict* props) {
(void)permissions;
(void)version;
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state || !props || id != state->target_node_id || !type) {
return;
}
if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) {
return;
}
const char* object_serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL);
if (!object_serial || object_serial[0] == '\0') {
object_serial = spa_dict_lookup(props, "object.serial");
}
if (!object_serial || object_serial[0] == '\0') {
return;
}
state->object_serial = object_serial;
state->found = true;
};
pw_core_events core_events{};
core_events.version = PW_VERSION_CORE_EVENTS;
core_events.done = [](void* userdata, uint32_t id, int seq) {
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state || id != PW_ID_CORE || seq != state->sync_seq) {
return;
}
state->done = true;
pw_thread_loop_signal(state->loop, false);
};
core_events.error = [](void* userdata, uint32_t id, int seq, int res,
const char* message) {
(void)id;
(void)seq;
(void)res;
auto* state = static_cast<PipeWireTargetLookupState*>(userdata);
if (!state) {
return;
}
LOG_WARN("PipeWire registry lookup error: {}",
message ? message : "unknown");
state->done = true;
pw_thread_loop_signal(state->loop, false);
};
pw_registry_add_listener(registry, &registry_listener, &registry_events,
&state);
pw_core_add_listener(core, &core_listener, &core_events, &state);
state.sync_seq = pw_core_sync(core, PW_ID_CORE, 0);
while (!state.done) {
pw_thread_loop_wait(loop);
}
spa_hook_remove(&registry_listener);
spa_hook_remove(&core_listener);
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(registry));
return state.found ? state.object_serial : "";
}
int BytesPerPixel(uint32_t spa_format) {
switch (spa_format) {
case SPA_VIDEO_FORMAT_BGRx:
case SPA_VIDEO_FORMAT_BGRA:
#ifdef SPA_VIDEO_FORMAT_RGBx
case SPA_VIDEO_FORMAT_RGBx:
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
case SPA_VIDEO_FORMAT_RGBA:
#endif
return 4;
default:
return 0;
}
}
} // namespace
bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
PipeWireConnectMode mode) {
if (pipewire_fd_ < 0 || pipewire_node_id_ == 0) {
return false;
}
if (!pipewire_initialized_) {
pw_init(nullptr, nullptr);
pipewire_initialized_ = true;
}
pw_thread_loop_ = pw_thread_loop_new("crossdesk-wayland-capture", nullptr);
if (!pw_thread_loop_) {
LOG_ERROR("Failed to create PipeWire thread loop");
return false;
}
if (pw_thread_loop_start(pw_thread_loop_) < 0) {
LOG_ERROR("Failed to start PipeWire thread loop");
CleanupPipeWire();
return false;
}
pipewire_thread_loop_started_ = true;
pw_thread_loop_lock(pw_thread_loop_);
pw_context_ =
pw_context_new(pw_thread_loop_get_loop(pw_thread_loop_), nullptr, 0);
if (!pw_context_) {
LOG_ERROR("Failed to create PipeWire context");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
pw_core_ = pw_context_connect_fd(pw_context_, pipewire_fd_, nullptr, 0);
if (!pw_core_) {
LOG_ERROR("Failed to connect to PipeWire remote");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
pipewire_fd_ = -1;
pw_properties* stream_props =
pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY,
"Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
if (!stream_props) {
LOG_ERROR("Failed to allocate PipeWire stream properties");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
std::string target_object_serial;
if (mode == PipeWireConnectMode::kTargetObject) {
target_object_serial =
LookupPipeWireTargetObjectSerial(pw_core_, pw_thread_loop_,
pipewire_node_id_);
if (!target_object_serial.empty()) {
pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT,
target_object_serial.c_str());
LOG_INFO("PipeWire target object serial for node {} is {}",
pipewire_node_id_, target_object_serial);
} else {
LOG_WARN("PipeWire target object serial lookup failed for node {}, "
"falling back to direct target id in target-object mode",
pipewire_node_id_);
}
}
pw_stream_ = pw_stream_new(pw_core_, "CrossDesk Wayland Capture",
stream_props);
if (!pw_stream_) {
LOG_ERROR("Failed to create PipeWire stream");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
auto* listener = new spa_hook();
stream_listener_ = listener;
static const pw_stream_events stream_events = [] {
pw_stream_events events{};
events.version = PW_VERSION_STREAM_EVENTS;
events.state_changed =
[](void* userdata, enum pw_stream_state old_state,
enum pw_stream_state state, const char* error_message) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (!self) {
return;
}
if (state == PW_STREAM_STATE_ERROR) {
LOG_ERROR("PipeWire stream error: {}",
error_message ? error_message : "unknown");
self->running_ = false;
return;
}
LOG_INFO("PipeWire stream state: {} -> {}",
pw_stream_state_as_string(old_state),
pw_stream_state_as_string(state));
};
events.param_changed =
[](void* userdata, uint32_t id, const struct spa_pod* param) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (!self || id != SPA_PARAM_Format || !param) {
return;
}
spa_video_info_raw info{};
if (spa_format_video_raw_parse(param, &info) < 0) {
LOG_ERROR("Failed to parse PipeWire video format");
return;
}
self->spa_video_format_ = info.format;
self->frame_width_ = static_cast<int>(info.size.width);
self->frame_height_ = static_cast<int>(info.size.height);
self->frame_stride_ = static_cast<int>(info.size.width) * 4;
bool supported_format =
(self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRx) ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_BGRA);
#ifdef SPA_VIDEO_FORMAT_RGBx
supported_format =
supported_format ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBx);
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
supported_format =
supported_format ||
(self->spa_video_format_ == SPA_VIDEO_FORMAT_RGBA);
#endif
if (!supported_format) {
LOG_ERROR("Unsupported PipeWire pixel format: {}",
PipeWireFormatName(self->spa_video_format_));
self->running_ = false;
return;
}
const int bytes_per_pixel = BytesPerPixel(self->spa_video_format_);
if (bytes_per_pixel <= 0 || self->frame_width_ <= 0 ||
self->frame_height_ <= 0) {
LOG_ERROR("Invalid PipeWire frame layout: format={}, size={}x{}",
PipeWireFormatName(self->spa_video_format_),
self->frame_width_, self->frame_height_);
self->running_ = false;
return;
}
self->frame_stride_ = self->frame_width_ * bytes_per_pixel;
uint8_t buffer[1024];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[2];
uint32_t param_count = 0;
params[param_count++] = reinterpret_cast<const spa_pod*>(
spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
CROSSDESK_SPA_PARAM_BUFFERS_BUFFERS,
SPA_POD_CHOICE_RANGE_Int(8, 4, 16),
CROSSDESK_SPA_PARAM_BUFFERS_BLOCKS, SPA_POD_Int(1),
CROSSDESK_SPA_PARAM_BUFFERS_SIZE,
SPA_POD_CHOICE_RANGE_Int(self->frame_stride_ *
self->frame_height_,
self->frame_stride_ *
self->frame_height_,
self->frame_stride_ *
self->frame_height_),
CROSSDESK_SPA_PARAM_BUFFERS_STRIDE,
SPA_POD_CHOICE_RANGE_Int(self->frame_stride_,
self->frame_stride_,
self->frame_stride_)));
params[param_count++] = reinterpret_cast<const spa_pod*>(
spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
CROSSDESK_SPA_PARAM_META_TYPE, SPA_POD_Id(SPA_META_Header),
CROSSDESK_SPA_PARAM_META_SIZE,
SPA_POD_Int(sizeof(struct spa_meta_header))));
if (self->pw_stream_) {
pw_stream_update_params(self->pw_stream_, params, param_count);
}
self->pipewire_format_ready_.store(true);
const int pointer_width =
self->logical_width_ > 0 ? self->logical_width_ : self->frame_width_;
const int pointer_height = self->logical_height_ > 0
? self->logical_height_
: self->frame_height_;
self->UpdateDisplayGeometry(pointer_width, pointer_height);
LOG_INFO(
"PipeWire video format: {}, {}x{} stride={} (pointer space {}x{})",
PipeWireFormatName(self->spa_video_format_),
self->frame_width_, self->frame_height_, self->frame_stride_,
pointer_width, pointer_height);
};
events.process = [](void* userdata) {
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
if (self) {
self->HandlePipeWireBuffer();
}
};
return events;
}();
pw_stream_add_listener(pw_stream_, listener, &stream_events, this);
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(NowMs());
pipewire_last_frame_ms_.store(0);
uint8_t buffer[4096];
spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
const spa_pod* params[8];
int param_count = 0;
const spa_rectangle fixed_size{
static_cast<uint32_t>(logical_width_ > 0 ? logical_width_ : kFallbackWidth),
static_cast<uint32_t>(logical_height_ > 0 ? logical_height_
: kFallbackHeight)};
const spa_rectangle min_size{1u, 1u};
const spa_rectangle max_size{16384u, 16384u};
if (!relaxed_connect) {
auto add_format_param = [&](uint32_t spa_format) {
if (param_count >= static_cast<int>(sizeof(params) / sizeof(params[0]))) {
return;
}
params[param_count++] =
reinterpret_cast<const spa_pod*>(spa_pod_builder_add_object(
&builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
SPA_FORMAT_VIDEO_format, SPA_POD_Id(spa_format),
SPA_FORMAT_VIDEO_size,
SPA_POD_CHOICE_RANGE_Rectangle(&fixed_size, &min_size,
&max_size)));
};
add_format_param(SPA_VIDEO_FORMAT_BGRx);
add_format_param(SPA_VIDEO_FORMAT_BGRA);
#ifdef SPA_VIDEO_FORMAT_RGBx
add_format_param(SPA_VIDEO_FORMAT_RGBx);
#endif
#ifdef SPA_VIDEO_FORMAT_RGBA
add_format_param(SPA_VIDEO_FORMAT_RGBA);
#endif
if (param_count == 0) {
LOG_ERROR("No valid PipeWire format params were built");
pw_thread_loop_unlock(pw_thread_loop_);
CleanupPipeWire();
return false;
}
} else {
LOG_INFO("PipeWire stream using relaxed format negotiation");
}
uint32_t target_id = PW_ID_ANY;
if (mode == PipeWireConnectMode::kNodeId ||
(mode == PipeWireConnectMode::kTargetObject &&
target_object_serial.empty())) {
target_id = pipewire_node_id_;
}
LOG_INFO(
"PipeWire connecting stream: mode={}, node_id={}, target_id={}, "
"target_object_serial={}, relaxed_connect={}, param_count={}, "
"requested_size={}x{}",
PipeWireConnectModeName(mode), pipewire_node_id_, target_id,
target_object_serial.empty() ? "none" : target_object_serial.c_str(),
relaxed_connect, param_count, fixed_size.width, fixed_size.height);
const int ret = pw_stream_connect(
pw_stream_, PW_DIRECTION_INPUT, target_id,
static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS),
param_count > 0 ? params : nullptr, static_cast<uint32_t>(param_count));
pw_thread_loop_unlock(pw_thread_loop_);
if (ret < 0) {
LOG_ERROR("pw_stream_connect failed: {}", spa_strerror(ret));
CleanupPipeWire();
return false;
}
return true;
}
void ScreenCapturerWayland::CleanupPipeWire() {
const bool need_lock = pw_thread_loop_ &&
(pw_stream_ != nullptr || pw_core_ != nullptr ||
pw_context_ != nullptr);
if (need_lock) {
pw_thread_loop_lock(pw_thread_loop_);
}
if (pw_stream_) {
pw_stream_set_active(pw_stream_, false);
pw_stream_disconnect(pw_stream_);
}
if (stream_listener_) {
spa_hook_remove(static_cast<spa_hook*>(stream_listener_));
delete static_cast<spa_hook*>(stream_listener_);
stream_listener_ = nullptr;
}
if (pw_stream_) {
pw_stream_destroy(pw_stream_);
pw_stream_ = nullptr;
}
if (pw_core_) {
pw_core_disconnect(pw_core_);
pw_core_ = nullptr;
}
if (pw_context_) {
pw_context_destroy(pw_context_);
pw_context_ = nullptr;
}
if (need_lock) {
pw_thread_loop_unlock(pw_thread_loop_);
}
if (pw_thread_loop_) {
if (pipewire_thread_loop_started_) {
pw_thread_loop_stop(pw_thread_loop_);
pipewire_thread_loop_started_ = false;
}
pw_thread_loop_destroy(pw_thread_loop_);
pw_thread_loop_ = nullptr;
}
if (pipewire_fd_ >= 0) {
close(pipewire_fd_);
pipewire_fd_ = -1;
}
pipewire_format_ready_.store(false);
pipewire_stream_start_ms_.store(0);
pipewire_last_frame_ms_.store(0);
if (pipewire_initialized_) {
pw_deinit();
pipewire_initialized_ = false;
}
}
void ScreenCapturerWayland::HandlePipeWireBuffer() {
if (!pw_stream_) {
return;
}
pw_buffer* buffer = pw_stream_dequeue_buffer(pw_stream_);
if (!buffer) {
return;
}
auto requeue = [&]() { pw_stream_queue_buffer(pw_stream_, buffer); };
if (paused_) {
requeue();
return;
}
spa_buffer* spa_buffer = buffer->buffer;
if (!spa_buffer || spa_buffer->n_datas == 0 || !spa_buffer->datas[0].data) {
requeue();
return;
}
const spa_data& data = spa_buffer->datas[0];
if (!data.chunk) {
requeue();
return;
}
if (frame_width_ <= 1 || frame_height_ <= 1) {
requeue();
return;
}
uint8_t* src = static_cast<uint8_t*>(data.data);
src += data.chunk->offset;
int stride = frame_stride_;
if (data.chunk->stride > 0) {
stride = data.chunk->stride;
} else if (stride <= 0) {
stride = frame_width_ * 4;
}
int even_width = frame_width_ & ~1;
int even_height = frame_height_ & ~1;
if (even_width <= 0 || even_height <= 0) {
requeue();
return;
}
const size_t y_size = static_cast<size_t>(even_width) * even_height;
const size_t uv_size = y_size / 2;
if (y_plane_.size() != y_size) {
y_plane_.resize(y_size);
}
if (uv_plane_.size() != uv_size) {
uv_plane_.resize(uv_size);
}
libyuv::ARGBToNV12(src, stride, y_plane_.data(), even_width,
uv_plane_.data(), even_width, even_width, even_height);
std::vector<uint8_t> nv12;
nv12.reserve(y_plane_.size() + uv_plane_.size());
nv12.insert(nv12.end(), y_plane_.begin(), y_plane_.end());
nv12.insert(nv12.end(), uv_plane_.begin(), uv_plane_.end());
if (callback_) {
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
even_height, display_name_.c_str());
}
pipewire_last_frame_ms_.store(NowMs());
requeue();
}
void ScreenCapturerWayland::UpdateDisplayGeometry(int width, int height) {
if (width <= 0 || height <= 0) {
return;
}
void* stream_handle =
reinterpret_cast<void*>(static_cast<uintptr_t>(pipewire_node_id_));
if (display_info_list_.empty()) {
display_info_list_.push_back(
DisplayInfo(stream_handle, display_name_, true, 0, 0, width, height));
return;
}
auto& display = display_info_list_[0];
display.handle = stream_handle;
display.left = 0;
display.top = 0;
display.right = width;
display.bottom = height;
display.width = width;
display.height = height;
}
} // namespace crossdesk
#endif

View File

@@ -1,816 +0,0 @@
#include "screen_capturer_wayland.h"
#include "screen_capturer_wayland_build.h"
#include "wayland_portal_shared.h"
#if CROSSDESK_WAYLAND_BUILD_ENABLED
#include <chrono>
#include <cstring>
#include <functional>
#include <string>
#include <unistd.h>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop";
constexpr const char* kPortalRemoteDesktopInterface =
"org.freedesktop.portal.RemoteDesktop";
constexpr const char* kPortalScreenCastInterface =
"org.freedesktop.portal.ScreenCast";
constexpr const char* kPortalRequestInterface =
"org.freedesktop.portal.Request";
constexpr const char* kPortalSessionInterface =
"org.freedesktop.portal.Session";
constexpr const char* kPortalRequestPathPrefix =
"/org/freedesktop/portal/desktop/request/";
constexpr const char* kPortalSessionPathPrefix =
"/org/freedesktop/portal/desktop/session/";
constexpr uint32_t kScreenCastSourceMonitor = 1u;
constexpr uint32_t kCursorModeHidden = 1u;
constexpr uint32_t kCursorModeEmbedded = 2u;
constexpr uint32_t kRemoteDesktopDevicePointer = 2u;
std::string MakeToken(const char* prefix) {
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
return std::string(prefix) + "_" + std::to_string(now);
}
void LogDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
void AppendDictEntryString(DBusMessageIter* dict, const char* key,
const std::string& value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
const char* value_cstr = value.c_str();
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryUint32(DBusMessageIter* dict, const char* key,
uint32_t value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryBool(DBusMessageIter* dict, const char* key, bool value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_bool_t bool_value = value ? TRUE : FALSE;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "b", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_BOOLEAN, &bool_value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
bool ReadIntLike(DBusMessageIter* iter, int* value) {
if (!iter || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(iter);
if (type == DBUS_TYPE_INT32) {
int32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = static_cast<int>(temp);
return true;
}
if (type == DBUS_TYPE_UINT32) {
uint32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = static_cast<int>(temp);
return true;
}
return false;
}
bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) {
if (!variant || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(variant);
if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) {
const char* temp = nullptr;
dbus_message_iter_get_basic(variant, &temp);
if (temp && temp[0] != '\0') {
*value = temp;
return true;
}
}
return false;
}
std::string BuildSessionHandleFromRequestPath(
const std::string& request_path, const std::string& session_handle_token) {
if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 ||
session_handle_token.empty()) {
return "";
}
const size_t sender_start = strlen(kPortalRequestPathPrefix);
const size_t token_sep = request_path.find('/', sender_start);
if (token_sep == std::string::npos || token_sep <= sender_start) {
return "";
}
const std::string sender = request_path.substr(sender_start,
token_sep - sender_start);
if (sender.empty()) {
return "";
}
return std::string(kPortalSessionPathPrefix) + sender + "/" +
session_handle_token;
}
struct PortalResponseState {
std::string request_path;
bool received = false;
DBusMessage* message = nullptr;
};
DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
auto* state = static_cast<PortalResponseState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->request_path != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (state->message) {
dbus_message_unref(state->message);
state->message = nullptr;
}
state->message = dbus_message_ref(message);
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
DBusMessage* WaitForPortalResponse(DBusConnection* connection,
const std::string& request_path,
const std::atomic<bool>& running,
int timeout_ms = 120000) {
if (!connection || request_path.empty()) {
return nullptr;
}
PortalResponseState state;
state.request_path = request_path;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalRequestInterface) +
"',member='Response',path='" + request_path + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_add_match", &error);
dbus_error_free(&error);
return nullptr;
}
dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state,
nullptr);
auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (running.load() && !state.received &&
std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
return state.message;
}
bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) {
if (!reply || !request_path) {
return false;
}
const char* path = nullptr;
DBusError error;
dbus_error_init(&error);
const dbus_bool_t ok = dbus_message_get_args(
reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
if (!ok || !path) {
LogDbusError("dbus_message_get_args(request_path)", &error);
dbus_error_free(&error);
return false;
}
*request_path = path;
return true;
}
bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code,
DBusMessageIter* results_array) {
if (!message || !response_code || !results_array) {
return false;
}
DBusMessageIter iter;
if (!dbus_message_iter_init(message, &iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
return false;
}
dbus_message_iter_get_basic(&iter, response_code);
if (!dbus_message_iter_next(&iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
return false;
}
*results_array = iter;
return true;
}
bool SendPortalRequestAndHandleResponse(
DBusConnection* connection, const char* interface_name,
const char* method_name,
const char* action_name,
const std::function<bool(DBusMessage*)>& append_message_args,
const std::atomic<bool>& running,
const std::function<bool(uint32_t, DBusMessageIter*)>& handle_results,
std::string* request_path_out = nullptr) {
if (!connection || !interface_name || interface_name[0] == '\0' ||
!method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message =
dbus_message_new_method_call(kPortalBusName, kPortalObjectPath,
interface_name, method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
if (append_message_args && !append_message_args(message)) {
dbus_message_unref(message);
LOG_ERROR("{} arguments are malformed", method_name);
return false;
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(connection, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(action_name ? action_name : method_name, &error);
dbus_error_free(&error);
return false;
}
std::string request_path;
const bool got_request_path = ExtractRequestPath(reply, &request_path);
dbus_message_unref(reply);
if (!got_request_path) {
return false;
}
if (request_path_out) {
*request_path_out = request_path;
}
DBusMessage* response =
WaitForPortalResponse(connection, request_path, running);
if (!response) {
LOG_ERROR("Timed out waiting for {} response", method_name);
return false;
}
uint32_t response_code = 1;
DBusMessageIter results;
const bool parsed = ExtractPortalResponse(response, &response_code, &results);
if (!parsed) {
dbus_message_unref(response);
LOG_ERROR("{} response was malformed", method_name);
return false;
}
const bool ok = handle_results ? handle_results(response_code, &results)
: (response_code == 0);
dbus_message_unref(response);
return ok;
}
} // namespace
bool ScreenCapturerWayland::CheckPortalAvailability() const {
DBusError error;
dbus_error_init(&error);
DBusConnection* connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
if (!connection) {
LogDbusError("dbus_bus_get", &error);
dbus_error_free(&error);
return false;
}
const dbus_bool_t has_owner = dbus_bus_name_has_owner(
connection, kPortalBusName, &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_name_has_owner", &error);
dbus_error_free(&error);
dbus_connection_unref(connection);
return false;
}
dbus_connection_unref(connection);
return has_owner == TRUE;
}
bool ScreenCapturerWayland::ConnectSessionBus() {
if (dbus_connection_) {
return true;
}
DBusError error;
dbus_error_init(&error);
dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
if (!dbus_connection_) {
LogDbusError("dbus_bus_get_private", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE);
return true;
}
bool ScreenCapturerWayland::CreatePortalSession() {
if (!dbus_connection_) {
return false;
}
const std::string session_handle_token = MakeToken("crossdesk_session");
std::string request_path;
const bool ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession",
"CreateSession",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "session_handle_token",
session_handle_token);
AppendDictEntryString(&options, "handle_token", MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_,
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("CreateSession was denied or malformed, response={}",
response_code);
return false;
}
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT &&
strcmp(key, "session_handle") == 0) {
DBusMessageIter variant;
std::string parsed_handle;
dbus_message_iter_recurse(&entry, &variant);
if (ReadPathLikeVariant(&variant, &parsed_handle) &&
!parsed_handle.empty()) {
session_handle_ = parsed_handle;
break;
}
}
}
dbus_message_iter_next(&dict);
}
return true;
},
&request_path);
if (!ok) {
return false;
}
if (session_handle_.empty()) {
const std::string fallback_handle = BuildSessionHandleFromRequestPath(
request_path, session_handle_token);
if (!fallback_handle.empty()) {
LOG_WARN(
"CreateSession response missing session_handle, using derived handle "
"{}",
fallback_handle);
session_handle_ = fallback_handle;
}
}
if (session_handle_.empty()) {
LOG_ERROR("CreateSession response did not include a session handle");
return false;
}
return true;
}
bool ScreenCapturerWayland::SelectPortalSource() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalScreenCastInterface, "SelectSources",
"SelectSources",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kScreenCastSourceMonitor);
AppendDictEntryBool(&options, "multiple", false);
AppendDictEntryUint32(
&options, "cursor_mode",
show_cursor_ ? kCursorModeEmbedded : kCursorModeHidden);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_, [](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("SelectSources was denied or malformed, response={}",
response_code);
return false;
}
return true;
});
}
bool ScreenCapturerWayland::SelectPortalDevices() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
return SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices",
"SelectDevices",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kRemoteDesktopDevicePointer);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_, [](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("SelectDevices was denied or malformed, response={}",
response_code);
return false;
}
return true;
});
}
bool ScreenCapturerWayland::StartPortalSession() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
const char* session_handle = session_handle_.c_str();
const char* parent_window = "";
pointer_granted_ = false;
const bool ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "handle_token", MakeToken("crossdesk_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
running_,
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("Start was denied or malformed, response={}", response_code);
return false;
}
uint32_t granted_devices = 0;
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&entry, &variant);
if (strcmp(key, "devices") == 0) {
int granted_devices_int = 0;
if (ReadIntLike(&variant, &granted_devices_int) &&
granted_devices_int >= 0) {
granted_devices = static_cast<uint32_t>(granted_devices_int);
}
} else if (strcmp(key, "streams") == 0) {
DBusMessageIter streams;
dbus_message_iter_recurse(&variant, &streams);
if (dbus_message_iter_get_arg_type(&streams) == DBUS_TYPE_STRUCT) {
DBusMessageIter stream;
dbus_message_iter_recurse(&streams, &stream);
if (dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_UINT32) {
dbus_message_iter_get_basic(&stream, &pipewire_node_id_);
}
if (dbus_message_iter_next(&stream) &&
dbus_message_iter_get_arg_type(&stream) == DBUS_TYPE_ARRAY) {
DBusMessageIter props;
int stream_width = 0;
int stream_height = 0;
int logical_width = 0;
int logical_height = 0;
dbus_message_iter_recurse(&stream, &props);
while (dbus_message_iter_get_arg_type(&props) !=
DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&props) ==
DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter prop_entry;
dbus_message_iter_recurse(&props, &prop_entry);
const char* prop_key = nullptr;
dbus_message_iter_get_basic(&prop_entry, &prop_key);
if (prop_key && dbus_message_iter_next(&prop_entry) &&
dbus_message_iter_get_arg_type(&prop_entry) ==
DBUS_TYPE_VARIANT) {
DBusMessageIter prop_variant;
dbus_message_iter_recurse(&prop_entry, &prop_variant);
if (dbus_message_iter_get_arg_type(&prop_variant) ==
DBUS_TYPE_STRUCT) {
DBusMessageIter size_iter;
int width = 0;
int height = 0;
dbus_message_iter_recurse(&prop_variant, &size_iter);
if (ReadIntLike(&size_iter, &width) &&
dbus_message_iter_next(&size_iter) &&
ReadIntLike(&size_iter, &height)) {
if (strcmp(prop_key, "logical_size") == 0) {
logical_width = width;
logical_height = height;
} else if (strcmp(prop_key, "size") == 0) {
stream_width = width;
stream_height = height;
}
}
}
}
}
dbus_message_iter_next(&props);
}
const int picked_width =
logical_width > 0 ? logical_width : stream_width;
const int picked_height =
logical_height > 0 ? logical_height : stream_height;
LOG_INFO(
"Wayland portal stream geometry: stream_size={}x{}, "
"logical_size={}x{}, pointer_space={}x{}",
stream_width, stream_height, logical_width,
logical_height, picked_width, picked_height);
if (logical_width > 0 && logical_height > 0) {
logical_width_ = logical_width;
logical_height_ = logical_height;
UpdateDisplayGeometry(logical_width_, logical_height_);
} else if (stream_width > 0 && stream_height > 0) {
logical_width_ = stream_width;
logical_height_ = stream_height;
UpdateDisplayGeometry(logical_width_, logical_height_);
}
}
}
}
}
}
dbus_message_iter_next(&dict);
}
pointer_granted_ =
(granted_devices & kRemoteDesktopDevicePointer) != 0;
return true;
});
if (!ok) {
return false;
}
if (pipewire_node_id_ == 0) {
LOG_ERROR("Start response did not include a PipeWire node id");
return false;
}
if (!pointer_granted_) {
LOG_ERROR("Start response did not grant pointer control");
return false;
}
shared_session_registered_ = PublishSharedWaylandPortalSession(
SharedWaylandPortalSessionInfo{
dbus_connection_, session_handle_, pipewire_node_id_, logical_width_,
logical_height_, pointer_granted_});
if (!shared_session_registered_) {
LOG_WARN("Failed to publish shared Wayland portal session");
}
LOG_INFO("Wayland screencast ready, node_id={}", pipewire_node_id_);
return true;
}
bool ScreenCapturerWayland::OpenPipeWireRemote() {
if (!dbus_connection_ || session_handle_.empty()) {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, kPortalScreenCastInterface,
"OpenPipeWireRemote");
if (!message) {
LOG_ERROR("Failed to allocate OpenPipeWireRemote message");
return false;
}
DBusMessageIter iter;
DBusMessageIter options;
const char* session_handle = session_handle_.c_str();
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &options);
dbus_message_iter_close_container(&iter, &options);
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(dbus_connection_, message, -1,
&error);
dbus_message_unref(message);
if (!reply) {
LogDbusError("OpenPipeWireRemote", &error);
dbus_error_free(&error);
return false;
}
DBusMessageIter reply_iter;
if (!dbus_message_iter_init(reply, &reply_iter) ||
dbus_message_iter_get_arg_type(&reply_iter) != DBUS_TYPE_UNIX_FD) {
LOG_ERROR("OpenPipeWireRemote returned an unexpected payload");
dbus_message_unref(reply);
return false;
}
int received_fd = -1;
dbus_message_iter_get_basic(&reply_iter, &received_fd);
dbus_message_unref(reply);
if (received_fd < 0) {
LOG_ERROR("OpenPipeWireRemote returned an invalid fd");
return false;
}
pipewire_fd_ = dup(received_fd);
if (pipewire_fd_ < 0) {
LOG_ERROR("Failed to duplicate PipeWire remote fd");
return false;
}
return true;
}
void ScreenCapturerWayland::CleanupDbus() {
if (!dbus_connection_) {
return;
}
if (shared_session_registered_) {
return;
}
dbus_connection_close(dbus_connection_);
dbus_connection_unref(dbus_connection_);
dbus_connection_ = nullptr;
}
void ScreenCapturerWayland::ClosePortalSession() {
if (shared_session_registered_) {
DBusConnection* close_connection = nullptr;
std::string close_session_handle;
ReleaseSharedWaylandPortalSession(&close_connection, &close_session_handle);
shared_session_registered_ = false;
if (close_connection) {
CloseWaylandPortalSessionAndConnection(close_connection,
close_session_handle,
"Session.Close");
}
dbus_connection_ = nullptr;
} else if (dbus_connection_ && !session_handle_.empty()) {
CloseWaylandPortalSessionAndConnection(dbus_connection_, session_handle_,
"Session.Close");
dbus_connection_ = nullptr;
}
session_handle_.clear();
pipewire_node_id_ = 0;
UpdateDisplayGeometry(logical_width_ > 0 ? logical_width_ : kFallbackWidth,
logical_height_ > 0 ? logical_height_
: kFallbackHeight);
pointer_granted_ = false;
}
} // namespace crossdesk
#endif

View File

@@ -5,9 +5,7 @@
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/Xrandr.h>
#include <algorithm>
#include <chrono>
#include <mutex>
#include <thread>
#include "libyuv.h"
@@ -15,58 +13,11 @@
namespace crossdesk {
namespace {
std::atomic<int> g_x11_last_error_code{0};
std::mutex g_x11_error_handler_mutex;
int CaptureX11ErrorHandler([[maybe_unused]] Display* display,
XErrorEvent* error_event) {
if (error_event) {
g_x11_last_error_code.store(error_event->error_code);
} else {
g_x11_last_error_code.store(-1);
}
return 0;
}
class ScopedX11ErrorTrap {
public:
explicit ScopedX11ErrorTrap(Display* display)
: display_(display), lock_(g_x11_error_handler_mutex) {
g_x11_last_error_code.store(0);
previous_handler_ = XSetErrorHandler(CaptureX11ErrorHandler);
}
~ScopedX11ErrorTrap() {
if (display_) {
XSync(display_, False);
}
XSetErrorHandler(previous_handler_);
}
int SyncAndGetError() const {
if (display_) {
XSync(display_, False);
}
return g_x11_last_error_code.load();
}
private:
Display* display_ = nullptr;
int (*previous_handler_)(Display*, XErrorEvent*) = nullptr;
std::unique_lock<std::mutex> lock_;
};
} // namespace
ScreenCapturerX11::ScreenCapturerX11() {}
ScreenCapturerX11::~ScreenCapturerX11() { Destroy(); }
int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
Destroy();
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Cannot connect to X server");
@@ -78,7 +29,6 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
if (!screen_res_) {
LOG_ERROR("Failed to get screen resources");
XCloseDisplay(display_);
display_ = nullptr;
return 1;
}
@@ -132,11 +82,6 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
y_plane_.resize(width_ * height_);
uv_plane_.resize((width_ / 2) * (height_ / 2) * 2);
if (!ProbeCapture()) {
LOG_ERROR("X11 backend probe failed, XGetImage is not usable");
return -3;
}
return 0;
}
@@ -163,23 +108,9 @@ int ScreenCapturerX11::Start(bool show_cursor) {
show_cursor_ = show_cursor;
running_ = true;
paused_ = false;
capture_error_count_ = 0;
thread_ = std::thread([this]() {
using clock = std::chrono::steady_clock;
const auto frame_interval =
std::chrono::milliseconds(std::max(1, 1000 / std::max(1, fps_)));
while (running_) {
const auto frame_start = clock::now();
if (!paused_) {
OnFrame();
}
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
clock::now() - frame_start);
if (elapsed < frame_interval) {
std::this_thread::sleep_for(frame_interval - elapsed);
}
if (!paused_) OnFrame();
}
});
return 0;
@@ -207,10 +138,6 @@ int ScreenCapturerX11::SwitchTo(int monitor_index) {
return 0;
}
int ScreenCapturerX11::ResetToInitialMonitor() {
monitor_index_ = initial_monitor_index_;
return 0;
}
std::vector<DisplayInfo> ScreenCapturerX11::GetDisplayInfoList() {
return display_info_list_;
}
@@ -221,44 +148,19 @@ void ScreenCapturerX11::OnFrame() {
return;
}
const int monitor_index = monitor_index_.load();
if (monitor_index < 0 ||
monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index);
if (monitor_index_ < 0 || monitor_index_ >= display_info_list_.size()) {
LOG_ERROR("Invalid monitor index: {}", monitor_index_.load());
return;
}
left_ = display_info_list_[monitor_index].left;
top_ = display_info_list_[monitor_index].top;
width_ = display_info_list_[monitor_index].width & ~1;
height_ = display_info_list_[monitor_index].height & ~1;
left_ = display_info_list_[monitor_index_].left;
top_ = display_info_list_[monitor_index_].top;
width_ = display_info_list_[monitor_index_].width;
height_ = display_info_list_[monitor_index_].height;
if (width_ <= 1 || height_ <= 1) {
LOG_ERROR("Invalid capture size: {}x{}", width_, height_);
return;
}
XImage* image = nullptr;
int x11_error = 0;
{
ScopedX11ErrorTrap trap(display_);
image = XGetImage(display_, root_, left_, top_, width_, height_, AllPlanes,
ZPixmap);
x11_error = trap.SyncAndGetError();
}
if (x11_error != 0 || !image) {
if (image) {
XDestroyImage(image);
}
++capture_error_count_;
if (capture_error_count_ == 1 || capture_error_count_ % 120 == 0) {
LOG_WARN("X11 capture failed: x11_error={}, image={}, consecutive={}",
x11_error, image ? "valid" : "null", capture_error_count_);
}
return;
}
capture_error_count_ = 0;
XImage* image = XGetImage(display_, root_, left_, top_, width_, height_,
AllPlanes, ZPixmap);
if (!image) return;
// if enable show cursor, draw cursor
if (show_cursor_) {
@@ -299,7 +201,7 @@ void ScreenCapturerX11::OnFrame() {
if (callback_) {
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
display_info_list_[monitor_index].name.c_str());
display_info_list_[monitor_index_].name.c_str());
}
XDestroyImage(image);
@@ -382,32 +284,4 @@ void ScreenCapturerX11::DrawCursor(XImage* image, int x, int y) {
XFree(cursor_image);
}
bool ScreenCapturerX11::ProbeCapture() {
if (!display_ || display_info_list_.empty()) {
return false;
}
const auto& first_display = display_info_list_[0];
XImage* probe_image = nullptr;
int x11_error = 0;
{
ScopedX11ErrorTrap trap(display_);
probe_image = XGetImage(display_, root_, first_display.left,
first_display.top, 1, 1, AllPlanes, ZPixmap);
x11_error = trap.SyncAndGetError();
}
if (probe_image) {
XDestroyImage(probe_image);
}
if (x11_error != 0 || !probe_image) {
LOG_WARN("X11 probe XGetImage failed: x11_error={}, image={}", x11_error,
probe_image ? "valid" : "null");
return false;
}
return true;
}
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -17,7 +17,6 @@ struct _XImage;
typedef struct _XImage XImage;
#include <atomic>
#include <cctype>
#include <cstring>
#include <functional>
#include <iostream>
@@ -43,7 +42,6 @@ class ScreenCapturerX11 : public ScreenCapturer {
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
@@ -51,7 +49,6 @@ class ScreenCapturerX11 : public ScreenCapturer {
private:
void DrawCursor(XImage* image, int x, int y);
bool ProbeCapture();
private:
Display* display_ = nullptr;
@@ -65,15 +62,13 @@ class ScreenCapturerX11 : public ScreenCapturer {
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
int fps_ = 60;
cb_desktop_data callback_;
std::vector<DisplayInfo> display_info_list_;
int capture_error_count_ = 0;
std::vector<uint8_t> y_plane_;
std::vector<uint8_t> uv_plane_;
};
} // namespace crossdesk
#endif
#endif

View File

@@ -62,13 +62,6 @@ int ScreenCapturerSck::SwitchTo(int monitor_index) {
return -1;
}
int ScreenCapturerSck::ResetToInitialMonitor() {
if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->ResetToInitialMonitor();
}
return -1;
}
std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
if (screen_capturer_sck_impl_) {
return screen_capturer_sck_impl_->GetDisplayInfoList();

View File

@@ -33,7 +33,6 @@ class ScreenCapturerSck : public ScreenCapturer {
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;

View File

@@ -70,7 +70,6 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
int Resume(int monitor_index) override { return 0; }
std::vector<DisplayInfo> GetDisplayInfoList() override { return display_info_list_; }
int ResetToInitialMonitor() override;
private:
std::vector<DisplayInfo> display_info_list_;
@@ -114,7 +113,6 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
// support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0;
int initial_monitor_index_ = 0;
};
std::string GetDisplayName(CGDirectDisplayID display_id) {
@@ -263,7 +261,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
display_id_name_map_[display_id] = name;
}
initial_monitor_index_ = 0;
return 0;
}
@@ -298,25 +295,6 @@ int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
return 0;
}
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1;
CGDirectDisplayID target_display = display_id_map_[target];
if (current_display_ == target_display) return 0;
if (stream_) {
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
std::lock_guard<std::mutex> lock(lock_);
stream_ = nil;
current_display_ = target_display;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = target_display;
StartOrReconfigureCapturer();
}
return 0;
}
int ScreenCapturerSckImpl::Destroy() {
std::lock_guard<std::mutex> lock(lock_);
if (stream_) {

View File

@@ -31,7 +31,6 @@ class ScreenCapturer {
virtual std::vector<DisplayInfo> GetDisplayInfoList() = 0;
virtual int SwitchTo(int monitor_index) = 0;
virtual int ResetToInitialMonitor() = 0;
};
} // namespace crossdesk
#endif

View File

@@ -8,9 +8,9 @@
#define _SCREEN_CAPTURER_FACTORY_H_
#ifdef _WIN32
#include "screen_capturer_win.h"
#include "screen_capturer_wgc.h"
#elif __linux__
#include "screen_capturer_linux.h"
#include "screen_capturer_x11.h"
#elif __APPLE__
// #include "screen_capturer_avf.h"
#include "screen_capturer_sck.h"
@@ -25,9 +25,9 @@ class ScreenCapturerFactory {
public:
ScreenCapturer* Create() {
#ifdef _WIN32
return new ScreenCapturerWin();
return new ScreenCapturerWgc();
#elif __linux__
return new ScreenCapturerLinux();
return new ScreenCapturerX11();
#elif __APPLE__
// return new ScreenCapturerAvf();
return new ScreenCapturerSck();
@@ -37,4 +37,4 @@ class ScreenCapturerFactory {
}
};
} // namespace crossdesk
#endif
#endif

View File

@@ -1,356 +0,0 @@
#include "screen_capturer_dxgi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
return name;
}
} // namespace
ScreenCapturerDxgi::ScreenCapturerDxgi() {}
ScreenCapturerDxgi::~ScreenCapturerDxgi() {
Stop();
Destroy();
}
int ScreenCapturerDxgi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("DXGI: callback is null");
return -1;
}
if (!InitializeDxgi()) {
LOG_ERROR("DXGI: initialize DXGI failed");
return -2;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("DXGI: no displays found");
return -3;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerDxgi::Destroy() {
Stop();
ReleaseDuplication();
outputs_.clear();
d3d_context_.Reset();
d3d_device_.Reset();
dxgi_factory_.Reset();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerDxgi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -1;
}
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerDxgi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
ReleaseDuplication();
return 0;
}
int ScreenCapturerDxgi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerDxgi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1;
}
paused_ = true;
monitor_index_ = monitor_index;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load());
return -2;
}
paused_ = false;
LOG_INFO("DXGI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerDxgi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
if (monitor_index_ == target) return 0;
if (running_) {
paused_ = true;
monitor_index_ = target;
ReleaseDuplication();
if (!CreateDuplicationForMonitor(monitor_index_)) {
paused_ = false;
return -2;
}
paused_ = false;
LOG_INFO("DXGI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
} else {
monitor_index_ = target;
}
return 0;
}
bool ScreenCapturerDxgi::InitializeDxgi() {
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
D3D_FEATURE_LEVEL feature_levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0};
D3D_FEATURE_LEVEL out_level{};
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, feature_levels,
ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, flags,
feature_levels, ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION, d3d_device_.GetAddressOf(),
&out_level, d3d_context_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: D3D11CreateDevice failed, hr={}", (int)hr);
return false;
}
}
hr = CreateDXGIFactory1(
__uuidof(IDXGIFactory1),
reinterpret_cast<void**>(dxgi_factory_.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateDXGIFactory1 failed, hr={}", (int)hr);
return false;
}
return true;
}
void ScreenCapturerDxgi::EnumerateDisplays() {
display_info_list_.clear();
outputs_.clear();
Microsoft::WRL::ComPtr<IDXGIAdapter> adapter;
for (UINT a = 0;
dxgi_factory_->EnumAdapters(a, adapter.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++a) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; adapter->EnumOutputs(o, output.ReleaseAndGetAddressOf()) !=
DXGI_ERROR_NOT_FOUND;
++o) {
DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc))) {
continue;
}
std::string name = CleanDisplayName(desc.DeviceName);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(desc.Monitor, &mi)) {
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)desc.Monitor, name, is_primary,
mi.rcMonitor.left, mi.rcMonitor.top,
mi.rcMonitor.right, mi.rcMonitor.bottom);
// primary first
if (is_primary)
display_info_list_.insert(display_info_list_.begin(), info);
else
display_info_list_.push_back(info);
outputs_.push_back(output);
}
}
}
}
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
HRESULT hr = outputs_[monitor_index]->QueryInterface(
IID_PPV_ARGS(output1.GetAddressOf()));
if (FAILED(hr)) {
LOG_ERROR("DXGI: Query IDXGIOutput1 failed, hr={}", (int)hr);
return false;
}
duplication_.Reset();
hr = output1->DuplicateOutput(d3d_device_.Get(), duplication_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: DuplicateOutput failed, hr={}", (int)hr);
return false;
}
staging_.Reset();
return true;
}
void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset();
if (duplication_) {
duplication_->ReleaseFrame();
}
duplication_.Reset();
}
void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33;
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!duplication_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
DXGI_OUTDUPL_FRAME_INFO frame_info{};
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
HRESULT hr = duplication_->AcquireNextFrame(
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
continue;
}
if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
// attempt to recreate duplication
ReleaseDuplication();
CreateDuplicationForMonitor(monitor_index_);
continue;
}
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
if (desktop_resource) {
hr = desktop_resource->QueryInterface(
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
} else {
duplication_->ReleaseFrame();
continue;
}
D3D11_TEXTURE2D_DESC src_desc{};
acquired_tex->GetDesc(&src_desc);
if (!staging_) {
D3D11_TEXTURE2D_DESC staging_desc = src_desc;
staging_desc.Usage = D3D11_USAGE_STAGING;
staging_desc.BindFlags = 0;
staging_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
staging_desc.MiscFlags = 0;
hr = d3d_device_->CreateTexture2D(&staging_desc, nullptr,
staging_.GetAddressOf());
if (FAILED(hr)) {
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
duplication_->ReleaseFrame();
continue;
}
}
d3d_context_->CopyResource(staging_.Get(), acquired_tex.Get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3d_context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr)) {
duplication_->ReleaseFrame();
continue;
}
int logical_width = static_cast<int>(src_desc.Width);
int even_width = logical_width & ~1;
int even_height = static_cast<int>(src_desc.Height) & ~1;
if (even_width <= 0 || even_height <= 0) {
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
continue;
}
int nv12_size = even_width * even_height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != even_width ||
nv12_height_ != even_height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = even_width;
nv12_height_ = even_height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
static_cast<int>(mapped.RowPitch), nv12_frame_,
even_width, nv12_frame_ + even_width * even_height,
even_width, even_width, even_height);
if (callback_) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[monitor_index_].name.c_str());
}
d3d_context_->Unmap(staging_.Get(), 0);
duplication_->ReleaseFrame();
}
}
} // namespace crossdesk

View File

@@ -1,81 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_DXGI_H_
#define _SCREEN_CAPTURER_DXGI_H_
#include <Windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerDxgi : public ScreenCapturer {
public:
ScreenCapturerDxgi();
~ScreenCapturerDxgi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
bool InitializeDxgi();
void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index);
void CaptureLoop();
void ReleaseDuplication();
private:
std::vector<DisplayInfo> display_info_list_;
std::vector<Microsoft::WRL::ComPtr<IDXGIOutput>> outputs_;
Microsoft::WRL::ComPtr<IDXGIFactory1> dxgi_factory_;
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif

View File

@@ -1,217 +0,0 @@
#include "screen_capturer_gdi.h"
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include "libyuv.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
std::string WideToUtf8(const std::wstring& wstr) {
if (wstr.empty()) return {};
int size_needed = WideCharToMultiByte(
CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
std::string result(size_needed, 0);
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), result.data(),
size_needed, nullptr, nullptr);
return result;
}
std::string CleanDisplayName(const std::wstring& wide_name) {
std::string name = WideToUtf8(wide_name);
name.erase(std::remove_if(name.begin(), name.end(),
[](unsigned char c) { return !std::isalnum(c); }),
name.end());
return name;
}
} // namespace
ScreenCapturerGdi::ScreenCapturerGdi() {}
ScreenCapturerGdi::~ScreenCapturerGdi() {
Stop();
Destroy();
}
BOOL CALLBACK ScreenCapturerGdi::EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data) {
auto displays = reinterpret_cast<std::vector<DisplayInfo>*>(data);
MONITORINFOEX mi{};
mi.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(hMonitor, &mi)) {
std::string name = CleanDisplayName(mi.szDevice);
bool is_primary = (mi.dwFlags & MONITORINFOF_PRIMARY) ? true : false;
DisplayInfo info((void*)hMonitor, name, is_primary, mi.rcMonitor.left,
mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
if (is_primary)
displays->insert(displays->begin(), info);
else
displays->push_back(info);
}
return TRUE;
}
void ScreenCapturerGdi::EnumerateDisplays() {
display_info_list_.clear();
EnumDisplayMonitors(nullptr, nullptr, EnumMonitorProc,
(LPARAM)&display_info_list_);
}
int ScreenCapturerGdi::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
callback_ = cb;
if (!callback_) {
LOG_ERROR("GDI: callback is null");
return -1;
}
EnumerateDisplays();
if (display_info_list_.empty()) {
LOG_ERROR("GDI: no displays found");
return -2;
}
monitor_index_ = 0;
initial_monitor_index_ = monitor_index_;
return 0;
}
int ScreenCapturerGdi::Destroy() {
Stop();
if (nv12_frame_) {
delete[] nv12_frame_;
nv12_frame_ = nullptr;
nv12_width_ = 0;
nv12_height_ = 0;
}
return 0;
}
int ScreenCapturerGdi::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
paused_ = false;
running_ = true;
thread_ = std::thread([this]() { CaptureLoop(); });
return 0;
}
int ScreenCapturerGdi::Stop() {
if (!running_) return 0;
running_ = false;
if (thread_.joinable()) thread_.join();
return 0;
}
int ScreenCapturerGdi::Pause(int monitor_index) {
paused_ = true;
return 0;
}
int ScreenCapturerGdi::Resume(int monitor_index) {
paused_ = false;
return 0;
}
int ScreenCapturerGdi::SwitchTo(int monitor_index) {
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("GDI: invalid monitor index {}", monitor_index);
return -1;
}
monitor_index_ = monitor_index;
LOG_INFO("GDI: switched to monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
int ScreenCapturerGdi::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1;
monitor_index_ = target;
LOG_INFO("GDI: reset to initial monitor {}:{}", monitor_index_.load(),
display_info_list_[monitor_index_].name);
return 0;
}
void ScreenCapturerGdi::CaptureLoop() {
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
HDC screen_dc = GetDC(nullptr);
while (running_) {
if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (!screen_dc) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
const auto& di = display_info_list_[monitor_index_];
int left = di.left;
int top = di.top;
int width = di.width & ~1;
int height = di.height & ~1;
if (width <= 0 || height <= 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
void* bits = nullptr;
HDC mem_dc = CreateCompatibleDC(screen_dc);
HBITMAP dib =
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
HGDIOBJ old = SelectObject(mem_dc, dib);
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
SRCCOPY | CAPTUREBLT);
if (show_cursor_) {
CURSORINFO ci{};
ci.cbSize = sizeof(CURSORINFO);
if (GetCursorInfo(&ci) && ci.flags == CURSOR_SHOWING && ci.hCursor) {
POINT pt = ci.ptScreenPos;
int cx = pt.x - left;
int cy = pt.y - top;
if (cx >= -64 && cy >= -64 && cx < width + 64 && cy < height + 64) {
DrawIconEx(mem_dc, cx, cy, ci.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
}
}
}
int stride_argb = width * 4;
int nv12_size = width * height * 3 / 2;
if (!nv12_frame_ || nv12_width_ != width || nv12_height_ != height) {
delete[] nv12_frame_;
nv12_frame_ = new unsigned char[nv12_size];
nv12_width_ = width;
nv12_height_ = height;
}
libyuv::ARGBToNV12(static_cast<const uint8_t*>(bits), stride_argb,
nv12_frame_, width, nv12_frame_ + width * height, width,
width, height);
if (callback_) {
callback_(nv12_frame_, nv12_size, width, height, di.name.c_str());
}
SelectObject(mem_dc, old);
DeleteObject(dib);
DeleteDC(mem_dc);
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
}
ReleaseDC(nullptr, screen_dc);
}
} // namespace crossdesk

View File

@@ -1,68 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_GDI_H_
#define _SCREEN_CAPTURER_GDI_H_
#include <Windows.h>
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "rd_log.h"
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerGdi : public ScreenCapturer {
public:
ScreenCapturerGdi();
~ScreenCapturerGdi();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override {
return display_info_list_;
}
private:
static BOOL CALLBACK EnumMonitorProc(HMONITOR hMonitor, HDC, LPRECT,
LPARAM data);
void EnumerateDisplays();
void CaptureLoop();
private:
std::vector<DisplayInfo> display_info_list_;
std::atomic<bool> running_{false};
std::atomic<bool> paused_{false};
std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0;
std::atomic<bool> show_cursor_{true};
std::thread thread_;
int fps_ = 60;
cb_desktop_data callback_ = nullptr;
unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0;
int nv12_height_ = 0;
};
} // namespace crossdesk
#endif

View File

@@ -56,6 +56,8 @@ BOOL WINAPI EnumMonitorProc(HMONITOR hmonitor, [[maybe_unused]] HDC hdc,
}
}
if (monitor_info_.dwFlags == DISPLAY_DEVICE_MIRRORING_DRIVER) return true;
return true;
}
@@ -147,7 +149,6 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
LOG_INFO("Default on monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
initial_monitor_index_ = monitor_index_;
return 0;
}
@@ -164,8 +165,6 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
return 4;
}
bool any_started = false;
int last_error = 0;
for (int i = 0; i < sessions_.size(); i++) {
if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i);
@@ -175,27 +174,17 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
if (sessions_[i].running_) {
LOG_ERROR("Session {} is already running", i);
} else {
int ret = sessions_[i].session_->Start(show_cursor);
if (ret != 0) {
LOG_ERROR("Session {} start failed, ret={}", i, ret);
last_error = ret;
continue;
}
sessions_[i].session_->Start(show_cursor);
if (i != 0) {
sessions_[i].session_->Pause();
sessions_[i].paused_ = true;
}
sessions_[i].running_ = true;
any_started = true;
}
running_ = running_ || any_started;
running_ = true;
}
if (!any_started) {
LOG_ERROR("WGC: no session started successfully");
return last_error != 0 ? last_error : -1;
}
return 0;
}
@@ -268,26 +257,6 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
return 0;
}
int ScreenCapturerWgc::ResetToInitialMonitor() {
if (display_info_list_.empty()) return -1;
if (initial_monitor_index_ < 0 ||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
return -1;
}
if (monitor_index_ == initial_monitor_index_) {
return 0;
}
if (running_) {
Pause(monitor_index_);
}
monitor_index_ = initial_monitor_index_;
LOG_INFO("Reset to initial monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name);
if (running_) {
Resume(monitor_index_);
}
return 0;
}
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
int id) {
if (!running_ || !on_data_) {

View File

@@ -34,7 +34,6 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::vector<DisplayInfo> GetDisplayInfoList() { return display_info_list_; }
int SwitchTo(int monitor_index);
int ResetToInitialMonitor() override;
void OnFrame(const WgcSession::wgc_session_frame& frame, int id);
@@ -46,7 +45,6 @@ class ScreenCapturerWgc : public ScreenCapturer,
MONITORINFOEX monitor_info_;
std::vector<DisplayInfo> display_info_list_;
int monitor_index_ = 0;
int initial_monitor_index_ = 0;
private:
class WgcSessionInfo {
@@ -59,8 +57,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::vector<WgcSessionInfo> sessions_;
std::atomic_bool running_{false};
std::atomic_bool inited_{false};
std::atomic_bool running_;
std::atomic_bool inited_;
int fps_ = 60;

View File

@@ -1,300 +0,0 @@
#include "screen_capturer_win.h"
#include <Windows.h>
#include <cmath>
#include <filesystem>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "rd_log.h"
#include "screen_capturer_dxgi.h"
#include "screen_capturer_gdi.h"
#include "wgc_plugin_api.h"
namespace crossdesk {
namespace {
class WgcPluginCapturer final : public ScreenCapturer {
public:
using CreateFn = ScreenCapturer* (*)();
using DestroyFn = void (*)(ScreenCapturer*);
static std::unique_ptr<ScreenCapturer> Create() {
std::filesystem::path plugin_path;
wchar_t module_path[MAX_PATH] = {0};
const DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
if (len == 0 || len >= MAX_PATH) {
return nullptr;
}
plugin_path =
std::filesystem::path(module_path).parent_path() / L"wgc_plugin.dll";
HMODULE module = LoadLibraryW(plugin_path.c_str());
if (!module) {
return nullptr;
}
auto create_fn = reinterpret_cast<CreateFn>(
GetProcAddress(module, "CrossDeskCreateWgcCapturer"));
auto destroy_fn = reinterpret_cast<DestroyFn>(
GetProcAddress(module, "CrossDeskDestroyWgcCapturer"));
if (!create_fn || !destroy_fn) {
FreeLibrary(module);
return nullptr;
}
ScreenCapturer* impl = create_fn();
if (!impl) {
FreeLibrary(module);
return nullptr;
}
return std::unique_ptr<ScreenCapturer>(
new WgcPluginCapturer(module, impl, destroy_fn));
}
~WgcPluginCapturer() override {
if (impl_) {
destroy_fn_(impl_);
impl_ = nullptr;
}
if (module_) {
FreeLibrary(module_);
module_ = nullptr;
}
}
int Init(const int fps, cb_desktop_data cb) override {
return impl_ ? impl_->Init(fps, std::move(cb)) : -1;
}
int Destroy() override { return impl_ ? impl_->Destroy() : 0; }
int Start(bool show_cursor) override {
return impl_ ? impl_->Start(show_cursor) : -1;
}
int Stop() override { return impl_ ? impl_->Stop() : 0; }
int Pause(int monitor_index) override {
return impl_ ? impl_->Pause(monitor_index) : -1;
}
int Resume(int monitor_index) override {
return impl_ ? impl_->Resume(monitor_index) : -1;
}
std::vector<DisplayInfo> GetDisplayInfoList() override {
return impl_ ? impl_->GetDisplayInfoList() : std::vector<DisplayInfo>{};
}
int SwitchTo(int monitor_index) override {
return impl_ ? impl_->SwitchTo(monitor_index) : -1;
}
int ResetToInitialMonitor() override {
return impl_ ? impl_->ResetToInitialMonitor() : -1;
}
private:
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {}
HMODULE module_ = nullptr;
ScreenCapturer* impl_ = nullptr;
DestroyFn destroy_fn_ = nullptr;
};
} // namespace
ScreenCapturerWin::ScreenCapturerWin() {}
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
fps_ = fps;
cb_orig_ = cb;
cb_ = [this](unsigned char* data, int size, int w, int h,
const char* display_name) {
std::string mapped_name;
{
std::lock_guard<std::mutex> lock(alias_mutex_);
auto it = label_alias_.find(display_name);
if (it != label_alias_.end())
mapped_name = it->second;
else
mapped_name = display_name;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
return;
}
}
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
};
int ret = -1;
impl_ = WgcPluginCapturer::Create();
impl_is_wgc_plugin_ = (impl_ != nullptr);
ret = impl_ ? impl_->Init(fps_, cb_) : -1;
if (ret == 0) {
LOG_INFO("Windows capturer: using WGC plugin");
BuildCanonicalFromImpl();
return 0;
}
LOG_WARN("Windows capturer: WGC plugin init failed (ret={}), try DXGI", ret);
impl_.reset();
impl_is_wgc_plugin_ = false;
impl_ = std::make_unique<ScreenCapturerDxgi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using DXGI Desktop Duplication");
BuildCanonicalFromImpl();
return 0;
}
LOG_WARN("Windows capturer: DXGI init failed (ret={}), fallback to GDI", ret);
impl_.reset();
impl_ = std::make_unique<ScreenCapturerGdi>();
impl_is_wgc_plugin_ = false;
ret = impl_->Init(fps_, cb_);
if (ret == 0) {
LOG_INFO("Windows capturer: using GDI BitBlt");
BuildCanonicalFromImpl();
return 0;
}
LOG_ERROR("Windows capturer: all implementations failed, ret={}", ret);
impl_.reset();
return -1;
}
int ScreenCapturerWin::Destroy() {
if (impl_) {
impl_->Destroy();
impl_.reset();
impl_is_wgc_plugin_ = false;
}
{
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
handle_to_canonical_.clear();
canonical_labels_.clear();
}
return 0;
}
int ScreenCapturerWin::Start(bool show_cursor) {
if (!impl_) return -1;
int ret = impl_->Start(show_cursor);
if (ret == 0) return 0;
LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret);
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
int r = cand->Init(fps_, cb_);
if (r != 0) return false;
int s = cand->Start(show_cursor);
if (s == 0) {
impl_ = std::move(cand);
impl_is_wgc_plugin_ = false;
RebuildAliasesFromImpl();
return true;
}
return false;
};
if (impl_is_wgc_plugin_) {
if (try_init_start(std::make_unique<ScreenCapturerDxgi>())) {
LOG_INFO("Windows capturer: fallback to DXGI");
return 0;
}
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
return 0;
}
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
LOG_INFO("Windows capturer: fallback to GDI");
return 0;
}
}
LOG_ERROR("Windows capturer: all fallbacks failed to start");
return ret;
}
int ScreenCapturerWin::Stop() {
if (!impl_) return 0;
return impl_->Stop();
}
int ScreenCapturerWin::Pause(int monitor_index) {
if (!impl_) return -1;
return impl_->Pause(monitor_index);
}
int ScreenCapturerWin::Resume(int monitor_index) {
if (!impl_) return -1;
return impl_->Resume(monitor_index);
}
int ScreenCapturerWin::SwitchTo(int monitor_index) {
if (!impl_) return -1;
return impl_->SwitchTo(monitor_index);
}
int ScreenCapturerWin::ResetToInitialMonitor() {
if (!impl_) return -1;
return impl_->ResetToInitialMonitor();
}
std::vector<DisplayInfo> ScreenCapturerWin::GetDisplayInfoList() {
if (!impl_) return {};
return impl_->GetDisplayInfoList();
}
void ScreenCapturerWin::BuildCanonicalFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
handle_to_canonical_.clear();
label_alias_.clear();
canonical_displays_ = impl_->GetDisplayInfoList();
canonical_labels_.clear();
for (const auto& di : canonical_displays_) {
handle_to_canonical_[di.handle] = di.name;
canonical_labels_.insert(di.name);
}
}
void ScreenCapturerWin::RebuildAliasesFromImpl() {
std::lock_guard<std::mutex> lock(alias_mutex_);
label_alias_.clear();
auto current = impl_->GetDisplayInfoList();
auto similar = [&](const DisplayInfo& a, const DisplayInfo& b) {
int dl = std::abs(a.left - b.left);
int dt = std::abs(a.top - b.top);
int dw = std::abs(a.width - b.width);
int dh = std::abs(a.height - b.height);
return dl <= 10 && dt <= 10 && dw <= 20 && dh <= 20;
};
for (const auto& di : current) {
std::string canonical;
auto it = handle_to_canonical_.find(di.handle);
if (it != handle_to_canonical_.end()) {
canonical = it->second;
} else {
for (const auto& c : canonical_displays_) {
if (similar(di, c) || (di.is_primary && c.is_primary)) {
canonical = c.name;
break;
}
}
}
if (!canonical.empty() && canonical != di.name) {
label_alias_[di.name] = canonical;
}
}
}
} // namespace crossdesk

View File

@@ -1,56 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-27
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SCREEN_CAPTURER_WIN_H_
#define _SCREEN_CAPTURER_WIN_H_
#include <memory>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturerWin : public ScreenCapturer {
public:
ScreenCapturerWin();
~ScreenCapturerWin();
public:
int Init(const int fps, cb_desktop_data cb) override;
int Destroy() override;
int Start(bool show_cursor) override;
int Stop() override;
int Pause(int monitor_index) override;
int Resume(int monitor_index) override;
int SwitchTo(int monitor_index) override;
int ResetToInitialMonitor() override;
std::vector<DisplayInfo> GetDisplayInfoList() override;
private:
std::unique_ptr<ScreenCapturer> impl_;
bool impl_is_wgc_plugin_ = false;
int fps_ = 60;
cb_desktop_data cb_;
cb_desktop_data cb_orig_;
std::unordered_map<void*, std::string> handle_to_canonical_;
std::unordered_map<std::string, std::string> label_alias_;
std::mutex alias_mutex_;
std::vector<DisplayInfo> canonical_displays_;
std::unordered_set<std::string> canonical_labels_;
void BuildCanonicalFromImpl();
void RebuildAliasesFromImpl();
};
} // namespace crossdesk
#endif

View File

@@ -1,29 +0,0 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-03-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WGC_PLUGIN_API_H_
#define _WGC_PLUGIN_API_H_
#include "screen_capturer.h"
namespace crossdesk {
class ScreenCapturer;
}
#if defined(_WIN32) && defined(CROSSDESK_WGC_PLUGIN_BUILD)
#define CROSSDESK_WGC_PLUGIN_API __declspec(dllexport)
#else
#define CROSSDESK_WGC_PLUGIN_API
#endif
extern "C" {
CROSSDESK_WGC_PLUGIN_API crossdesk::ScreenCapturer*
CrossDeskCreateWgcCapturer();
CROSSDESK_WGC_PLUGIN_API void CrossDeskDestroyWgcCapturer(
crossdesk::ScreenCapturer* capturer);
}
#endif

View File

@@ -1,13 +0,0 @@
#include "screen_capturer_wgc.h"
#include "wgc_plugin_api.h"
extern "C" {
crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
return new crossdesk::ScreenCapturerWgc();
}
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
delete capturer;
}
}

View File

@@ -4,14 +4,13 @@
#include <atomic>
#include <functional>
#include <iostream>
#include <memory>
#include "rd_log.h"
#define CHECK_INIT \
if (!is_initialized_) { \
LOG_ERROR("AE_NEED_INIT"); \
return 4; \
#define CHECK_INIT \
if (!is_initialized_) { \
std::cout << "AE_NEED_INIT" << std::endl; \
return 4; \
}
#define CHECK_CLOSED \
@@ -65,7 +64,6 @@ int WgcSessionImpl::Start(bool show_cursor) {
CHECK_INIT;
try {
last_show_cursor_ = show_cursor;
if (!capture_session_) {
auto current_size = capture_item_.Size();
capture_framepool_ =
@@ -91,12 +89,13 @@ int WgcSessionImpl::Start(bool show_cursor) {
// we need to test the performance later
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
capture_session_.IsCursorCaptureEnabled(show_cursor);
capture_session_.StartCapture();
capture_session_.IsCursorCaptureEnabled(show_cursor);
error = 0;
} catch (winrt::hresult_error) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl;
return 86;
} catch (...) {
return 86;
@@ -247,15 +246,8 @@ void WgcSessionImpl::OnFrame(
auto frame_captured =
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
if (!d3d11_texture_mapped_ || is_new_size) {
HRESULT tex_hr = CreateMappedTexture(frame_captured);
if (FAILED(tex_hr)) {
OutputDebugStringW(
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
.c_str());
return;
}
}
if (!d3d11_texture_mapped_ || is_new_size)
CreateMappedTexture(frame_captured);
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
frame_captured.get());
@@ -270,7 +262,6 @@ void WgcSessionImpl::OnFrame(
if (FAILED(hr)) {
OutputDebugStringW(
(L"map resource failed: " + std::to_wstring(hr)).c_str());
return;
}
// copy data from map_result.pData
@@ -299,31 +290,14 @@ void WgcSessionImpl::OnFrame(
void WgcSessionImpl::OnClosed(
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) {
std::lock_guard locker(lock_);
try {
CleanUp();
is_initialized_ = false;
if (Initialize() == 0) {
int ret = Start(last_show_cursor_);
if (ret == 0) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
} else {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: recover Start failed");
}
} else {
OutputDebugStringW(
L"WgcSessionImpl::OnClosed: recover Initialize failed");
}
} catch (...) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: exception during recover");
}
OutputDebugStringW(L"WgcSessionImpl::OnClosed");
}
int WgcSessionImpl::Initialize() {
if (is_initialized_) return 0;
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
std::cout << "AE_D3D_CREATE_DEVICE_FAILED" << std::endl;
return 1;
}
@@ -339,7 +313,7 @@ int WgcSessionImpl::Initialize() {
d3d11_device->GetImmediateContext(d3d11_device_context_.put());
} catch (winrt::hresult_error) {
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
std::cout << "AE_WGC_CREATE_CAPTURER_FAILED" << std::endl;
return 86;
} catch (...) {
return 86;
@@ -404,4 +378,4 @@ LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
// ::CloseWindow(hwnd_);
// ::DestroyWindow(hwnd_);
// }
} // namespace crossdesk
} // namespace crossdesk

View File

@@ -79,7 +79,6 @@ class WgcSessionImpl : public WgcSession {
bool is_initialized_ = false;
bool is_running_ = false;
bool is_paused_ = false;
bool last_show_cursor_ = false;
wgc_session_observer* observer_ = nullptr;
@@ -117,4 +116,4 @@ class WgcSessionImpl : public WgcSession {
// return result;
// }
} // namespace crossdesk
#endif
#endif

View File

@@ -452,17 +452,11 @@ static void MonitorThreadFunc() {
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
XEvent event;
constexpr int kEventPollIntervalMs = 20;
while (g_monitoring.load()) {
// Avoid blocking on XNextEvent so StopMonitoring() can stop quickly.
while (g_monitoring.load() && XPending(g_x11_display) > 0) {
XNextEvent(g_x11_display, &event);
if (event.type == event_base + XFixesSelectionNotify) {
HandleClipboardChange();
}
XNextEvent(g_x11_display, &event);
if (event.type == event_base + XFixesSelectionNotify) {
HandleClipboardChange();
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kEventPollIntervalMs));
}
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);

View File

@@ -97,14 +97,6 @@ class FileReceiver {
const std::filesystem::path& OutputDir() const { return output_dir_; }
void SetOutputDir(const std::filesystem::path& dir) {
output_dir_ = dir;
if (!output_dir_.empty()) {
std::error_code ec;
std::filesystem::create_directories(output_dir_, ec);
}
}
private:
static std::filesystem::path GetDefaultDesktopPath();

208
xmake.lua
View File

@@ -1,10 +1,206 @@
set_project("crossdesk")
set_license("LGPL-3.0")
includes("xmake/options.lua")
includes("xmake/platform.lua")
includes("xmake/targets.lua")
option("CROSSDESK_VERSION")
set_default("0.0.0")
set_showmenu(true)
set_description("Set CROSSDESK_VERSION for build")
option_end()
setup_options_and_dependencies()
setup_platform_settings()
setup_targets()
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")
-- set_policy("build.warning", true)
-- set_warnings("all", "extra")
-- 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
add_requires("spdlog 1.14.1", {system = false})
add_requires("imgui v1.92.1-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}})
add_requires("tinyfiledialogs 3.15.1")
if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21")
add_links("Shell32", "windowsapp", "dwmapi", "User32", "kernel32",
"SDL3-static", "gdi32", "winmm", "setupapi", "version",
"Imm32", "iphlpapi")
add_cxflags("/WX")
set_runtimes("MT")
elseif is_os("linux") then
add_links("pulse-simple", "pulse")
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
add_cxflags("-Wno-unused-variable")
elseif is_os("macosx") then
add_links("SDL3")
add_ldflags("-Wl,-ld_classic")
add_cxflags("-Wno-unused-variable")
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
end
add_packages("spdlog", "imgui", "nlohmann_json")
includes("submodules", "thirdparty")
target("rd_log")
set_kind("object")
add_packages("spdlog")
add_files("src/log/rd_log.cpp")
add_includedirs("src/log", {public = true})
target("common")
set_kind("object")
add_deps("rd_log")
add_files("src/common/*.cpp")
if is_os("macosx") then
add_files("src/common/*.mm")
end
add_includedirs("src/common", {public = true})
target("path_manager")
set_kind("object")
add_deps("rd_log")
add_includedirs("src/path_manager", {public = true})
add_files("src/path_manager/*.cpp")
add_includedirs("src/path_manager", {public = true})
target("screen_capturer")
set_kind("object")
add_deps("rd_log", "common")
add_includedirs("src/screen_capturer", {public = true})
if is_os("windows") then
add_packages("libyuv")
add_files("src/screen_capturer/windows/*.cpp")
add_includedirs("src/screen_capturer/windows", {public = true})
elseif is_os("macosx") then
add_files("src/screen_capturer/macosx/*.cpp",
"src/screen_capturer/macosx/*.mm")
add_includedirs("src/screen_capturer/macosx", {public = true})
elseif is_os("linux") then
add_packages("libyuv")
add_files("src/screen_capturer/linux/*.cpp")
add_includedirs("src/screen_capturer/linux", {public = true})
end
target("speaker_capturer")
set_kind("object")
add_deps("rd_log")
add_includedirs("src/speaker_capturer", {public = true})
if is_os("windows") then
add_packages("miniaudio")
add_files("src/speaker_capturer/windows/*.cpp")
add_includedirs("src/speaker_capturer/windows", {public = true})
elseif is_os("macosx") then
add_files("src/speaker_capturer/macosx/*.cpp",
"src/speaker_capturer/macosx/*.mm")
add_includedirs("src/speaker_capturer/macosx", {public = true})
elseif is_os("linux") then
add_files("src/speaker_capturer/linux/*.cpp")
add_includedirs("src/speaker_capturer/linux", {public = true})
end
target("device_controller")
set_kind("object")
add_deps("rd_log", "common")
add_includedirs("src/device_controller", {public = true})
if is_os("windows") then
add_files("src/device_controller/mouse/windows/*.cpp",
"src/device_controller/keyboard/windows/*.cpp")
add_includedirs("src/device_controller/mouse/windows",
"src/device_controller/keyboard/windows", {public = true})
elseif is_os("macosx") then
add_files("src/device_controller/mouse/mac/*.cpp",
"src/device_controller/keyboard/mac/*.cpp")
add_includedirs("src/device_controller/mouse/mac",
"src/device_controller/keyboard/mac", {public = true})
elseif is_os("linux") then
add_files("src/device_controller/mouse/linux/*.cpp",
"src/device_controller/keyboard/linux/*.cpp")
add_includedirs("src/device_controller/mouse/linux",
"src/device_controller/keyboard/linux", {public = true})
end
target("thumbnail")
set_kind("object")
add_packages("libyuv", "openssl3")
add_deps("rd_log", "common")
add_files("src/thumbnail/*.cpp")
add_includedirs("src/thumbnail", {public = true})
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})
target("assets")
set_kind("headeronly")
add_includedirs("src/gui/assets/localization",
"src/gui/assets/fonts",
"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("tools")
set_kind("object")
add_deps("rd_log")
add_files("src/tools/*.cpp")
if is_os("macosx") then
add_files("src/tools/*.mm")
end
add_includedirs("src/tools", {public = true})
target("gui")
set_kind("object")
add_packages("libyuv", "tinyfiledialogs")
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
"path_manager", "screen_capturer", "speaker_capturer",
"device_controller", "thumbnail", "version_checker", "tools")
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",
"src/gui/windows", {public = true})
if is_os("windows") then
add_files("src/gui/tray/*.cpp")
add_includedirs("src/gui/tray", {public = true})
elseif is_os("macosx") then
add_files("src/gui/windows/*.mm")
end
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true})

View File

@@ -1,50 +0,0 @@
function setup_options_and_dependencies()
option("CROSSDESK_VERSION")
set_default("0.0.0")
set_showmenu(true)
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()
option("USE_WAYLAND")
set_default(false)
set_showmenu(true)
set_description("Enable Wayland capture on Linux (assumes dependencies are installed)")
option_end()
option("USE_DRM")
set_default(false)
set_showmenu(true)
set_description("Enable DRM capture on Linux (assumes dependencies are installed)")
option_end()
add_rules("mode.release", "mode.debug")
set_languages("c++17")
set_encodings("utf-8")
-- set_policy("build.warning", true)
-- set_warnings("all", "extra")
-- add_cxxflags("/W4", "/WX")
add_defines("UNICODE")
add_defines("USE_CUDA=" .. (is_config("USE_CUDA", true) and "1" or "0"))
add_defines("USE_WAYLAND=" .. (is_config("USE_WAYLAND", true) and "1" or "0"))
add_defines("USE_DRM=" .. (is_config("USE_DRM", true) and "1" or "0"))
if is_mode("debug") then
add_defines("CROSSDESK_DEBUG")
end
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
add_requires("spdlog 1.14.1", {system = false})
add_requires("imgui v1.92.1-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}})
add_requires("tinyfiledialogs 3.15.1")
end

View File

@@ -1,81 +0,0 @@
local function add_existing_include_dirs(paths, opts)
for _, dir in ipairs(paths) do
if os.isdir(dir) then
add_includedirs(dir, opts)
end
end
end
local function collect_dbus_arch_include_dirs()
local include_dirs = {}
for _, pattern in ipairs({
"/usr/lib/*/dbus-1.0/include",
"/usr/lib64/dbus-1.0/include",
"/usr/lib/dbus-1.0/include",
"/lib/*/dbus-1.0/include",
"/lib64/dbus-1.0/include",
"/lib/dbus-1.0/include"
}) do
for _, include_dir in ipairs(os.dirs(pattern)) do
table.insert(include_dirs, include_dir)
end
end
return include_dirs
end
function setup_platform_settings()
if is_os("windows") then
add_requires("libyuv", "miniaudio 0.11.21")
add_links("Shell32", "dwmapi", "User32", "kernel32",
"SDL3-static", "gdi32", "winmm", "setupapi", "version",
"Imm32", "iphlpapi", "d3d11", "dxgi")
add_cxflags("/WX")
set_runtimes("MT")
elseif is_os("linux") then
add_links("pulse-simple", "pulse")
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
if is_config("USE_DRM", true) then
add_links("drm")
add_defines("CROSSDESK_HAS_DRM=1")
add_existing_include_dirs({
"/usr/include/libdrm",
"/usr/local/include/libdrm"
}, {system = true})
else
add_defines("CROSSDESK_HAS_DRM=0")
end
if is_config("USE_WAYLAND", true) then
add_links("dbus-1", "pipewire-0.3")
add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=1")
add_existing_include_dirs({
"/usr/include/dbus-1.0",
"/usr/local/include/dbus-1.0",
"/usr/include/pipewire-0.3",
"/usr/local/include/pipewire-0.3",
"/usr/include/pipewire",
"/usr/local/include/pipewire",
"/usr/include/spa-0.2",
"/usr/local/include/spa-0.2",
"/usr/include/spa",
"/usr/local/include/spa"
}, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true})
end
else
add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=0")
end
add_cxflags("-Wno-unused-variable")
elseif is_os("macosx") then
add_links("SDL3")
add_ldflags("-Wl,-ld_classic")
add_cxflags("-Wno-unused-variable")
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation",
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")
end
end

View File

@@ -1,177 +0,0 @@
function setup_targets()
add_packages("spdlog", "imgui", "nlohmann_json")
includes("submodules", "thirdparty")
target("rd_log")
set_kind("object")
add_packages("spdlog")
add_files("src/log/rd_log.cpp")
add_includedirs("src/log", {public = true})
target("common")
set_kind("object")
add_deps("rd_log")
add_files("src/common/*.cpp")
if is_os("macosx") then
add_files("src/common/*.mm")
end
add_includedirs("src/common", {public = true})
target("path_manager")
set_kind("object")
add_deps("rd_log")
add_includedirs("src/path_manager", {public = true})
add_files("src/path_manager/*.cpp")
add_includedirs("src/path_manager", {public = true})
target("screen_capturer")
set_kind("object")
add_deps("rd_log", "common")
add_includedirs("src/screen_capturer", {public = true})
if is_os("windows") then
add_packages("libyuv")
add_files("src/screen_capturer/windows/screen_capturer_dxgi.cpp",
"src/screen_capturer/windows/screen_capturer_gdi.cpp",
"src/screen_capturer/windows/screen_capturer_win.cpp")
add_includedirs("src/screen_capturer/windows", {public = true})
elseif is_os("macosx") then
add_files("src/screen_capturer/macosx/*.cpp",
"src/screen_capturer/macosx/*.mm")
add_includedirs("src/screen_capturer/macosx", {public = true})
elseif is_os("linux") then
add_packages("libyuv")
add_files("src/screen_capturer/linux/screen_capturer_linux.cpp")
add_files("src/screen_capturer/linux/screen_capturer_x11.cpp")
add_files("src/screen_capturer/linux/screen_capturer_drm.cpp")
if is_config("USE_WAYLAND", true) then
add_files("src/screen_capturer/linux/screen_capturer_wayland.cpp")
add_files("src/screen_capturer/linux/screen_capturer_wayland_portal.cpp")
add_files("src/screen_capturer/linux/screen_capturer_wayland_pipewire.cpp")
end
add_includedirs("src/screen_capturer/linux", {public = true})
end
target("speaker_capturer")
set_kind("object")
add_deps("rd_log")
add_includedirs("src/speaker_capturer", {public = true})
if is_os("windows") then
add_packages("miniaudio")
add_files("src/speaker_capturer/windows/*.cpp")
add_includedirs("src/speaker_capturer/windows", {public = true})
elseif is_os("macosx") then
add_files("src/speaker_capturer/macosx/*.cpp",
"src/speaker_capturer/macosx/*.mm")
add_includedirs("src/speaker_capturer/macosx", {public = true})
elseif is_os("linux") then
add_files("src/speaker_capturer/linux/*.cpp")
add_includedirs("src/speaker_capturer/linux", {public = true})
end
target("device_controller")
set_kind("object")
add_deps("rd_log", "common")
add_includedirs("src/device_controller", {public = true})
if is_os("windows") then
add_files("src/device_controller/mouse/windows/*.cpp",
"src/device_controller/keyboard/windows/*.cpp")
add_includedirs("src/device_controller/mouse/windows",
"src/device_controller/keyboard/windows", {public = true})
elseif is_os("macosx") then
add_files("src/device_controller/mouse/mac/*.cpp",
"src/device_controller/keyboard/mac/*.cpp")
add_includedirs("src/device_controller/mouse/mac",
"src/device_controller/keyboard/mac", {public = true})
elseif is_os("linux") then
add_files("src/device_controller/mouse/linux/*.cpp",
"src/device_controller/keyboard/linux/*.cpp")
add_includedirs("src/device_controller/mouse/linux",
"src/device_controller/keyboard/linux", {public = true})
end
target("thumbnail")
set_kind("object")
add_packages("libyuv", "openssl3")
add_deps("rd_log", "common")
add_files("src/thumbnail/*.cpp")
add_includedirs("src/thumbnail", {public = true})
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})
target("assets")
set_kind("headeronly")
add_includedirs("src/gui/assets/localization",
"src/gui/assets/fonts",
"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("tools")
set_kind("object")
add_deps("rd_log")
add_files("src/tools/*.cpp")
if is_os("macosx") then
add_files("src/tools/*.mm")
end
add_includedirs("src/tools", {public = true})
target("gui")
set_kind("object")
add_packages("libyuv", "tinyfiledialogs")
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
"path_manager", "screen_capturer", "speaker_capturer",
"device_controller", "thumbnail", "version_checker", "tools")
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",
"src/gui/windows", {public = true})
if is_os("windows") then
add_files("src/gui/tray/*.cpp")
add_includedirs("src/gui/tray", {public = true})
elseif is_os("macosx") then
add_files("src/gui/windows/*.mm")
end
if is_os("windows") then
target("wgc_plugin")
set_kind("shared")
add_packages("libyuv")
add_deps("rd_log")
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
add_links("windowsapp")
add_files("src/screen_capturer/windows/screen_capturer_wgc.cpp",
"src/screen_capturer/windows/wgc_session_impl.cpp",
"src/screen_capturer/windows/wgc_plugin_entry.cpp")
add_includedirs("src/common", "src/screen_capturer",
"src/screen_capturer/windows")
end
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true})
if is_os("windows") then
add_deps("wgc_plugin")
add_files("scripts/windows/crossdesk.rc")
end
end