mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-06-10 01:14:53 +08:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4895ac9c23 | |||
| f121aa47f7 | |||
| 00a8d59284 | |||
| a30489e05b | |||
| dfbeb3ed20 | |||
| 2eed1c974e | |||
| 63a79a90ac | |||
| a61e74d97b | |||
| 54438a4aa1 | |||
| 7682ad63e4 | |||
| 06c53fdc9c | |||
| 665f4e684c | |||
| 52b894fe0e | |||
| 82c0cbbad4 | |||
| ce004af379 | |||
| 15bd9e9fdc | |||
| 37aabeaf72 | |||
| 473737ac9b | |||
| 1e29ec708f | |||
| 515d517a99 | |||
| a3aedcb624 | |||
| 98b7c6c966 | |||
| b1d956af2c | |||
| b7a031bb7f | |||
| 15cce07b6e | |||
| 1d5d6f5121 | |||
| 5f541f5c8b | |||
| 71bce08549 | |||
| 37b9badb2a | |||
| 4089e80fe8 | |||
| 2be6e727ce | |||
| d3b886c3f6 | |||
| 97e48bfe71 | |||
| a8769dee06 | |||
| ffa94986d5 | |||
| e4dfb61509 | |||
| d42b6e3261 | |||
| 855b15025c | |||
| 3701b2c0d9 | |||
| ecbec4d301 | |||
| 21425c7132 | |||
| 3e95a7ba29 | |||
| c1394db285 | |||
| eee6c588bd | |||
| eca68f6c7a | |||
| f4e28d8774 | |||
| 21b179e01c | |||
| 83cacf6f51 | |||
| 13c37f01b1 | |||
| 511831ced3 | |||
| 518e1afa58 | |||
| 43d03ac081 | |||
| f7f62c5fe0 | |||
| 2bbddbca6b | |||
| f0f8f27f4c | |||
| 262af263f2 | |||
| 38b7775b1b | |||
| 56c0bca62f | |||
| 4b1b09fd5b | |||
| 1d6425bbf4 | |||
| 5ec6552d25 | |||
| 79e4a0790a | |||
| 1d3cac54ab | |||
| 2f26334775 | |||
| 9270d528e3 | |||
| 91db3a7e34 | |||
| d017561e54 | |||
| 8e8a85bae3 | |||
| bea89e9111 | |||
| 499ce0190a | |||
| 91bde91238 | |||
| 3e31ba102d | |||
| 263c5eefd3 |
+44
-14
@@ -58,7 +58,7 @@ jobs:
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
${{ matrix.package_script }} ${LEGAL_VERSION}
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
|
||||
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
|
||||
@@ -112,10 +112,10 @@ jobs:
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.xmake/packages
|
||||
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
|
||||
key: "${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
run: brew install xmake
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
${{ matrix.package_script }} ${VERSION_NUM}
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
|
||||
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
|
||||
@@ -169,10 +169,10 @@ jobs:
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: D:\xmake_global\.xmake\packages
|
||||
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
|
||||
key: "${{ runner.os }}-xmake-deps-intel-${{ github.run_id }}"
|
||||
restore-keys: |
|
||||
${{ runner.os }}-xmake-deps-intel-
|
||||
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
Copy-Item $source $target -Force
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
@@ -242,22 +242,52 @@ jobs:
|
||||
cd "${{ github.workspace }}\scripts\windows"
|
||||
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package Portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
|
||||
$portableDir = "${{ github.workspace }}\portable"
|
||||
New-Item -ItemType Directory -Force -Path $portableDir
|
||||
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
|
||||
|
||||
$portableFiles = @(
|
||||
@("crossdesk.exe", "CrossDesk.exe"),
|
||||
@("crossdesk_service.exe", "crossdesk_service.exe"),
|
||||
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
|
||||
)
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$source = Join-Path $buildDir $file[0]
|
||||
$destination = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $source)) {
|
||||
throw "Missing portable package file: $source"
|
||||
}
|
||||
Copy-Item $source $destination -Force
|
||||
}
|
||||
|
||||
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$packagedFile = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $packagedFile)) {
|
||||
throw "Portable package is missing: $packagedFile"
|
||||
}
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
|
||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
|
||||
- name: Upload portable artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
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
|
||||
@@ -271,10 +301,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
|
||||
@@ -53,6 +53,29 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows 服务(CrossDesk Service)
|
||||
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||
|
||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||
|
||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||
```
|
||||
# 安装
|
||||
.\CrossDesk.exe --service-install
|
||||
# 启动
|
||||
.\CrossDesk.exe --service-start
|
||||
# 查看状态
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-stop
|
||||
# 卸载
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||
|
||||
## 如何编译
|
||||
|
||||
依赖:
|
||||
@@ -181,7 +204,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.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
上述命令中,用户需注意的参数如下:
|
||||
@@ -208,7 +231,7 @@ 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.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**注意**:
|
||||
@@ -262,3 +285,8 @@ sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keyc
|
||||
|
||||
# 常见问题
|
||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
|
||||
|
||||
# 致谢
|
||||
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
||||
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
||||
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
||||
|
||||
+32
-2
@@ -56,6 +56,31 @@ Enter the **Remote Device ID** and **Password**, then click Connect to access th
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows Service (CrossDesk Service)
|
||||
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
||||
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
||||
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
||||
|
||||
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
||||
|
||||
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
||||
```
|
||||
# install
|
||||
.\CrossDesk.exe --service-install
|
||||
# start
|
||||
.\CrossDesk.exe --service-start
|
||||
# check status
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# stop
|
||||
.\CrossDesk.exe --service-stop
|
||||
# uninstall
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
||||
|
||||
## How to build
|
||||
|
||||
Requirements:
|
||||
@@ -189,7 +214,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.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
The parameters you need to pay attention to are as follows:
|
||||
@@ -216,7 +241,7 @@ 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.3
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**Notes**
|
||||
@@ -274,3 +299,8 @@ See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
# FAQ
|
||||
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
|
||||
|
||||
# Acknowledgements
|
||||
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
|
||||
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
|
||||
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
|
||||
|
||||
@@ -8,6 +8,8 @@ APP_VERSION="$1"
|
||||
ARCHITECTURE="amd64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
@@ -41,9 +43,9 @@ Maintainer: $MAINTAINER
|
||||
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
|
||||
Recommends: nvidia-cuda-toolkit
|
||||
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
Recommends: $PORTAL_RUNTIME_RECOMMENDS, nvidia-cuda-toolkit
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
@@ -93,4 +95,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
|
||||
|
||||
rm -rf "$DEB_DIR"
|
||||
|
||||
echo "✅ Deb package created: $OUTPUT_FILE"
|
||||
echo "✅ Deb package created: $OUTPUT_FILE"
|
||||
|
||||
@@ -8,6 +8,8 @@ APP_VERSION="$1"
|
||||
ARCHITECTURE="arm64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
@@ -41,8 +43,9 @@ Maintainer: $MAINTAINER
|
||||
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
|
||||
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
Recommends: $PORTAL_RUNTIME_RECOMMENDS
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
@@ -92,4 +95,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
|
||||
|
||||
rm -rf "$DEB_DIR"
|
||||
|
||||
echo "✅ Deb package created: $OUTPUT_FILE"
|
||||
echo "✅ Deb package created: $OUTPUT_FILE"
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<!-- 应用程序标识 -->
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<!-- 描述信息 -->
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<!-- 权限:要求管理员运行 -->
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
@@ -20,24 +16,17 @@
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- DPI 感知设置:支持高分屏 -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- Windows Vista/7 风格 DPI 感知 -->
|
||||
<dpiAware>true/pm</dpiAware>
|
||||
<!-- Windows 10/11 高级 DPI 感知 -->
|
||||
<dpiAwareness>PerMonitorV2</dpiAwareness>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Windows 兼容性声明 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- 支持 Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- 支持 Windows 11(向下兼容 Win10 GUID) -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
</assembly>
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
// Application icon (IDI_ICON1 = 1, which is the default app icon resource ID)
|
||||
// Application icon resource; load by the resource name IDI_ICON1.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -8,6 +8,7 @@
|
||||
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
||||
!define APP_NAME "CrossDesk"
|
||||
!define UNINSTALL_REG_KEY "CrossDesk"
|
||||
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
||||
|
||||
; Installer icon path
|
||||
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
|
||||
@@ -68,11 +69,20 @@ cancelInstall:
|
||||
Abort
|
||||
|
||||
installApp:
|
||||
Call StopInstalledService
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
SetOverwrite ifnewer
|
||||
|
||||
; Main application executable path
|
||||
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
|
||||
; Bundle service-side binaries required by the Windows service flow
|
||||
File "..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||
File "..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||
; Bundle runtime DLLs from the release output directory
|
||||
File "..\..\build\windows\x64\release\*.dll"
|
||||
|
||||
Call RegisterInstalledService
|
||||
|
||||
; Write uninstall information
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
@@ -120,8 +130,12 @@ cancelUninstall:
|
||||
Abort
|
||||
|
||||
uninstallApp:
|
||||
Call un.UnregisterInstalledService
|
||||
|
||||
; Delete main executable and uninstaller
|
||||
Delete "$INSTDIR\CrossDesk.exe"
|
||||
Delete "$INSTDIR\crossdesk_service.exe"
|
||||
Delete "$INSTDIR\crossdesk_session_helper.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Recursively delete installation directory
|
||||
@@ -146,3 +160,78 @@ SectionEnd
|
||||
Function LaunchApp
|
||||
Exec "$INSTDIR\CrossDesk.exe"
|
||||
FunctionEnd
|
||||
|
||||
Function StopInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
|
||||
|
||||
DetailPrint "Stopping existing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
stop_with_sc:
|
||||
DetailPrint "Stopping existing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
FunctionEnd
|
||||
|
||||
Function RegisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary
|
||||
|
||||
DetailPrint "Registering CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0
|
||||
${If} $0 != 0
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
DetailPrint "CrossDesk service registered for on-demand start"
|
||||
|
||||
Return
|
||||
|
||||
missing_service_binary:
|
||||
MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted."
|
||||
Abort
|
||||
FunctionEnd
|
||||
|
||||
Function un.UnregisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
|
||||
|
||||
DetailPrint "Stopping CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
DetailPrint "Removing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
unregister_with_sc:
|
||||
DetailPrint "Removing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
|
||||
ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
+76
-51
@@ -34,9 +34,16 @@
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
Daemon* Daemon::instance_ = nullptr;
|
||||
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
constexpr int kRestartDelayMs = 1000;
|
||||
#ifndef _WIN32
|
||||
constexpr int kWaitPollIntervalMs = 200;
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
// get executable file path
|
||||
static std::string GetExecutablePath() {
|
||||
#ifdef _WIN32
|
||||
@@ -66,33 +73,35 @@ static std::string GetExecutablePath() {
|
||||
return "";
|
||||
}
|
||||
|
||||
Daemon::Daemon(const std::string& name)
|
||||
: name_(name)
|
||||
#ifdef _WIN32
|
||||
,
|
||||
running_(false)
|
||||
#else
|
||||
,
|
||||
running_(true)
|
||||
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
|
||||
|
||||
void Daemon::stop() {
|
||||
running_.store(false);
|
||||
#ifndef _WIN32
|
||||
stop_requested_ = 1;
|
||||
#endif
|
||||
{
|
||||
}
|
||||
|
||||
void Daemon::stop() { running_ = false; }
|
||||
|
||||
bool Daemon::isRunning() const { return running_; }
|
||||
bool Daemon::isRunning() const {
|
||||
#ifndef _WIN32
|
||||
return running_.load() && (stop_requested_ == 0);
|
||||
#else
|
||||
return running_.load();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Daemon::start(MainLoopFunc loop) {
|
||||
#ifdef _WIN32
|
||||
running_ = true;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#elif __APPLE__
|
||||
// macOS: Use child process monitoring (like Windows) to preserve GUI
|
||||
running_ = true;
|
||||
stop_requested_ = 0;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#else
|
||||
// linux: Daemonize first, then run with restart monitoring
|
||||
instance_ = this;
|
||||
stop_requested_ = 0;
|
||||
|
||||
// check if running from terminal before fork
|
||||
bool from_terminal =
|
||||
@@ -134,29 +143,13 @@ bool Daemon::start(MainLoopFunc loop) {
|
||||
}
|
||||
|
||||
// set up signal handlers
|
||||
signal(SIGTERM, [](int) {
|
||||
if (instance_) instance_->stop();
|
||||
});
|
||||
signal(SIGINT, [](int) {
|
||||
if (instance_) instance_->stop();
|
||||
});
|
||||
signal(SIGTERM, [](int) { stop_requested_ = 1; });
|
||||
signal(SIGINT, [](int) { stop_requested_ = 1; });
|
||||
|
||||
// ignore SIGPIPE
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
// set up SIGCHLD handler to reap zombie processes
|
||||
struct sigaction sa_chld;
|
||||
sa_chld.sa_handler = [](int) {
|
||||
// reap zombie processes
|
||||
while (waitpid(-1, nullptr, WNOHANG) > 0) {
|
||||
// continue until no more zombie children
|
||||
}
|
||||
};
|
||||
sigemptyset(&sa_chld.sa_mask);
|
||||
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
|
||||
sigaction(SIGCHLD, &sa_chld, nullptr);
|
||||
|
||||
running_ = true;
|
||||
running_.store(true);
|
||||
return runWithRestart(loop);
|
||||
#endif
|
||||
}
|
||||
@@ -204,8 +197,7 @@ 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(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -237,27 +229,41 @@ 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(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
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 (exit_code == 0) {
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
std::cerr << "Child process exited with code " << exit_code
|
||||
<< ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
#else
|
||||
// linux: use fork + exec to create child process
|
||||
pid_t pid = fork();
|
||||
@@ -266,21 +272,39 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
_exit(1); // exec failed
|
||||
} else if (pid > 0) {
|
||||
int status = 0;
|
||||
pid_t waited_pid = waitpid(pid, &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);
|
||||
}
|
||||
|
||||
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(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
int exit_code = WEXITSTATUS(status);
|
||||
if (exit_code == 0) {
|
||||
if (!isRunning() || exit_code == 0) {
|
||||
break; // normal exit
|
||||
}
|
||||
restart_count++;
|
||||
@@ -288,6 +312,9 @@ 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 << ")"
|
||||
@@ -298,12 +325,10 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
"(attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
} else {
|
||||
std::cerr << "Failed to fork child process" << std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
restart_count++;
|
||||
}
|
||||
#endif
|
||||
|
||||
+6
-8
@@ -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,12 +28,10 @@ class Daemon {
|
||||
std::string name_;
|
||||
bool runWithRestart(MainLoopFunc loop);
|
||||
|
||||
#ifdef _WIN32
|
||||
bool running_;
|
||||
#else
|
||||
static Daemon* instance_;
|
||||
volatile bool running_;
|
||||
#ifndef _WIN32
|
||||
static volatile std::sig_atomic_t stop_requested_;
|
||||
#endif
|
||||
std::atomic<bool> running_;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -7,15 +7,179 @@
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
#include "config_center.h"
|
||||
#include "daemon.h"
|
||||
#include "path_manager.h"
|
||||
#include "render.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
|
||||
void EnsureConsoleForCli() {
|
||||
static bool console_ready = false;
|
||||
if (console_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
DWORD error = GetLastError();
|
||||
if (error != ERROR_ACCESS_DENIED) {
|
||||
AllocConsole();
|
||||
}
|
||||
}
|
||||
|
||||
FILE* stream = nullptr;
|
||||
freopen_s(&stream, "CONOUT$", "w", stdout);
|
||||
freopen_s(&stream, "CONOUT$", "w", stderr);
|
||||
freopen_s(&stream, "CONIN$", "r", stdin);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
console_ready = true;
|
||||
}
|
||||
|
||||
void PrintServiceCliUsage() {
|
||||
std::cout
|
||||
<< "CrossDesk service management commands\n"
|
||||
<< " --service-install Install the sibling crossdesk_service.exe\n"
|
||||
<< " --service-uninstall Remove the installed Windows service\n"
|
||||
<< " --service-start Start the Windows service\n"
|
||||
<< " --service-stop Stop the Windows service\n"
|
||||
<< " --service-sas Ask the service to send Secure Attention "
|
||||
"Sequence\n"
|
||||
<< " --service-ping Ping the service over named pipe IPC\n"
|
||||
<< " --service-status Query service runtime status\n"
|
||||
<< " --service-help Show this help\n";
|
||||
}
|
||||
|
||||
std::wstring GetCurrentExecutablePathW() {
|
||||
wchar_t path[MAX_PATH] = {0};
|
||||
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH) {
|
||||
return L"";
|
||||
}
|
||||
return std::wstring(path, length);
|
||||
}
|
||||
|
||||
std::filesystem::path GetSiblingServiceExecutablePath() {
|
||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||
if (current_executable.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::filesystem::path(current_executable).parent_path() /
|
||||
L"crossdesk_service.exe";
|
||||
}
|
||||
|
||||
bool IsServiceCliCommand(const char* arg) {
|
||||
if (arg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::strcmp(arg, "--service-install") == 0 ||
|
||||
std::strcmp(arg, "--service-uninstall") == 0 ||
|
||||
std::strcmp(arg, "--service-start") == 0 ||
|
||||
std::strcmp(arg, "--service-stop") == 0 ||
|
||||
std::strcmp(arg, "--service-sas") == 0 ||
|
||||
std::strcmp(arg, "--service-ping") == 0 ||
|
||||
std::strcmp(arg, "--service-status") == 0 ||
|
||||
std::strcmp(arg, "--service-help") == 0;
|
||||
}
|
||||
|
||||
void TryStartManagedWindowsService() {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty() || !std::filesystem::exists(service_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!crossdesk::IsCrossDeskServiceInstalled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
crossdesk::StartCrossDeskService();
|
||||
}
|
||||
|
||||
int HandleServiceCliCommand(const std::string& command) {
|
||||
EnsureConsoleForCli();
|
||||
|
||||
if (command == "--service-help") {
|
||||
PrintServiceCliUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-install") {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty()) {
|
||||
std::cerr << "Failed to locate crossdesk_service.exe" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (!std::filesystem::exists(service_path)) {
|
||||
std::cerr << "Service binary not found: " << service_path.string()
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool success = crossdesk::InstallCrossDeskService(service_path.wstring());
|
||||
std::cout << (success ? "install ok" : "install failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-uninstall") {
|
||||
bool success = crossdesk::UninstallCrossDeskService();
|
||||
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-start") {
|
||||
bool success = crossdesk::StartCrossDeskService();
|
||||
std::cout << (success ? "start ok" : "start failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-stop") {
|
||||
bool success = crossdesk::StopCrossDeskService();
|
||||
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-sas") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-ping") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-status") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrintServiceCliUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
#endif
|
||||
|
||||
// check if running as child process
|
||||
bool is_child = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
@@ -32,6 +196,10 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
TryStartManagedWindowsService();
|
||||
#endif
|
||||
|
||||
bool enable_daemon = false;
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
if (path_manager) {
|
||||
|
||||
+26
-2
@@ -1,5 +1,8 @@
|
||||
#include "platform.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -108,7 +111,7 @@ std::string GetHostName() {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsaData;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
|
||||
std::cerr << "WSAStartup failed." << std::endl;
|
||||
LOG_ERROR("WSAStartup failed");
|
||||
return "";
|
||||
}
|
||||
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
|
||||
@@ -125,4 +128,25 @@ std::string GetHostName() {
|
||||
#endif
|
||||
return hostname;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
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
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace crossdesk {
|
||||
|
||||
std::string GetMac();
|
||||
std::string GetHostName();
|
||||
bool IsWaylandSession();
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
#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
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -20,8 +20,14 @@ int ConfigCenter::Load() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
language_ = static_cast<LANGUAGE>(
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_)));
|
||||
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);
|
||||
}
|
||||
|
||||
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
|
||||
section_, "video_quality", static_cast<long>(video_quality_)));
|
||||
@@ -385,4 +391,4 @@ int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
std::string ConfigCenter::GetFileTransferSavePath() const {
|
||||
return file_transfer_save_path_;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace crossdesk {
|
||||
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
@@ -90,4 +90,4 @@ class ConfigCenter {
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
@@ -23,6 +25,8 @@ typedef enum {
|
||||
audio_capture,
|
||||
host_infomation,
|
||||
display_id,
|
||||
service_status,
|
||||
service_command,
|
||||
} ControlType;
|
||||
typedef enum {
|
||||
move = 0,
|
||||
@@ -36,6 +40,7 @@ typedef enum {
|
||||
wheel_horizontal
|
||||
} MouseFlag;
|
||||
typedef enum { key_down = 0, key_up } KeyFlag;
|
||||
typedef enum { send_sas = 0, lock_workstation } ServiceCommandFlag;
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
@@ -45,6 +50,8 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
KeyFlag flag;
|
||||
} Key;
|
||||
|
||||
@@ -59,6 +66,15 @@ typedef struct {
|
||||
int* bottom;
|
||||
} HostInfo;
|
||||
|
||||
typedef struct {
|
||||
bool available;
|
||||
char interactive_stage[32];
|
||||
} ServiceStatus;
|
||||
|
||||
typedef struct {
|
||||
ServiceCommandFlag flag;
|
||||
} ServiceCommand;
|
||||
|
||||
struct RemoteAction {
|
||||
ControlType type;
|
||||
union {
|
||||
@@ -67,6 +83,8 @@ struct RemoteAction {
|
||||
HostInfo i;
|
||||
bool a;
|
||||
int d;
|
||||
ServiceStatus ss;
|
||||
ServiceCommand c;
|
||||
};
|
||||
|
||||
// parse
|
||||
@@ -88,7 +106,10 @@ struct RemoteAction {
|
||||
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
j["keyboard"] = {{"key_value", a.k.key_value}, {"flag", a.k.flag}};
|
||||
j["keyboard"] = {{"key_value", a.k.key_value},
|
||||
{"scan_code", a.k.scan_code},
|
||||
{"extended", a.k.extended},
|
||||
{"flag", a.k.flag}};
|
||||
break;
|
||||
case ControlType::audio_capture:
|
||||
j["audio_capture"] = a.a;
|
||||
@@ -96,6 +117,13 @@ struct RemoteAction {
|
||||
case ControlType::display_id:
|
||||
j["display_id"] = a.d;
|
||||
break;
|
||||
case ControlType::service_status:
|
||||
j["service_status"] = {{"available", a.ss.available},
|
||||
{"interactive_stage", a.ss.interactive_stage}};
|
||||
break;
|
||||
case ControlType::service_command:
|
||||
j["service_command"] = {{"flag", a.c.flag}};
|
||||
break;
|
||||
case ControlType::host_infomation: {
|
||||
json displays = json::array();
|
||||
for (size_t idx = 0; idx < a.i.display_num; idx++) {
|
||||
@@ -129,6 +157,9 @@ struct RemoteAction {
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
|
||||
out.k.scan_code =
|
||||
j.at("keyboard").value("scan_code", static_cast<uint32_t>(0));
|
||||
out.k.extended = j.at("keyboard").value("extended", false);
|
||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::audio_capture:
|
||||
@@ -137,6 +168,20 @@ struct RemoteAction {
|
||||
case ControlType::display_id:
|
||||
out.d = j.at("display_id").get<int>();
|
||||
break;
|
||||
case ControlType::service_status: {
|
||||
const auto& service_status_json = j.at("service_status");
|
||||
out.ss.available = service_status_json.value("available", false);
|
||||
std::string interactive_stage =
|
||||
service_status_json.value("interactive_stage", std::string());
|
||||
std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(),
|
||||
sizeof(out.ss.interactive_stage) - 1);
|
||||
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = '\0';
|
||||
break;
|
||||
}
|
||||
case ControlType::service_command:
|
||||
out.c.flag = static_cast<ServiceCommandFlag>(
|
||||
j.at("service_command").at("flag").get<int>());
|
||||
break;
|
||||
case ControlType::host_infomation: {
|
||||
std::string host_name =
|
||||
j.at("host_info").at("host_name").get<std::string>();
|
||||
@@ -174,8 +219,8 @@ struct RemoteAction {
|
||||
}
|
||||
};
|
||||
|
||||
// int key_code, bool is_down
|
||||
typedef void (*OnKeyAction)(int, bool, void*);
|
||||
// int key_code, bool is_down, uint32_t scan_code, bool extended
|
||||
typedef void (*OnKeyAction)(int, bool, uint32_t, bool, void*);
|
||||
|
||||
class DeviceController {
|
||||
public:
|
||||
@@ -190,4 +235,4 @@ class DeviceController {
|
||||
// virtual int Unhook();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,27 +1,60 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static KeySym NormalizeKeySym(KeySym key_sym) {
|
||||
if (key_sym >= XK_a && key_sym <= XK_z) {
|
||||
return key_sym - XK_a + XK_A;
|
||||
}
|
||||
return key_sym;
|
||||
}
|
||||
|
||||
static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
(void)display;
|
||||
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
|
||||
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0);
|
||||
int key_code = XKeysymToKeycode(display, keySym);
|
||||
KeySym key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 0));
|
||||
auto key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 1));
|
||||
key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
|
||||
}
|
||||
|
||||
if (key_it == x11KeySymToVkCode.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int key_code = key_it->second;
|
||||
bool is_key_down = (event->xkey.type == KeyPress);
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
|
||||
if (g_on_key_action) {
|
||||
g_on_key_action(key_code, is_key_down, g_user_ptr);
|
||||
g_on_key_action(key_code, is_key_down, scan_code, extended, g_user_ptr);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
|
||||
KeyboardCapturer::KeyboardCapturer()
|
||||
: display_(nullptr),
|
||||
root_(0),
|
||||
running_(false),
|
||||
use_wayland_portal_(false),
|
||||
wayland_init_attempted_(false),
|
||||
dbus_connection_(nullptr) {
|
||||
XInitThreads();
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
@@ -29,42 +62,116 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
|
||||
}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {
|
||||
Unhook();
|
||||
CleanupWaylandPortal();
|
||||
|
||||
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;
|
||||
|
||||
XSelectInput(display_, DefaultRootWindow(display_),
|
||||
KeyPressMask | KeyReleaseMask);
|
||||
|
||||
while (running_) {
|
||||
XEvent event;
|
||||
XNextEvent(display_, &event);
|
||||
KeyboardEventHandler(display_, &event);
|
||||
if (running_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
running_ = false;
|
||||
|
||||
if (display_) {
|
||||
XSelectInput(display_, DefaultRootWindow(display_), 0);
|
||||
if (display_ && root_ != 0) {
|
||||
XSelectInput(display_, root_, 0);
|
||||
XFlush(display_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (IsWaylandSession()) {
|
||||
if (!use_wayland_portal_ && !wayland_init_attempted_) {
|
||||
wayland_init_attempted_ = true;
|
||||
if (InitWaylandPortal()) {
|
||||
use_wayland_portal_ = true;
|
||||
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Wayland keyboard control init failed, falling back to X11/XTest "
|
||||
"backend");
|
||||
}
|
||||
}
|
||||
|
||||
if (use_wayland_portal_) {
|
||||
return SendWaylandKeyboardCommand(key_code, is_down, scan_code, extended);
|
||||
}
|
||||
}
|
||||
|
||||
if (!display_) {
|
||||
LOG_ERROR("Display not initialized.");
|
||||
return -1;
|
||||
@@ -78,4 +185,4 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -11,8 +11,17 @@
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class KeyboardCapturer : public DeviceController {
|
||||
@@ -23,12 +32,30 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
bool InitWaylandPortal();
|
||||
void CleanupWaylandPortal();
|
||||
int SendWaylandKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
|
||||
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
|
||||
bool SendWaylandPortalVoidCall(const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>&
|
||||
append_args);
|
||||
|
||||
private:
|
||||
Display* display_;
|
||||
Window root_;
|
||||
bool running_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
bool use_wayland_portal_ = false;
|
||||
bool wayland_init_attempted_ = false;
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
std::string wayland_session_handle_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,717 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
#include <dbus/dbus.h>
|
||||
#endif
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern std::map<int, int> vkCodeToX11KeySym;
|
||||
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
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* kPortalRequestInterface =
|
||||
"org.freedesktop.portal.Request";
|
||||
constexpr const char* kPortalRequestPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/request/";
|
||||
constexpr const char* kPortalSessionPathPrefix =
|
||||
"/org/freedesktop/portal/desktop/session/";
|
||||
|
||||
constexpr uint32_t kRemoteDesktopDeviceKeyboard = 1u;
|
||||
constexpr uint32_t kKeyboardReleased = 0u;
|
||||
constexpr uint32_t kKeyboardPressed = 1u;
|
||||
|
||||
int NormalizeFallbackKeysym(int keysym) {
|
||||
if (keysym >= XK_A && keysym <= XK_Z) {
|
||||
return keysym - XK_A + XK_a;
|
||||
}
|
||||
return keysym;
|
||||
}
|
||||
|
||||
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 AppendEmptyOptionsDict(DBusMessageIter* iter) {
|
||||
DBusMessageIter options;
|
||||
dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &options);
|
||||
dbus_message_iter_close_container(iter, &options);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool ReadUint32Like(DBusMessageIter* iter, uint32_t* value) {
|
||||
if (!iter || !value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int type = dbus_message_iter_get_arg_type(iter);
|
||||
if (type == DBUS_TYPE_UINT32) {
|
||||
uint32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
*value = temp;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type == DBUS_TYPE_INT32) {
|
||||
int32_t temp = 0;
|
||||
dbus_message_iter_get_basic(iter, &temp);
|
||||
if (temp < 0) {
|
||||
return false;
|
||||
}
|
||||
*value = static_cast<uint32_t>(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) {
|
||||
(void)connection;
|
||||
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,
|
||||
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 (!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::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);
|
||||
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
|
||||
#endif
|
||||
|
||||
bool KeyboardCapturer::InitWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
CleanupWaylandPortal();
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
|
||||
DBusConnection* check_connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
|
||||
if (!check_connection) {
|
||||
LogDbusError("dbus_bus_get", &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbus_bool_t has_owner =
|
||||
dbus_bus_name_has_owner(check_connection, kPortalBusName, &error);
|
||||
if (dbus_error_is_set(&error)) {
|
||||
LogDbusError("dbus_bus_name_has_owner", &error);
|
||||
dbus_error_free(&error);
|
||||
dbus_connection_unref(check_connection);
|
||||
return false;
|
||||
}
|
||||
dbus_connection_unref(check_connection);
|
||||
|
||||
if (!has_owner) {
|
||||
LOG_ERROR("xdg-desktop-portal is not available on session bus");
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const std::string session_handle_token =
|
||||
MakeToken("crossdesk_keyboard_session");
|
||||
std::string request_path;
|
||||
const bool create_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_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.CreateSession denied, 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()) {
|
||||
wayland_session_handle_ = parsed_handle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
&request_path);
|
||||
|
||||
if (!create_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (wayland_session_handle_.empty()) {
|
||||
wayland_session_handle_ =
|
||||
BuildSessionHandleFromRequestPath(request_path, session_handle_token);
|
||||
}
|
||||
|
||||
if (wayland_session_handle_.empty()) {
|
||||
LOG_ERROR("RemoteDesktop.CreateSession did not return session handle");
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
const bool select_ok = 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", kRemoteDesktopDeviceKeyboard);
|
||||
AppendDictEntryString(&options, "handle_token",
|
||||
MakeToken("crossdesk_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[](uint32_t response_code, DBusMessageIter*) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.SelectDevices denied, response={}",
|
||||
response_code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!select_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* parent_window = "";
|
||||
bool keyboard_granted = false;
|
||||
const bool start_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_keyboard_req"));
|
||||
dbus_message_iter_close_container(&iter, &options);
|
||||
return true;
|
||||
},
|
||||
[&](uint32_t response_code, DBusMessageIter* results) {
|
||||
if (response_code != 0) {
|
||||
LOG_ERROR("RemoteDesktop.Start denied, 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) {
|
||||
ReadUint32Like(&variant, &granted_devices);
|
||||
}
|
||||
}
|
||||
}
|
||||
dbus_message_iter_next(&dict);
|
||||
}
|
||||
|
||||
keyboard_granted =
|
||||
(granted_devices & kRemoteDesktopDeviceKeyboard) != 0;
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR(
|
||||
"RemoteDesktop.Start granted devices mask={}, keyboard not allowed",
|
||||
granted_devices);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!start_ok) {
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!keyboard_granted) {
|
||||
LOG_ERROR("RemoteDesktop session started without keyboard permission");
|
||||
CleanupWaylandPortal();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KeyboardCapturer::CleanupWaylandPortal() {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (dbus_connection_) {
|
||||
CloseWaylandPortalSessionAndConnection(dbus_connection_,
|
||||
wayland_session_handle_,
|
||||
"RemoteDesktop.Session.Close");
|
||||
dbus_connection_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
use_wayland_portal_ = false;
|
||||
wayland_session_handle_.clear();
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it == vkCodeToX11KeySym.end()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
|
||||
const int keysym = key_it->second;
|
||||
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
if (x11_keycode > 8) {
|
||||
const int evdev_keycode = static_cast<int>(x11_keycode) - 8;
|
||||
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
|
||||
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Failed to send Wayland keyboard event, vk_code={}, is_down={}",
|
||||
key_code, is_down);
|
||||
return -3;
|
||||
#else
|
||||
(void)key_code;
|
||||
(void)is_down;
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::NotifyWaylandKeyboardKeysym(int keysym, uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeysym", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_sym = keysym;
|
||||
uint32_t key_state = state;
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
AppendEmptyOptionsDict(iter);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_sym);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keysym;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::NotifyWaylandKeyboardKeycode(int keycode,
|
||||
uint32_t state) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
return SendWaylandPortalVoidCall(
|
||||
"NotifyKeyboardKeycode", [&](DBusMessageIter* iter) {
|
||||
const char* session_handle = wayland_session_handle_.c_str();
|
||||
int32_t key_code = keycode;
|
||||
uint32_t key_state = state;
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
|
||||
&session_handle);
|
||||
AppendEmptyOptionsDict(iter);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_code);
|
||||
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
|
||||
});
|
||||
#else
|
||||
(void)keycode;
|
||||
(void)state;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::SendWaylandPortalVoidCall(
|
||||
const char* method_name,
|
||||
const std::function<void(DBusMessageIter*)>& append_args) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
if (!dbus_connection_ || !method_name || method_name[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessage* message = dbus_message_new_method_call(
|
||||
kPortalBusName, kPortalObjectPath, kPortalRemoteDesktopInterface,
|
||||
method_name);
|
||||
if (!message) {
|
||||
LOG_ERROR("Failed to allocate {} message", method_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
DBusMessageIter iter;
|
||||
dbus_message_iter_init_append(message, &iter);
|
||||
if (append_args) {
|
||||
append_args(&iter);
|
||||
}
|
||||
|
||||
DBusError error;
|
||||
dbus_error_init(&error);
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
|
||||
dbus_connection_, message, 5000, &error);
|
||||
dbus_message_unref(message);
|
||||
if (!reply) {
|
||||
LogDbusError(method_name, &error);
|
||||
dbus_error_free(&error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
|
||||
const char* error_name = dbus_message_get_error_name(reply);
|
||||
LOG_ERROR("{} returned DBus error: {}", method_name,
|
||||
error_name ? error_name : "unknown");
|
||||
dbus_message_unref(reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
dbus_message_unref(reply);
|
||||
return true;
|
||||
#else
|
||||
(void)method_name;
|
||||
(void)append_args;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -7,9 +9,100 @@ namespace crossdesk {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
|
||||
|
||||
static int VkCodeFromUnicode(UniChar ch) {
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
return static_cast<int>(ch - 'a' + 'A');
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
return static_cast<int>(ch);
|
||||
}
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
return static_cast<int>(ch);
|
||||
}
|
||||
|
||||
switch (ch) {
|
||||
case ' ':
|
||||
return 0x20; // VK_SPACE
|
||||
case '-':
|
||||
case '_':
|
||||
return 0xBD; // VK_OEM_MINUS
|
||||
case '=':
|
||||
case '+':
|
||||
return 0xBB; // VK_OEM_PLUS
|
||||
case '[':
|
||||
case '{':
|
||||
return 0xDB; // VK_OEM_4
|
||||
case ']':
|
||||
case '}':
|
||||
return 0xDD; // VK_OEM_6
|
||||
case '\\':
|
||||
case '|':
|
||||
return 0xDC; // VK_OEM_5
|
||||
case ';':
|
||||
case ':':
|
||||
return 0xBA; // VK_OEM_1
|
||||
case '\'':
|
||||
case '"':
|
||||
return 0xDE; // VK_OEM_7
|
||||
case ',':
|
||||
case '<':
|
||||
return 0xBC; // VK_OEM_COMMA
|
||||
case '.':
|
||||
case '>':
|
||||
return 0xBE; // VK_OEM_PERIOD
|
||||
case '/':
|
||||
case '?':
|
||||
return 0xBF; // VK_OEM_2
|
||||
case '`':
|
||||
case '~':
|
||||
return 0xC0; // VK_OEM_3
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
|
||||
bool is_key_down) {
|
||||
auto key_it = CGKeyCodeToVkCode.find(key_code);
|
||||
if (key_it != CGKeyCodeToVkCode.end()) {
|
||||
if (is_key_down) {
|
||||
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
|
||||
}
|
||||
return key_it->second;
|
||||
}
|
||||
|
||||
int vk_code = -1;
|
||||
UniChar chars[4] = {0};
|
||||
UniCharCount char_count = 0;
|
||||
CGEventKeyboardGetUnicodeString(event, 4, &char_count, chars);
|
||||
if (char_count > 0) {
|
||||
vk_code = VkCodeFromUnicode(chars[0]);
|
||||
}
|
||||
|
||||
if (vk_code < 0) {
|
||||
auto fallback_it =
|
||||
g_unmapped_keycode_to_vk.find(static_cast<int>(key_code));
|
||||
if (fallback_it != g_unmapped_keycode_to_vk.end()) {
|
||||
vk_code = fallback_it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (vk_code >= 0) {
|
||||
if (is_key_down) {
|
||||
g_unmapped_keycode_to_vk[static_cast<int>(key_code)] = vk_code;
|
||||
} else {
|
||||
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
|
||||
}
|
||||
}
|
||||
|
||||
return vk_code;
|
||||
}
|
||||
|
||||
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
CGEventRef event, void* userInfo) {
|
||||
(void)proxy;
|
||||
if (!g_on_key_action) {
|
||||
return event;
|
||||
}
|
||||
@@ -20,84 +113,79 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
return event;
|
||||
}
|
||||
|
||||
int vk_code = 0;
|
||||
|
||||
if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
|
||||
const bool is_key_down = (type == kCGEventKeyDown);
|
||||
CGKeyCode key_code = static_cast<CGKeyCode>(
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||
if (CGKeyCodeToVkCode.find(key_code) != CGKeyCodeToVkCode.end()) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], type == kCGEventKeyDown,
|
||||
g_user_ptr);
|
||||
int vk_code = ResolveVkCodeFromMacEvent(event, key_code, is_key_down);
|
||||
if (vk_code >= 0) {
|
||||
g_on_key_action(vk_code, is_key_down, 0, false, g_user_ptr);
|
||||
}
|
||||
} else if (type == kCGEventFlagsChanged) {
|
||||
CGEventFlags current_flags = CGEventGetFlags(event);
|
||||
CGKeyCode key_code = static_cast<CGKeyCode>(
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||
auto key_it = CGKeyCodeToVkCode.find(key_code);
|
||||
if (key_it == CGKeyCodeToVkCode.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
const int vk_code = key_it->second;
|
||||
|
||||
// caps lock
|
||||
bool caps_lock_state = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
|
||||
if (caps_lock_state != keyboard_capturer->caps_lock_flag_) {
|
||||
keyboard_capturer->caps_lock_flag_ = caps_lock_state;
|
||||
if (keyboard_capturer->caps_lock_flag_) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
|
||||
} else {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
|
||||
}
|
||||
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// shift
|
||||
bool shift_state = (current_flags & kCGEventFlagMaskShift) != 0;
|
||||
if (shift_state != keyboard_capturer->shift_flag_) {
|
||||
keyboard_capturer->shift_flag_ = shift_state;
|
||||
if (keyboard_capturer->shift_flag_) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
|
||||
} else {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
|
||||
}
|
||||
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// control
|
||||
bool control_state = (current_flags & kCGEventFlagMaskControl) != 0;
|
||||
if (control_state != keyboard_capturer->control_flag_) {
|
||||
keyboard_capturer->control_flag_ = control_state;
|
||||
if (keyboard_capturer->control_flag_) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
|
||||
} else {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
|
||||
}
|
||||
g_on_key_action(vk_code, keyboard_capturer->control_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// option
|
||||
bool option_state = (current_flags & kCGEventFlagMaskAlternate) != 0;
|
||||
if (option_state != keyboard_capturer->option_flag_) {
|
||||
keyboard_capturer->option_flag_ = option_state;
|
||||
if (keyboard_capturer->option_flag_) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
|
||||
} else {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
|
||||
}
|
||||
g_on_key_action(vk_code, keyboard_capturer->option_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// command
|
||||
bool command_state = (current_flags & kCGEventFlagMaskCommand) != 0;
|
||||
if (command_state != keyboard_capturer->command_flag_) {
|
||||
keyboard_capturer->command_flag_ = command_state;
|
||||
if (keyboard_capturer->command_flag_) {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
|
||||
} else {
|
||||
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
|
||||
}
|
||||
g_on_key_action(vk_code, keyboard_capturer->command_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() {}
|
||||
KeyboardCapturer::KeyboardCapturer()
|
||||
: event_tap_(nullptr), run_loop_source_(nullptr) {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {}
|
||||
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
if (event_tap_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_unmapped_keycode_to_vk.clear();
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
|
||||
@@ -115,15 +203,30 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
|
||||
run_loop_source_ =
|
||||
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap_, 0);
|
||||
if (!run_loop_source_) {
|
||||
LOG_ERROR("CFMachPortCreateRunLoopSource failed");
|
||||
CFRelease(event_tap_);
|
||||
event_tap_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source_,
|
||||
kCFRunLoopCommonModes);
|
||||
|
||||
const CGEventFlags current_flags =
|
||||
CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
|
||||
caps_lock_flag_ = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
|
||||
shift_flag_ = (current_flags & kCGEventFlagMaskShift) != 0;
|
||||
control_flag_ = (current_flags & kCGEventFlagMaskControl) != 0;
|
||||
option_flag_ = (current_flags & kCGEventFlagMaskAlternate) != 0;
|
||||
command_flag_ = (current_flags & kCGEventFlagMaskCommand) != 0;
|
||||
|
||||
CGEventTapEnable(event_tap_, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
g_unmapped_keycode_to_vk.clear();
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
|
||||
@@ -166,13 +269,39 @@ inline bool IsFunctionKey(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
CGEventFlags ToCGEventFlags(uint32_t injected_flags) {
|
||||
CGEventFlags flags = 0;
|
||||
if ((injected_flags & kMacInjectedModifierShift) != 0) {
|
||||
flags |= kCGEventFlagMaskShift;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierControl) != 0) {
|
||||
flags |= kCGEventFlagMaskControl;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierOption) != 0) {
|
||||
flags |= kCGEventFlagMaskAlternate;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierCommand) != 0) {
|
||||
flags |= kCGEventFlagMaskCommand;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
const uint32_t injected_flags =
|
||||
injected_modifier_state_.Update(key_code, is_down);
|
||||
|
||||
if (vkCodeToCGKeyCode.find(key_code) != vkCodeToCGKeyCode.end()) {
|
||||
CGKeyCode cg_key_code = vkCodeToCGKeyCode[key_code];
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(NULL, cg_key_code, is_down);
|
||||
CGEventRef clearFlags =
|
||||
CGEventCreateKeyboardEvent(NULL, (CGKeyCode)0, true);
|
||||
CGEventSetFlags(clearFlags, 0);
|
||||
if (!event) {
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
@@ -188,4 +317,4 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "macos_keyboard_modifier_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -21,11 +22,13 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
CFMachPortRef event_tap_;
|
||||
CFRunLoopSourceRef run_loop_source_;
|
||||
CFMachPortRef event_tap_ = nullptr;
|
||||
CFRunLoopSourceRef run_loop_source_ = nullptr;
|
||||
|
||||
public:
|
||||
bool caps_lock_flag_ = false;
|
||||
@@ -34,6 +37,7 @@ class KeyboardCapturer : public DeviceController {
|
||||
bool option_flag_ = false;
|
||||
bool command_flag_ = false;
|
||||
int fn_key_code_ = 0x3F;
|
||||
MacKeyboardModifierState injected_modifier_state_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -7,14 +7,56 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
|
||||
if (kb_data == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
|
||||
kb_data->vkCode != VK_MENU) {
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
|
||||
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
|
||||
scan_code |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized_vk != 0) {
|
||||
return static_cast<int>(normalized_vk);
|
||||
}
|
||||
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
g_on_key_action(kbData->vkCode, true, g_user_ptr);
|
||||
g_on_key_action(key_code, true, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||
g_on_key_action(kbData->vkCode, false, g_user_ptr);
|
||||
g_on_key_action(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -49,16 +91,53 @@ int KeyboardCapturer::Unhook() {
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
scan_code != 0
|
||||
? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
|
||||
: MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
|
||||
if (scan_code != 0 && !prefer_vk) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if (extended) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else if (resolved_scan_code != 0) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_down) {
|
||||
input.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
||||
is_down, GetLastError());
|
||||
return -1;
|
||||
}
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -21,7 +21,9 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
|
||||
@@ -73,13 +73,13 @@ std::map<int, int> vkCodeToCGKeyCode = {
|
||||
{0x20, 0x31}, // Space
|
||||
{0x08, 0x33}, // Backspace
|
||||
{0x09, 0x30}, // Tab
|
||||
{0x2C, 0x74}, // Print Screen
|
||||
{0x2C, 0x69}, // Print Screen(F13)
|
||||
{0x2D, 0x72}, // Insert
|
||||
{0x2E, 0x75}, // Delete
|
||||
{0x24, 0x73}, // Home
|
||||
{0x23, 0x77}, // End
|
||||
{0x21, 0x79}, // Page Up
|
||||
{0x22, 0x7A}, // Page Down
|
||||
{0x21, 0x74}, // Page Up
|
||||
{0x22, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x25, 0x7B}, // Left Arrow
|
||||
@@ -98,6 +98,7 @@ std::map<int, int> vkCodeToCGKeyCode = {
|
||||
{0x67, 0x59}, // Numpad 7
|
||||
{0x68, 0x5B}, // Numpad 8
|
||||
{0x69, 0x5C}, // Numpad 9
|
||||
{0x90, 0x47}, // Num Lock / Keypad Clear
|
||||
{0x6E, 0x41}, // Numpad .
|
||||
{0x6F, 0x4B}, // Numpad /
|
||||
{0x6A, 0x43}, // Numpad *
|
||||
@@ -191,13 +192,13 @@ std::map<int, int> CGKeyCodeToVkCode = {
|
||||
{0x31, 0x20}, // Space
|
||||
{0x33, 0x08}, // Backspace
|
||||
{0x30, 0x09}, // Tab
|
||||
{0x74, 0x2C}, // Print Screen
|
||||
{0x69, 0x2C}, // Print Screen(F13)
|
||||
{0x72, 0x2D}, // Insert
|
||||
{0x75, 0x2E}, // Delete
|
||||
{0x73, 0x24}, // Home
|
||||
{0x77, 0x23}, // End
|
||||
{0x79, 0x21}, // Page Up
|
||||
{0x7A, 0x22}, // Page Down
|
||||
{0x74, 0x21}, // Page Up
|
||||
{0x79, 0x22}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0x25}, // Left Arrow
|
||||
@@ -216,6 +217,7 @@ std::map<int, int> CGKeyCodeToVkCode = {
|
||||
{0x59, 0x67}, // Numpad 7
|
||||
{0x5B, 0x68}, // Numpad 8
|
||||
{0x5C, 0x69}, // Numpad 9
|
||||
{0x47, 0x90}, // Num Lock / Keypad Clear
|
||||
{0x41, 0x6E}, // Numpad .
|
||||
{0x4B, 0x6F}, // Numpad /
|
||||
{0x43, 0x6A}, // Numpad *
|
||||
@@ -326,21 +328,22 @@ std::map<int, int> vkCodeToX11KeySym = {
|
||||
{0x28, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x60, 0x0030}, // Numpad 0
|
||||
{0x61, 0x0031}, // Numpad 1
|
||||
{0x62, 0x0032}, // Numpad 2
|
||||
{0x63, 0x0033}, // Numpad 3
|
||||
{0x64, 0x0034}, // Numpad 4
|
||||
{0x65, 0x0035}, // Numpad 5
|
||||
{0x66, 0x0036}, // Numpad 6
|
||||
{0x67, 0x0037}, // Numpad 7
|
||||
{0x68, 0x0038}, // Numpad 8
|
||||
{0x69, 0x0039}, // Numpad 9
|
||||
{0x6E, 0x003A}, // Numpad .
|
||||
{0x6F, 0x002F}, // Numpad /
|
||||
{0x6A, 0x002A}, // Numpad *
|
||||
{0x6D, 0x002D}, // Numpad -
|
||||
{0x6B, 0x002B}, // Numpad +
|
||||
{0x60, 0xFFB0}, // Numpad 0
|
||||
{0x61, 0xFFB1}, // Numpad 1
|
||||
{0x62, 0xFFB2}, // Numpad 2
|
||||
{0x63, 0xFFB3}, // Numpad 3
|
||||
{0x64, 0xFFB4}, // Numpad 4
|
||||
{0x65, 0xFFB5}, // Numpad 5
|
||||
{0x66, 0xFFB6}, // Numpad 6
|
||||
{0x67, 0xFFB7}, // Numpad 7
|
||||
{0x68, 0xFFB8}, // Numpad 8
|
||||
{0x69, 0xFFB9}, // Numpad 9
|
||||
{0x90, 0xFF7F}, // Num Lock
|
||||
{0x6E, 0xFFAE}, // Numpad .
|
||||
{0x6F, 0xFFAF}, // Numpad /
|
||||
{0x6A, 0xFFAA}, // Numpad *
|
||||
{0x6D, 0xFFAD}, // Numpad -
|
||||
{0x6B, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0xBA, 0x003B}, // ; (Semicolon)
|
||||
@@ -454,21 +457,22 @@ std::map<int, int> x11KeySymToVkCode = {
|
||||
{0xFF54, 0x28}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x0030, 0x60}, // Numpad 0
|
||||
{0x0031, 0x61}, // Numpad 1
|
||||
{0x0032, 0x62}, // Numpad 2
|
||||
{0x0033, 0x63}, // Numpad 3
|
||||
{0x0034, 0x64}, // Numpad 4
|
||||
{0x0035, 0x65}, // Numpad 5
|
||||
{0x0036, 0x66}, // Numpad 6
|
||||
{0x0037, 0x67}, // Numpad 7
|
||||
{0x0038, 0x68}, // Numpad 8
|
||||
{0x0039, 0x69}, // Numpad 9
|
||||
{0x003A, 0x6E}, // Numpad .
|
||||
{0x002F, 0x6F}, // Numpad /
|
||||
{0x002A, 0x6A}, // Numpad *
|
||||
{0x002D, 0x6D}, // Numpad -
|
||||
{0x002B, 0x6B}, // Numpad +
|
||||
{0xFFB0, 0x60}, // Numpad 0
|
||||
{0xFFB1, 0x61}, // Numpad 1
|
||||
{0xFFB2, 0x62}, // Numpad 2
|
||||
{0xFFB3, 0x63}, // Numpad 3
|
||||
{0xFFB4, 0x64}, // Numpad 4
|
||||
{0xFFB5, 0x65}, // Numpad 5
|
||||
{0xFFB6, 0x66}, // Numpad 6
|
||||
{0xFFB7, 0x67}, // Numpad 7
|
||||
{0xFFB8, 0x68}, // Numpad 8
|
||||
{0xFFB9, 0x69}, // Numpad 9
|
||||
{0xFF7F, 0x90}, // Num Lock
|
||||
{0xFFAE, 0x6E}, // Numpad .
|
||||
{0xFFAF, 0x6F}, // Numpad /
|
||||
{0xFFAA, 0x6A}, // Numpad *
|
||||
{0xFFAD, 0x6D}, // Numpad -
|
||||
{0xFFAB, 0x6B}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0xBA}, // ; (Semicolon)
|
||||
@@ -557,13 +561,13 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
|
||||
{0x31, 0x0020}, // Space
|
||||
{0x33, 0xFF08}, // Backspace
|
||||
{0x30, 0xFF09}, // Tab
|
||||
{0x74, 0xFF15}, // Print Screen
|
||||
{0x69, 0xFF15}, // Print Screen(F13)
|
||||
{0x72, 0xFF63}, // Insert
|
||||
{0x75, 0xFFFF}, // Delete
|
||||
{0x73, 0xFF50}, // Home
|
||||
{0x77, 0xFF57}, // End
|
||||
{0x79, 0xFF55}, // Page Up
|
||||
{0x7A, 0xFF56}, // Page Down
|
||||
{0x74, 0xFF55}, // Page Up
|
||||
{0x79, 0xFF56}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0xFF51}, // Left Arrow
|
||||
@@ -572,21 +576,22 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
|
||||
{0x7D, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x52, 0x0030}, // Numpad 0
|
||||
{0x53, 0x0031}, // Numpad 1
|
||||
{0x54, 0x0032}, // Numpad 2
|
||||
{0x55, 0x0033}, // Numpad 3
|
||||
{0x56, 0x0034}, // Numpad 4
|
||||
{0x57, 0x0035}, // Numpad 5
|
||||
{0x58, 0x0036}, // Numpad 6
|
||||
{0x59, 0x0037}, // Numpad 7
|
||||
{0x5B, 0x0038}, // Numpad 8
|
||||
{0x5C, 0x0039}, // Numpad 9
|
||||
{0x41, 0x003A}, // Numpad .
|
||||
{0x4B, 0x002F}, // Numpad /
|
||||
{0x43, 0x002A}, // Numpad *
|
||||
{0x4E, 0x002D}, // Numpad -
|
||||
{0x45, 0x002B}, // Numpad +
|
||||
{0x52, 0xFFB0}, // Numpad 0
|
||||
{0x53, 0xFFB1}, // Numpad 1
|
||||
{0x54, 0xFFB2}, // Numpad 2
|
||||
{0x55, 0xFFB3}, // Numpad 3
|
||||
{0x56, 0xFFB4}, // Numpad 4
|
||||
{0x57, 0xFFB5}, // Numpad 5
|
||||
{0x58, 0xFFB6}, // Numpad 6
|
||||
{0x59, 0xFFB7}, // Numpad 7
|
||||
{0x5B, 0xFFB8}, // Numpad 8
|
||||
{0x5C, 0xFFB9}, // Numpad 9
|
||||
{0x47, 0xFF7F}, // Num Lock / Keypad Clear
|
||||
{0x41, 0xFFAE}, // Numpad .
|
||||
{0x4B, 0xFFAF}, // Numpad /
|
||||
{0x43, 0xFFAA}, // Numpad *
|
||||
{0x4E, 0xFFAD}, // Numpad -
|
||||
{0x45, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x29, 0x003B}, // ; (Semicolon)
|
||||
@@ -683,13 +688,13 @@ std::map<int, int> x11KeySymToCgKeyCode = {
|
||||
{0x0020, 0x31}, // Space
|
||||
{0xFF08, 0x33}, // Backspace
|
||||
{0xFF09, 0x30}, // Tab
|
||||
{0xFF15, 0x74}, // Print Screen
|
||||
{0xFF15, 0x69}, // Print Screen(F13)
|
||||
{0xFF63, 0x72}, // Insert
|
||||
{0xFFFF, 0x75}, // Delete
|
||||
{0xFF50, 0x73}, // Home
|
||||
{0xFF57, 0x77}, // End
|
||||
{0xFF55, 0x79}, // Page Up
|
||||
{0xFF56, 0x7A}, // Page Down
|
||||
{0xFF55, 0x74}, // Page Up
|
||||
{0xFF56, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0xFF51, 0x7B}, // Left Arrow
|
||||
@@ -698,21 +703,22 @@ std::map<int, int> x11KeySymToCgKeyCode = {
|
||||
{0xFF54, 0x7D}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x0030, 0x52}, // Numpad 0
|
||||
{0x0031, 0x53}, // Numpad 1
|
||||
{0x0032, 0x54}, // Numpad 2
|
||||
{0x0033, 0x55}, // Numpad 3
|
||||
{0x0034, 0x56}, // Numpad 4
|
||||
{0x0035, 0x57}, // Numpad 5
|
||||
{0x0036, 0x58}, // Numpad 6
|
||||
{0x0037, 0x59}, // Numpad 7
|
||||
{0x0038, 0x5B}, // Numpad 8
|
||||
{0x0039, 0x5C}, // Numpad 9
|
||||
{0x003A, 0x41}, // Numpad .
|
||||
{0x002F, 0x4B}, // Numpad /
|
||||
{0x002A, 0x43}, // Numpad *
|
||||
{0x002D, 0x4E}, // Numpad -
|
||||
{0x002B, 0x45}, // Numpad +
|
||||
{0xFFB0, 0x52}, // Numpad 0
|
||||
{0xFFB1, 0x53}, // Numpad 1
|
||||
{0xFFB2, 0x54}, // Numpad 2
|
||||
{0xFFB3, 0x55}, // Numpad 3
|
||||
{0xFFB4, 0x56}, // Numpad 4
|
||||
{0xFFB5, 0x57}, // Numpad 5
|
||||
{0xFFB6, 0x58}, // Numpad 6
|
||||
{0xFFB7, 0x59}, // Numpad 7
|
||||
{0xFFB8, 0x5B}, // Numpad 8
|
||||
{0xFFB9, 0x5C}, // Numpad 9
|
||||
{0xFF7F, 0x47}, // Num Lock / Keypad Clear
|
||||
{0xFFAE, 0x41}, // Numpad .
|
||||
{0xFFAF, 0x4B}, // Numpad /
|
||||
{0xFFAA, 0x43}, // Numpad *
|
||||
{0xFFAD, 0x4E}, // Numpad -
|
||||
{0xFFAB, 0x45}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0x29}, // ; (Semicolon)
|
||||
@@ -739,4 +745,4 @@ std::map<int, int> x11KeySymToCgKeyCode = {
|
||||
{0xFFEC, 0x36}, // Right Command
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
#define _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr uint32_t kMacInjectedModifierShift = 1u << 0;
|
||||
inline constexpr uint32_t kMacInjectedModifierControl = 1u << 1;
|
||||
inline constexpr uint32_t kMacInjectedModifierOption = 1u << 2;
|
||||
inline constexpr uint32_t kMacInjectedModifierCommand = 1u << 3;
|
||||
|
||||
class MacKeyboardModifierState {
|
||||
public:
|
||||
uint32_t Update(int key_code, bool is_down) {
|
||||
bool* state = MutableStateForVk(key_code);
|
||||
if (state != nullptr) {
|
||||
*state = is_down;
|
||||
}
|
||||
return flags();
|
||||
}
|
||||
|
||||
uint32_t flags() const {
|
||||
uint32_t result = 0;
|
||||
if (left_shift_down_ || right_shift_down_) {
|
||||
result |= kMacInjectedModifierShift;
|
||||
}
|
||||
if (left_control_down_ || right_control_down_) {
|
||||
result |= kMacInjectedModifierControl;
|
||||
}
|
||||
if (left_option_down_ || right_option_down_) {
|
||||
result |= kMacInjectedModifierOption;
|
||||
}
|
||||
if (left_command_down_ || right_command_down_) {
|
||||
result |= kMacInjectedModifierCommand;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
left_shift_down_ = false;
|
||||
right_shift_down_ = false;
|
||||
left_control_down_ = false;
|
||||
right_control_down_ = false;
|
||||
left_option_down_ = false;
|
||||
right_option_down_ = false;
|
||||
left_command_down_ = false;
|
||||
right_command_down_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool* MutableStateForVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0xA0: // VK_LSHIFT
|
||||
return &left_shift_down_;
|
||||
case 0xA1: // VK_RSHIFT
|
||||
return &right_shift_down_;
|
||||
case 0xA2: // VK_LCONTROL
|
||||
return &left_control_down_;
|
||||
case 0xA3: // VK_RCONTROL
|
||||
return &right_control_down_;
|
||||
case 0xA4: // VK_LMENU / left Option
|
||||
return &left_option_down_;
|
||||
case 0xA5: // VK_RMENU / right Option
|
||||
return &right_option_down_;
|
||||
case 0x5B: // VK_LWIN / left Command
|
||||
return &left_command_down_;
|
||||
case 0x5C: // VK_RWIN / right Command
|
||||
return &right_command_down_;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool left_shift_down_ = false;
|
||||
bool right_shift_down_ = false;
|
||||
bool left_control_down_ = false;
|
||||
bool right_control_down_ = false;
|
||||
bool left_option_down_ = false;
|
||||
bool right_option_down_ = false;
|
||||
bool left_command_down_ = false;
|
||||
bool right_command_down_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <X11/extensions/XTest.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -12,6 +13,17 @@ 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");
|
||||
@@ -25,26 +37,68 @@ 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:
|
||||
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;
|
||||
}
|
||||
|
||||
SetMousePosition(
|
||||
static_cast<int>(remote_action.m.x *
|
||||
display_info_list_[display_index].width +
|
||||
@@ -53,6 +107,7 @@ 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_);
|
||||
@@ -103,25 +158,39 @@ 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
|
||||
|
||||
@@ -11,10 +11,16 @@
|
||||
#include <X11/Xutil.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
struct DBusConnection;
|
||||
struct DBusMessageIter;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class MouseController : public DeviceController {
|
||||
@@ -26,18 +32,49 @@ 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;
|
||||
int wayland_portal_space_width_ = 0;
|
||||
int wayland_portal_space_height_ = 0;
|
||||
bool using_shared_wayland_session_ = false;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -18,7 +20,14 @@ int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
INPUT ip;
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
INPUT ip = {0};
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
ip.type = INPUT_MOUSE;
|
||||
@@ -63,13 +72,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
|
||||
ip.mi.time = 0;
|
||||
|
||||
SetCursorPos(ip.mi.dx, ip.mi.dy);
|
||||
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
|
||||
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
|
||||
GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
|
||||
SendInput(1, &ip, sizeof(INPUT));
|
||||
UINT sent = SendInput(1, &ip, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN(
|
||||
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), GetLastError());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_KEY_METADATA_H_
|
||||
#define _WINDOWS_KEY_METADATA_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool LookupWindowsKeyMetadataFromVk(int key_code,
|
||||
uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key_code) {
|
||||
case 0x21: // Page Up
|
||||
*scan_code_out = 0x49;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x22: // Page Down
|
||||
*scan_code_out = 0x51;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x23: // End
|
||||
*scan_code_out = 0x4F;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x24: // Home
|
||||
*scan_code_out = 0x47;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x25: // Left Arrow
|
||||
*scan_code_out = 0x4B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x26: // Up Arrow
|
||||
*scan_code_out = 0x48;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x27: // Right Arrow
|
||||
*scan_code_out = 0x4D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x28: // Down Arrow
|
||||
*scan_code_out = 0x50;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2D: // Insert
|
||||
*scan_code_out = 0x52;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2E: // Delete
|
||||
*scan_code_out = 0x53;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x6F: // Numpad /
|
||||
*scan_code_out = 0x35;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA3: // Right Ctrl
|
||||
*scan_code_out = 0x1D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA5: // Right Alt
|
||||
*scan_code_out = 0x38;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5B: // Left Win
|
||||
*scan_code_out = 0x5B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5C: // Right Win
|
||||
*scan_code_out = 0x5C;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,246 +1,156 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
/*
|
||||
* @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>
|
||||
|
||||
#include "localization_data.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
struct LanguageOption {
|
||||
std::string code;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
namespace crossdesk {
|
||||
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 localization {
|
||||
inline int ClampLanguageIndex(int language_index) {
|
||||
if (language_index >= 0 &&
|
||||
language_index < static_cast<int>(GetSupportedLanguages().size())) {
|
||||
return language_index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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"};
|
||||
using TranslationTable =
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, std::string>>;
|
||||
|
||||
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> select_a_file = {
|
||||
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
|
||||
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 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> new_password = {
|
||||
reinterpret_cast<const char*>(u8"请输入六位密码:"),
|
||||
"Please input a six-char password:"};
|
||||
inline TranslationTable BuildTranslationTable() {
|
||||
TranslationTable table;
|
||||
for (const auto& row : kTranslationRows) {
|
||||
table[row.key] = MakeLocalizedValues(row);
|
||||
}
|
||||
|
||||
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> receiving_screen = {
|
||||
reinterpret_cast<const char*>(u8"画面接收中..."), "Receiving screen..."};
|
||||
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:"};
|
||||
static std::vector<std::string> file_transfer_save_path = {
|
||||
reinterpret_cast<const char*>(u8"文件接收保存路径:"),
|
||||
"File Transfer Save Path:"};
|
||||
static std::vector<std::string> browse = {
|
||||
reinterpret_cast<const char*>(u8"浏览"), "Browse"};
|
||||
static std::vector<std::string> default_desktop = {
|
||||
reinterpret_cast<const char*>(u8"桌面"), "Desktop"};
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
||||
"Minimize to system tray when exit:"};
|
||||
static std::vector<std::string> resolution = {
|
||||
reinterpret_cast<const char*>(u8"分辨率"), "Res"};
|
||||
static std::vector<std::string> connection_mode = {
|
||||
reinterpret_cast<const char*>(u8"连接模式"), "Mode"};
|
||||
static std::vector<std::string> connection_mode_direct = {
|
||||
reinterpret_cast<const char*>(u8"直连"), "Direct"};
|
||||
static std::vector<std::string> connection_mode_relay = {
|
||||
reinterpret_cast<const char*>(u8"中继"), "Relay"};
|
||||
static std::vector<std::string> online = {
|
||||
reinterpret_cast<const char*>(u8"在线"), "Online"};
|
||||
static std::vector<std::string> offline = {
|
||||
reinterpret_cast<const char*>(u8"离线"), "Offline"};
|
||||
static std::vector<std::string> device_offline = {
|
||||
reinterpret_cast<const char*>(u8"设备离线"), "Device Offline"};
|
||||
|
||||
#if _WIN32
|
||||
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
|
||||
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
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* @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(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
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 Mouse", u8"Управление мышью") \
|
||||
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \
|
||||
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \
|
||||
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \
|
||||
X(install_windows_service, u8"安装", "Install", \
|
||||
u8"Установить службу") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \
|
||||
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \
|
||||
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||
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 to Send", \
|
||||
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
|
||||
@@ -46,7 +46,7 @@ int Render::LocalWindow() {
|
||||
ImGui::BeginChild(
|
||||
"LocalDesktopPanel",
|
||||
ImVec2(local_window_width * 0.8f, local_window_height * 0.43f),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -300,4 +300,4 @@ int Render::LocalWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -17,7 +17,7 @@ int Render::RecentConnectionsWindow() {
|
||||
ImGui::BeginChild(
|
||||
"RecentConnectionsWindow",
|
||||
ImVec2(recent_connection_window_width, recent_connection_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
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_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
|
||||
@@ -360,4 +360,4 @@ int Render::OfflineWarningWindow() {
|
||||
ImGui::PopStyleVar();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -48,7 +48,7 @@ int Render::RemoteWindow() {
|
||||
ImGui::BeginChild(
|
||||
"RemoteDesktopWindow_1",
|
||||
ImVec2(remote_window_width * 0.8f, remote_window_height * 0.43f),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -165,7 +165,21 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
|
||||
}
|
||||
|
||||
int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_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;
|
||||
}
|
||||
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
@@ -190,11 +204,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 9.0f;
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
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_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
@@ -210,6 +224,8 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
@@ -256,4 +272,4 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
+669
-119
File diff suppressed because it is too large
Load Diff
+81
-4
@@ -20,6 +20,7 @@
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
@@ -43,6 +44,14 @@
|
||||
namespace crossdesk {
|
||||
class Render {
|
||||
public:
|
||||
enum class RemoteUnlockState {
|
||||
none,
|
||||
service_unavailable,
|
||||
lock_screen,
|
||||
credential_ui,
|
||||
secure_desktop,
|
||||
};
|
||||
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
@@ -53,6 +62,7 @@ class Render {
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
bool file_transfer_window_hovered_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
@@ -83,6 +93,8 @@ class Render {
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string file_label_ = "file";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
@@ -103,6 +115,7 @@ class Render {
|
||||
bool is_control_bar_in_left_ = true;
|
||||
bool control_bar_hovered_ = false;
|
||||
bool display_selectable_hovered_ = false;
|
||||
bool shortcut_selectable_hovered_ = false;
|
||||
bool control_bar_expand_ = true;
|
||||
bool reset_control_bar_pos_ = false;
|
||||
bool control_window_width_is_changing_ = false;
|
||||
@@ -113,10 +126,10 @@ class Render {
|
||||
float sub_stream_window_width_ = 1280;
|
||||
float sub_stream_window_height_ = 720;
|
||||
float control_window_min_width_ = 20;
|
||||
float control_window_max_width_ = 230;
|
||||
float control_window_max_width_ = 300;
|
||||
float control_window_min_height_ = 38;
|
||||
float control_window_max_height_ = 180;
|
||||
float control_window_width_ = 230;
|
||||
float control_window_width_ = 300;
|
||||
float control_window_height_ = 38;
|
||||
float control_bar_pos_x_ = 0;
|
||||
float control_bar_pos_y_ = 30;
|
||||
@@ -156,10 +169,14 @@ class Render {
|
||||
std::string mouse_control_button_label_ = "Mouse Control";
|
||||
std::string audio_capture_button_label_ = "Audio Capture";
|
||||
std::string remote_host_name_ = "";
|
||||
bool remote_service_status_received_ = false;
|
||||
bool remote_service_available_ = false;
|
||||
std::string remote_interactive_stage_ = "";
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
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,6 +211,7 @@ class Render {
|
||||
void UpdateInteractions();
|
||||
void HandleRecentConnections();
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
@@ -235,6 +253,7 @@ 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);
|
||||
@@ -242,7 +261,9 @@ class Render {
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password);
|
||||
bool remember_password, bool bypass_presence_check = false);
|
||||
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||
const char* password, bool remember_password);
|
||||
int CreateMainWindow();
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
@@ -263,6 +284,11 @@ class Render {
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawReceivingScreenText(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void ResetRemoteServiceStatus(SubStreamWindowProperties& props);
|
||||
void ApplyRemoteServiceStatus(SubStreamWindowProperties& props,
|
||||
const ServiceStatus& status);
|
||||
RemoteUnlockState GetRemoteUnlockState(
|
||||
const SubStreamWindowProperties& props) const;
|
||||
#ifdef __APPLE__
|
||||
int RequestPermissionWindow();
|
||||
bool CheckScreenRecordingPermission();
|
||||
@@ -314,7 +340,12 @@ class Render {
|
||||
static void FreeRemoteAction(RemoteAction& action);
|
||||
|
||||
private:
|
||||
int SendKeyCommand(int key_code, bool is_down);
|
||||
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
static bool IsModifierVkKey(int key_code);
|
||||
void TrackPressedKeyState(int key_code, bool is_down);
|
||||
void ForceReleasePressedKeys();
|
||||
int ProcessKeyboardEvent(const SDL_Event& event);
|
||||
int ProcessMouseEvent(const SDL_Event& event);
|
||||
|
||||
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
|
||||
@@ -348,6 +379,23 @@ class Render {
|
||||
|
||||
int AudioDeviceInit();
|
||||
int AudioDeviceDestroy();
|
||||
void HandleWindowsServiceIntegration();
|
||||
#if _WIN32
|
||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||
#if CROSSDESK_PORTABLE
|
||||
enum class PortableServiceInstallState {
|
||||
idle,
|
||||
installing,
|
||||
succeeded,
|
||||
failed,
|
||||
};
|
||||
|
||||
void CheckPortableWindowsService();
|
||||
int PortableServiceInstallWindow();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
private:
|
||||
struct CDCache {
|
||||
@@ -444,6 +492,7 @@ class Render {
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool keyboard_capturer_uses_sdl_events_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool focus_on_stream_window_ = false;
|
||||
bool main_window_minimized_ = false;
|
||||
@@ -499,9 +548,27 @@ class Render {
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
std::string remote_client_id_ = "";
|
||||
std::unordered_set<int> pressed_keyboard_keys_;
|
||||
std::mutex pressed_keyboard_keys_mutex_;
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
#if _WIN32
|
||||
std::atomic<bool> pending_windows_service_sas_{false};
|
||||
bool local_service_status_received_ = false;
|
||||
bool local_service_available_ = false;
|
||||
std::string local_interactive_stage_;
|
||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||
uint32_t last_windows_service_status_tick_ = 0;
|
||||
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
#if CROSSDESK_PORTABLE
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// stream window render
|
||||
SDL_Window* stream_window_ = nullptr;
|
||||
@@ -598,6 +665,8 @@ class Render {
|
||||
std::string video_secondary_label_ = "secondary_display";
|
||||
std::string audio_label_ = "audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
std::string keyboard_label_ = "keyboard";
|
||||
std::string info_label_ = "info";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_label_ = "file";
|
||||
@@ -623,6 +692,7 @@ class Render {
|
||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
uint64_t last_frame_time_;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
@@ -689,6 +759,13 @@ 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
|
||||
|
||||
+635
-53
@@ -1,3 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
@@ -16,11 +17,326 @@
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
|
||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||
return 0x90;
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_KP_0:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2D;
|
||||
}
|
||||
return 0x60;
|
||||
case SDL_SCANCODE_KP_1:
|
||||
if (!numlock_enabled) {
|
||||
return 0x23;
|
||||
}
|
||||
return 0x61;
|
||||
case SDL_SCANCODE_KP_2:
|
||||
if (!numlock_enabled) {
|
||||
return 0x28;
|
||||
}
|
||||
return 0x62;
|
||||
case SDL_SCANCODE_KP_3:
|
||||
if (!numlock_enabled) {
|
||||
return 0x22;
|
||||
}
|
||||
return 0x63;
|
||||
case SDL_SCANCODE_KP_4:
|
||||
if (!numlock_enabled) {
|
||||
return 0x25;
|
||||
}
|
||||
return 0x64;
|
||||
case SDL_SCANCODE_KP_5:
|
||||
return 0x65;
|
||||
case SDL_SCANCODE_KP_6:
|
||||
if (!numlock_enabled) {
|
||||
return 0x27;
|
||||
}
|
||||
return 0x66;
|
||||
case SDL_SCANCODE_KP_7:
|
||||
if (!numlock_enabled) {
|
||||
return 0x24;
|
||||
}
|
||||
return 0x67;
|
||||
case SDL_SCANCODE_KP_8:
|
||||
if (!numlock_enabled) {
|
||||
return 0x26;
|
||||
}
|
||||
return 0x68;
|
||||
case SDL_SCANCODE_KP_9:
|
||||
if (!numlock_enabled) {
|
||||
return 0x21;
|
||||
}
|
||||
return 0x69;
|
||||
case SDL_SCANCODE_KP_PERIOD:
|
||||
case SDL_SCANCODE_KP_COMMA:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2E;
|
||||
}
|
||||
return 0x6E;
|
||||
case SDL_SCANCODE_KP_DIVIDE:
|
||||
return 0x6F;
|
||||
case SDL_SCANCODE_KP_MULTIPLY:
|
||||
return 0x6A;
|
||||
case SDL_SCANCODE_KP_MINUS:
|
||||
return 0x6D;
|
||||
case SDL_SCANCODE_KP_PLUS:
|
||||
return 0x6B;
|
||||
case SDL_SCANCODE_KP_EQUALS:
|
||||
return 0xBB;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent& event) {
|
||||
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
|
||||
if (keypad_key_code >= 0) {
|
||||
return keypad_key_code;
|
||||
}
|
||||
|
||||
const int key = static_cast<int>(event.key);
|
||||
if (key >= 'a' && key <= 'z') {
|
||||
return key - 'a' + 0x41;
|
||||
}
|
||||
if (key >= 'A' && key <= 'Z') {
|
||||
return key;
|
||||
}
|
||||
if (key >= '0' && key <= '9') {
|
||||
return key;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case ';':
|
||||
return 0xBA;
|
||||
case '\'':
|
||||
return 0xDE;
|
||||
case '`':
|
||||
return 0xC0;
|
||||
case ',':
|
||||
return 0xBC;
|
||||
case '.':
|
||||
return 0xBE;
|
||||
case '/':
|
||||
return 0xBF;
|
||||
case '\\':
|
||||
return 0xDC;
|
||||
case '[':
|
||||
return 0xDB;
|
||||
case ']':
|
||||
return 0xDD;
|
||||
case '-':
|
||||
return 0xBD;
|
||||
case '=':
|
||||
return 0xBB;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
return 0x1B;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_SPACE:
|
||||
return 0x20;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
return 0x08;
|
||||
case SDL_SCANCODE_TAB:
|
||||
return 0x09;
|
||||
case SDL_SCANCODE_PRINTSCREEN:
|
||||
return 0x2C;
|
||||
case SDL_SCANCODE_SCROLLLOCK:
|
||||
return 0x91;
|
||||
case SDL_SCANCODE_PAUSE:
|
||||
return 0x13;
|
||||
case SDL_SCANCODE_INSERT:
|
||||
return 0x2D;
|
||||
case SDL_SCANCODE_DELETE:
|
||||
return 0x2E;
|
||||
case SDL_SCANCODE_HOME:
|
||||
return 0x24;
|
||||
case SDL_SCANCODE_END:
|
||||
return 0x23;
|
||||
case SDL_SCANCODE_PAGEUP:
|
||||
return 0x21;
|
||||
case SDL_SCANCODE_PAGEDOWN:
|
||||
return 0x22;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
return 0x25;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
return 0x27;
|
||||
case SDL_SCANCODE_UP:
|
||||
return 0x26;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
return 0x28;
|
||||
case SDL_SCANCODE_F1:
|
||||
return 0x70;
|
||||
case SDL_SCANCODE_F2:
|
||||
return 0x71;
|
||||
case SDL_SCANCODE_F3:
|
||||
return 0x72;
|
||||
case SDL_SCANCODE_F4:
|
||||
return 0x73;
|
||||
case SDL_SCANCODE_F5:
|
||||
return 0x74;
|
||||
case SDL_SCANCODE_F6:
|
||||
return 0x75;
|
||||
case SDL_SCANCODE_F7:
|
||||
return 0x76;
|
||||
case SDL_SCANCODE_F8:
|
||||
return 0x77;
|
||||
case SDL_SCANCODE_F9:
|
||||
return 0x78;
|
||||
case SDL_SCANCODE_F10:
|
||||
return 0x79;
|
||||
case SDL_SCANCODE_F11:
|
||||
return 0x7A;
|
||||
case SDL_SCANCODE_F12:
|
||||
return 0x7B;
|
||||
case SDL_SCANCODE_CAPSLOCK:
|
||||
return 0x14;
|
||||
case SDL_SCANCODE_LSHIFT:
|
||||
return 0xA0;
|
||||
case SDL_SCANCODE_RSHIFT:
|
||||
return 0xA1;
|
||||
case SDL_SCANCODE_LCTRL:
|
||||
return 0xA2;
|
||||
case SDL_SCANCODE_RCTRL:
|
||||
return 0xA3;
|
||||
case SDL_SCANCODE_LALT:
|
||||
return 0xA4;
|
||||
case SDL_SCANCODE_RALT:
|
||||
return 0xA5;
|
||||
case SDL_SCANCODE_LGUI:
|
||||
return 0x5B;
|
||||
case SDL_SCANCODE_RGUI:
|
||||
return 0x5C;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if _WIN32
|
||||
if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) {
|
||||
return key_code;
|
||||
}
|
||||
|
||||
UINT scan_code_with_prefix = static_cast<UINT>(scan_code & 0xFF);
|
||||
if (extended) {
|
||||
scan_code_with_prefix |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk =
|
||||
MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX);
|
||||
return normalized_vk != 0 ? static_cast<int>(normalized_vk) : key_code;
|
||||
#else
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return key_code;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const UINT scan_code =
|
||||
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
if (scan_code == 0) {
|
||||
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
|
||||
return;
|
||||
}
|
||||
|
||||
*scan_code_out = static_cast<uint32_t>(scan_code & 0xFF);
|
||||
*extended_out = (scan_code & 0xFF00) != 0;
|
||||
#else
|
||||
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo>& displays,
|
||||
int display_index, float normalized_x,
|
||||
float normalized_y, int* absolute_x_out,
|
||||
int* absolute_y_out) {
|
||||
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
|
||||
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const DisplayInfo& display = displays[display_index];
|
||||
if (display.width <= 0 || display.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
|
||||
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
|
||||
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
|
||||
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
|
||||
return true;
|
||||
}
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side,
|
||||
const char* stage) {
|
||||
if (last_tick == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
*last_tick = now;
|
||||
LOG_WARN(
|
||||
"{} secure-desktop input blocked, stage={}, normal SendInput path "
|
||||
"cannot drive the Windows password UI",
|
||||
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||
const RemoteAction& action) {
|
||||
if (!response.is_object()) {
|
||||
return false;
|
||||
}
|
||||
if (response.value("error", std::string()) != "send_input_failed") {
|
||||
return false;
|
||||
}
|
||||
if (response.value("code", 0u) != ERROR_ACCESS_DENIED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return action.type == ControlType::keyboard &&
|
||||
action.k.flag == KeyFlag::key_up;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void Render::OnSignalMessageCb(const char* message, size_t size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
@@ -48,6 +364,15 @@ void Render::OnSignalMessageCb(const char* message, size_t size,
|
||||
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") {
|
||||
@@ -57,20 +382,89 @@ void Render::OnSignalMessageCb(const char* message, size_t size,
|
||||
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;
|
||||
bool Render::IsModifierVkKey(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0x10: // VK_SHIFT
|
||||
case 0x11: // VK_CONTROL
|
||||
case 0x12: // VK_MENU(ALT)
|
||||
case 0x5B: // VK_LWIN
|
||||
case 0x5C: // VK_RWIN
|
||||
case 0xA0: // VK_LSHIFT
|
||||
case 0xA1: // VK_RSHIFT
|
||||
case 0xA2: // VK_LCONTROL
|
||||
case 0xA3: // VK_RCONTROL
|
||||
case 0xA4: // VK_LMENU
|
||||
case 0xA5: // VK_RMENU
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Render::TrackPressedKeyState(int key_code, bool is_down) {
|
||||
if (!IsWaylandSession() && !IsModifierVkKey(key_code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||
if (is_down) {
|
||||
pressed_keyboard_keys_.insert(key_code);
|
||||
} else {
|
||||
pressed_keyboard_keys_.erase(key_code);
|
||||
}
|
||||
}
|
||||
|
||||
void Render::ForceReleasePressedKeys() {
|
||||
std::vector<int> pressed_keys;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
|
||||
if (pressed_keyboard_keys_.empty()) {
|
||||
return;
|
||||
}
|
||||
pressed_keys.assign(pressed_keyboard_keys_.begin(),
|
||||
pressed_keyboard_keys_.end());
|
||||
pressed_keyboard_keys_.clear();
|
||||
}
|
||||
|
||||
for (int key_code : pressed_keys) {
|
||||
SendKeyCommand(key_code, false);
|
||||
}
|
||||
}
|
||||
|
||||
int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended) {
|
||||
RemoteAction remote_action{};
|
||||
remote_action.type = ControlType::keyboard;
|
||||
if (is_down) {
|
||||
remote_action.k.flag = KeyFlag::key_down;
|
||||
} else {
|
||||
remote_action.k.flag = KeyFlag::key_up;
|
||||
}
|
||||
|
||||
if (scan_code == 0) {
|
||||
PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
}
|
||||
#if _WIN32
|
||||
key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended);
|
||||
#endif
|
||||
|
||||
remote_action.k.key_value = key_code;
|
||||
remote_action.k.scan_code = scan_code;
|
||||
remote_action.k.extended = extended;
|
||||
|
||||
std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_
|
||||
: controlled_remote_id_;
|
||||
@@ -80,21 +474,101 @@ int Render::SendKeyCommand(int key_code, bool is_down) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->keyboard_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Send keyboard command failed, remote_id={}, ret={}",
|
||||
target_id, ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TrackPressedKeyState(key_code, is_down);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::ProcessKeyboardEvent(const SDL_Event& event) {
|
||||
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
||||
if (key_code < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_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;
|
||||
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_) {
|
||||
@@ -103,23 +577,31 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
const bool file_transfer_window_hovered =
|
||||
props->file_transfer_.file_transfer_window_hovered_;
|
||||
const bool overlay_hovered =
|
||||
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
|
||||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
|
||||
|
||||
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;
|
||||
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) {
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
@@ -144,21 +626,19 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
remote_action.m.flag = MouseFlag::move;
|
||||
}
|
||||
|
||||
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
|
||||
if (overlay_hovered) {
|
||||
break;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
|
||||
last_mouse_event.button.x >= props->stream_render_rect_.x &&
|
||||
last_mouse_event.button.x <= props->stream_render_rect_.x +
|
||||
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) {
|
||||
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) {
|
||||
float scroll_x = event.wheel.x;
|
||||
float scroll_y = event.wheel.y;
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
@@ -185,22 +665,20 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
remote_action.m.s = roundUp(scroll_x);
|
||||
}
|
||||
|
||||
render_width = props->stream_render_rect_.w;
|
||||
render_height = props->stream_render_rect_.h;
|
||||
remote_action.m.x =
|
||||
(float)(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;
|
||||
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);
|
||||
|
||||
if (props->control_bar_hovered_) {
|
||||
if (overlay_hovered) {
|
||||
continue;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -498,9 +976,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);
|
||||
}
|
||||
}
|
||||
@@ -581,16 +1059,36 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
}
|
||||
|
||||
std::string json_str(data, size);
|
||||
RemoteAction remote_action;
|
||||
|
||||
try {
|
||||
remote_action.from_json(json_str);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR("Failed to parse RemoteAction JSON: {}", e.what());
|
||||
RemoteAction remote_action{};
|
||||
if (!remote_action.from_json(json_str)) {
|
||||
LOG_ERROR("Failed to parse RemoteAction JSON payload");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
auto props_it = render->client_properties_.find(remote_id);
|
||||
if (props_it != render->client_properties_.end()) {
|
||||
render->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::service_command) {
|
||||
#if _WIN32
|
||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||
render->pending_windows_service_sas_.store(true,
|
||||
std::memory_order_relaxed);
|
||||
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
|
||||
if (!LockWorkStation()) {
|
||||
LOG_WARN("Remote lock workstation request failed, error={}",
|
||||
GetLastError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
@@ -620,6 +1118,68 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
}
|
||||
} else {
|
||||
// remote
|
||||
#if _WIN32
|
||||
if (render->local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
int absolute_x = 0;
|
||||
int absolute_y = 0;
|
||||
if (!BuildAbsoluteMousePosition(render->display_info_list_,
|
||||
render->selected_display_,
|
||||
remote_action.m.x, remote_action.m.y,
|
||||
&absolute_x, &absolute_y)) {
|
||||
LOG_WARN(
|
||||
"Secure desktop mouse injection skipped, invalid display "
|
||||
"mapping: display_index={}, x={}, y={}",
|
||||
render->selected_display_, remote_action.m.x, remote_action.m.y);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string response = SendCrossDeskSecureDesktopMouseInput(
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
LogSecureDesktopInputBlocked(
|
||||
&render->last_local_secure_input_block_log_tick_, "local",
|
||||
render->local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
||||
"flag={}, response={}",
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::keyboard) {
|
||||
const int key_code = static_cast<int>(remote_action.k.key_value);
|
||||
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended, 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, remote_action)) {
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return;
|
||||
}
|
||||
LogSecureDesktopInputBlocked(
|
||||
&render->last_local_secure_input_block_log_tick_, "local",
|
||||
render->local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop keyboard injection failed, key_code={}, "
|
||||
"is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||
render->mouse_controller_->SendMouseCommand(remote_action,
|
||||
render->selected_display_);
|
||||
@@ -632,7 +1192,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
render->keyboard_capturer_) {
|
||||
render->keyboard_capturer_->SendKeyboardCommand(
|
||||
(int)remote_action.k.key_value,
|
||||
remote_action.k.flag == KeyFlag::key_down);
|
||||
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended);
|
||||
} else if (remote_action.type == ControlType::display_id &&
|
||||
render->screen_capturer_) {
|
||||
render->selected_display_ = remote_action.d;
|
||||
@@ -713,6 +1274,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
render->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
@@ -765,6 +1327,9 @@ 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;
|
||||
}
|
||||
@@ -773,6 +1338,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
case ConnectionStatus::Closed: {
|
||||
props->connection_established_ = false;
|
||||
props->enable_mouse_control_ = false;
|
||||
render->ResetRemoteServiceStatus(*props);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
@@ -823,6 +1389,9 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
#if _WIN32
|
||||
render->last_windows_service_status_tick_ = 0;
|
||||
#endif
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
@@ -892,7 +1461,20 @@ 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;
|
||||
@@ -1056,4 +1638,4 @@ void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
props->net_traffic_stats_ = *net_traffic_stats;
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -15,6 +15,22 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
void ShowControlBarTooltip(const std::string& text) {
|
||||
if (!ImGui::IsItemHovered() || text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -162,7 +178,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
ImVec2 btn_min = ImGui::GetItemRectMin();
|
||||
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
|
||||
ShowControlBarTooltip(
|
||||
localization::select_display[localization_language_index_]);
|
||||
|
||||
props->display_selectable_hovered_ = false;
|
||||
if (ImGui::BeginPopup("display")) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < props->display_info_list_.size(); i++) {
|
||||
@@ -178,21 +197,66 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
|
||||
}
|
||||
props->display_selectable_hovered_ =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetWindowFontScale(0.35f);
|
||||
ImVec2 text_size = ImGui::CalcTextSize(
|
||||
std::to_string(props->selected_display_ + 1).c_str());
|
||||
ImVec2 text_pos =
|
||||
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f,
|
||||
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f);
|
||||
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
|
||||
btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
text_pos, IM_COL32(0, 0, 0, 255),
|
||||
std::to_string(props->selected_display_ + 1).c_str());
|
||||
|
||||
auto send_service_command = [&](ServiceCommandFlag flag,
|
||||
const char* log_action) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::service_command;
|
||||
remote_action.c.flag = flag;
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Send {} command failed, remote_id={}, ret={}", log_action,
|
||||
props->remote_id_, ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ImGui::SameLine();
|
||||
std::string shortcut = ICON_FA_KEYBOARD;
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
|
||||
ImGui::OpenPopup("shortcut");
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::send_shortcut[localization_language_index_]);
|
||||
|
||||
props->shortcut_selectable_hovered_ = false;
|
||||
if (ImGui::BeginPopup("shortcut")) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string sas_label = "Ctrl+Alt+Del";
|
||||
std::string lock_label = "Win+L";
|
||||
if (ImGui::Selectable(sas_label.c_str())) {
|
||||
send_service_command(ServiceCommandFlag::send_sas, "SAS");
|
||||
}
|
||||
if (ImGui::Selectable(lock_label.c_str())) {
|
||||
send_service_command(ServiceCommandFlag::lock_workstation,
|
||||
"remote lock");
|
||||
}
|
||||
props->shortcut_selectable_hovered_ =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
float mouse_x = ImGui::GetCursorScreenPos().x;
|
||||
float mouse_y = ImGui::GetCursorScreenPos().y;
|
||||
@@ -211,6 +275,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: localization::control_mouse[localization_language_index_];
|
||||
}
|
||||
}
|
||||
const bool mouse_button_hovered = ImGui::IsItemHovered();
|
||||
const std::string mouse_tooltip =
|
||||
props->enable_mouse_control_
|
||||
? localization::release_mouse[localization_language_index_]
|
||||
: localization::control_mouse[localization_language_index_];
|
||||
ShowControlBarTooltip(mouse_tooltip);
|
||||
|
||||
if (!props->enable_mouse_control_) {
|
||||
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
|
||||
@@ -223,8 +293,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(
|
||||
mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
mouse_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
}
|
||||
|
||||
@@ -256,6 +326,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
const bool audio_button_hovered = ImGui::IsItemHovered();
|
||||
const std::string audio_tooltip =
|
||||
props->audio_capture_button_pressed_
|
||||
? localization::mute[localization_language_index_]
|
||||
: localization::audio_capture[localization_language_index_];
|
||||
ShowControlBarTooltip(audio_tooltip);
|
||||
|
||||
if (!props->audio_capture_button_pressed_) {
|
||||
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
|
||||
@@ -268,8 +344,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(
|
||||
audio_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
audio_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
}
|
||||
|
||||
@@ -282,6 +358,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string path = OpenFileDialog(title);
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::select_file[localization_language_index_]);
|
||||
|
||||
ImGui::SameLine();
|
||||
// net traffic stats button
|
||||
@@ -306,6 +384,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: localization::show_net_traffic_stats
|
||||
[localization_language_index_];
|
||||
}
|
||||
const std::string net_traffic_stats_tooltip =
|
||||
props->net_traffic_stats_button_pressed_
|
||||
? localization::hide_net_traffic_stats[localization_language_index_]
|
||||
: localization::show_net_traffic_stats
|
||||
[localization_language_index_];
|
||||
ShowControlBarTooltip(net_traffic_stats_tooltip);
|
||||
|
||||
if (button_color_style_pushed) {
|
||||
ImGui::PopStyleColor();
|
||||
@@ -332,6 +416,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
}
|
||||
props->reset_control_bar_pos_ = true;
|
||||
}
|
||||
const std::string fullscreen_tooltip =
|
||||
fullscreen_button_pressed_
|
||||
? localization::exit_fullscreen[localization_language_index_]
|
||||
: localization::fullscreen[localization_language_index_];
|
||||
ShowControlBarTooltip(fullscreen_tooltip);
|
||||
|
||||
ImGui::SameLine();
|
||||
// close button
|
||||
@@ -341,6 +430,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(button_width, button_height))) {
|
||||
CleanupPeer(props);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::disconnect[localization_language_index_]);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -370,6 +461,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: ICON_FA_ANGLE_RIGHT)
|
||||
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
|
||||
: ICON_FA_ANGLE_LEFT);
|
||||
const std::string control_bar_tooltip =
|
||||
props->control_bar_expand_
|
||||
? localization::collapse_control_bar[localization_language_index_]
|
||||
: localization::expand_control_bar[localization_language_index_];
|
||||
if (ImGui::Button(control_bar.c_str(),
|
||||
ImVec2(button_height * 0.6f, button_height))) {
|
||||
props->control_bar_expand_ = !props->control_bar_expand_;
|
||||
@@ -381,6 +476,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->net_traffic_stats_button_pressed_ = false;
|
||||
}
|
||||
}
|
||||
ShowControlBarTooltip(control_bar_tooltip);
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
||||
NetTrafficStats(props);
|
||||
@@ -486,4 +582,4 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,7 +16,7 @@ int Render::StatusBar() {
|
||||
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_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleColor(2);
|
||||
@@ -45,4 +45,4 @@ int Render::StatusBar() {
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -51,7 +51,7 @@ int Render::TitleBar(bool main_window) {
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::BeginChild(main_window ? "MainTitleBar" : "StreamTitleBar",
|
||||
ImVec2(title_bar_width, title_bar_height_padding),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -329,4 +329,4 @@ int Render::TitleBar(bool main_window) {
|
||||
ImGui::EndChild();
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
GetCursorPos(&pt);
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING, 1001,
|
||||
localization::exit_program[language_index_]);
|
||||
localization::GetExitProgramLabel(language_index_));
|
||||
|
||||
SetForegroundWindow(hwnd_message_only_);
|
||||
int cmd =
|
||||
@@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -12,11 +12,44 @@
|
||||
|
||||
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::SetCursorPosX(window_width * 0.1f);
|
||||
ImGui::Text("%s", label.c_str());
|
||||
ImGui::TextUnformatted(label.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
@@ -27,35 +60,7 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
#if defined(_WIN32)
|
||||
int wide_len =
|
||||
MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
|
||||
if (wide_len > 0) {
|
||||
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)) {
|
||||
CloseHandle(process_info.hThread);
|
||||
CloseHandle(process_info.hProcess);
|
||||
}
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
std::string cmd = "open " + url;
|
||||
#else
|
||||
std::string cmd = "xdg-open " + url;
|
||||
#endif
|
||||
#if !defined(_WIN32)
|
||||
system(cmd.c_str()); // open browser
|
||||
#endif
|
||||
OpenUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +70,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.6f;
|
||||
: title_bar_button_width_ * 4.9f;
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(ImVec2(
|
||||
@@ -99,16 +104,23 @@ int Render::AboutWindow() {
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
|
||||
if (update_available_) {
|
||||
std::string latest_version =
|
||||
if (update_available_ && show_new_version_icon_in_menu_) {
|
||||
std::string new_version_available =
|
||||
localization::new_version_available[localization_language_index_] +
|
||||
": " + latest_version_;
|
||||
": ";
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", new_version_available.c_str());
|
||||
std::string access_website =
|
||||
localization::access_website[localization_language_index_];
|
||||
Hyperlink(latest_version, "https://crossdesk.cn", about_window_width);
|
||||
}
|
||||
ImGui::SetCursorPosX((about_window_width -
|
||||
ImGui::CalcTextSize(latest_version_.c_str()).x) /
|
||||
2.0f);
|
||||
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
|
||||
|
||||
ImGui::Text("");
|
||||
ImGui::Spacing();
|
||||
} else {
|
||||
ImGui::Text("%s", "");
|
||||
}
|
||||
|
||||
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
|
||||
std::string license_text = "Licensed under GNU LGPL v3.";
|
||||
@@ -118,7 +130,7 @@ int Render::AboutWindow() {
|
||||
ImGui::Text("%s", license_text.c_str());
|
||||
|
||||
ImGui::SetCursorPosX(about_window_width * 0.445f);
|
||||
ImGui::SetCursorPosY(about_window_height * 0.75f);
|
||||
ImGui::SetCursorPosY(about_window_height * 0.8f);
|
||||
// OK
|
||||
if (ImGui::Button(localization::ok[localization_language_index_].c_str())) {
|
||||
show_about_window_ = false;
|
||||
|
||||
@@ -224,7 +224,7 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImGui::BeginChild(
|
||||
control_child_window_title.c_str(),
|
||||
ImVec2(props->control_window_width_, props->control_window_height_),
|
||||
ImGuiChildFlags_Border, ImGuiWindowFlags_NoDecoration);
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_NoDecoration);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
props->control_window_pos_ = ImGui::GetWindowPos();
|
||||
|
||||
@@ -31,6 +31,7 @@ int BitrateDisplay(int bitrate) {
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
state->file_transfer_window_hovered_ = false;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
@@ -72,8 +73,6 @@ int Render::FileTransferWindow(
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Position window at bottom-left of stream window
|
||||
// Adjust window size based on number of files
|
||||
float file_transfer_window_width = main_window_width_ * 0.6f;
|
||||
@@ -82,15 +81,25 @@ int Render::FileTransferWindow(
|
||||
float pos_x = file_transfer_window_width * 0.05f;
|
||||
float pos_y = stream_window_height_ - file_transfer_window_height -
|
||||
file_transfer_window_width * 0.05;
|
||||
float same_line_width = file_transfer_window_width * 0.1f;
|
||||
|
||||
const ImVec2 mouse_pos = ImGui::GetMousePos();
|
||||
const bool mouse_in_window_rect =
|
||||
mouse_pos.x >= pos_x &&
|
||||
mouse_pos.x <= pos_x + file_transfer_window_width &&
|
||||
mouse_pos.y >= pos_y &&
|
||||
mouse_pos.y <= pos_y + file_transfer_window_height;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(file_transfer_window_width, file_transfer_window_height),
|
||||
ImGuiCond_Always);
|
||||
if (mouse_in_window_rect) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
}
|
||||
|
||||
// Set Chinese font for proper display
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
const bool has_chinese_font = stream_windows_system_chinese_font_ != nullptr;
|
||||
if (has_chinese_font) {
|
||||
ImGui::PushFont(stream_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
@@ -103,24 +112,27 @@ int Render::FileTransferWindow(
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
bool window_opened = true;
|
||||
if (ImGui::Begin(
|
||||
localization::file_transfer_progress[localization_language_index_]
|
||||
.c_str(),
|
||||
&window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar)) {
|
||||
const bool show_contents = ImGui::Begin(
|
||||
localization::file_transfer_progress[localization_language_index_]
|
||||
.c_str(),
|
||||
&window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
state->file_transfer_window_hovered_ =
|
||||
mouse_in_window_rect ||
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
|
||||
if (!window_opened) {
|
||||
state->file_transfer_window_visible_ = false;
|
||||
}
|
||||
|
||||
if (show_contents && window_opened) {
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
state->file_transfer_window_visible_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Display file list
|
||||
if (file_list.empty()) {
|
||||
@@ -130,7 +142,7 @@ int Render::FileTransferWindow(
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::BeginChild(
|
||||
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
|
||||
ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
|
||||
@@ -225,19 +237,15 @@ int Render::FileTransferWindow(
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Pop Chinese font if it was pushed
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
} else {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
if (has_chinese_font) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,9 +60,9 @@ int Render::SettingWindow() {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
{
|
||||
const char* language_items[] = {
|
||||
localization::language_zh[localization_language_index_].c_str(),
|
||||
localization::language_en[localization_language_index_].c_str()};
|
||||
const auto& supported_languages = localization::GetSupportedLanguages();
|
||||
language_button_value_ =
|
||||
localization::detail::ClampLanguageIndex(language_button_value_);
|
||||
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
@@ -77,13 +77,23 @@ int Render::SettingWindow() {
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
if (ImGui::BeginCombo("##language",
|
||||
language_items[language_button_value_])) {
|
||||
if (ImGui::BeginCombo(
|
||||
"##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name
|
||||
.c_str())) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < IM_ARRAYSIZE(language_items); i++) {
|
||||
for (int i = 0; i < static_cast<int>(supported_languages.size());
|
||||
++i) {
|
||||
bool selected = (i == language_button_value_);
|
||||
if (ImGui::Selectable(language_items[i], selected))
|
||||
if (ImGui::Selectable(
|
||||
supported_languages[i].display_name.c_str(), selected))
|
||||
language_button_value_ = i;
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
@@ -438,16 +448,24 @@ 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) {
|
||||
config_center_->SetLanguage(ConfigCenter::LANGUAGE::CHINESE);
|
||||
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
|
||||
} else if (language_button_value_ == 1) {
|
||||
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
config_center_->SetLanguage(ConfigCenter::LANGUAGE::ENGLISH);
|
||||
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
|
||||
}
|
||||
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_language_index_ == 0 ? "zh" : "en");
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
localization_language_index_)]
|
||||
.code
|
||||
.c_str());
|
||||
|
||||
// Video quality
|
||||
if (video_quality_button_value_ == 0) {
|
||||
@@ -602,4 +620,4 @@ int Render::SettingWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -19,7 +19,7 @@ int Render::MainWindow() {
|
||||
ImGui::BeginChild(
|
||||
"DeskWindow",
|
||||
ImVec2(local_remote_window_width, local_remote_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -57,4 +57,4 @@ int Render::MainWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "render.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "service_host.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length =
|
||||
GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length);
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool InstallServiceWithElevation() {
|
||||
const std::filesystem::path executable_path = GetCurrentExecutablePath();
|
||||
if (executable_path.empty()) {
|
||||
LOG_ERROR("Portable service install failed: current executable not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path service_path =
|
||||
executable_path.parent_path() / L"crossdesk_service.exe";
|
||||
const std::filesystem::path helper_path =
|
||||
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR(
|
||||
"Portable service install failed: service binaries missing, service={}, "
|
||||
"helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring executable = executable_path.wstring();
|
||||
std::wstring working_dir = executable_path.parent_path().wstring();
|
||||
std::wstring parameters = L"--service-install";
|
||||
|
||||
SHELLEXECUTEINFOW execute_info{};
|
||||
execute_info.cbSize = sizeof(execute_info);
|
||||
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
execute_info.hwnd = nullptr;
|
||||
execute_info.lpVerb = L"runas";
|
||||
execute_info.lpFile = executable.c_str();
|
||||
execute_info.lpParameters = parameters.c_str();
|
||||
execute_info.lpDirectory = working_dir.c_str();
|
||||
execute_info.nShow = SW_HIDE;
|
||||
|
||||
if (!ShellExecuteExW(&execute_info)) {
|
||||
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE);
|
||||
DWORD exit_code = 1;
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
GetExitCodeProcess(execute_info.hProcess, &exit_code);
|
||||
} else {
|
||||
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
|
||||
}
|
||||
CloseHandle(execute_info.hProcess);
|
||||
|
||||
if (exit_code != 0) {
|
||||
LOG_ERROR("Portable service install command failed, exit_code={}",
|
||||
exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool started = StartCrossDeskService();
|
||||
if (!started) {
|
||||
LOG_WARN("Portable service installed but start failed");
|
||||
}
|
||||
return IsCrossDeskServiceInstalled() && started;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Render::CheckPortableWindowsService() {
|
||||
if (portable_service_prompt_checked_) {
|
||||
return;
|
||||
}
|
||||
portable_service_prompt_checked_ = true;
|
||||
|
||||
if (IsCrossDeskServiceInstalled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void Render::StartPortableWindowsServiceInstall() {
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
std::memory_order_acq_rel)) {
|
||||
if (expected != PortableServiceInstallState::failed) {
|
||||
return;
|
||||
}
|
||||
portable_service_install_state_.store(
|
||||
PortableServiceInstallState::installing, std::memory_order_release);
|
||||
}
|
||||
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
portable_service_install_thread_ = std::thread([this]() {
|
||||
const bool installed = InstallServiceWithElevation();
|
||||
portable_service_install_state_.store(
|
||||
installed ? PortableServiceInstallState::succeeded
|
||||
: PortableServiceInstallState::failed,
|
||||
std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void Render::JoinPortableWindowsServiceInstallThread() {
|
||||
if (portable_service_install_thread_.joinable()) {
|
||||
portable_service_install_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
int Render::PortableServiceInstallWindow() {
|
||||
if (!show_portable_service_install_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
const float window_width = title_bar_button_width_ * 12.0f;
|
||||
const float window_height = title_bar_button_width_ * 4.0f;
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) /
|
||||
2.0f,
|
||||
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) /
|
||||
2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
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::Begin(
|
||||
localization::windows_service_setup_title[localization_language_index_]
|
||||
.c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::windows_service_setup_title[localization_language_index_]
|
||||
.c_str());
|
||||
|
||||
const PortableServiceInstallState state =
|
||||
portable_service_install_state_.load(std::memory_order_acquire);
|
||||
const char* status_text = nullptr;
|
||||
if (state == PortableServiceInstallState::installing ||
|
||||
state == PortableServiceInstallState::succeeded ||
|
||||
state == PortableServiceInstallState::failed) {
|
||||
status_text =
|
||||
localization::installing_windows_service[localization_language_index_]
|
||||
.c_str();
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
status_text =
|
||||
localization::windows_service_install_success
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
status_text =
|
||||
localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(0.45f);
|
||||
ImGui::SetCursorPosX(window_width * 0.04f);
|
||||
ImGui::SetCursorPosY(window_height * 0.22f);
|
||||
ImGui::BeginChild("PortableServiceInstallContent",
|
||||
ImVec2(window_width * 0.92f, window_height * 0.5f),
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float wrap_pos = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::PushTextWrapPos(wrap_pos);
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
localization::windows_service_setup_message[localization_language_index_]
|
||||
.c_str());
|
||||
if (status_text != nullptr) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", status_text);
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float button_y = window_height * 0.76f;
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const auto default_button_width = [&style](const std::string& label) {
|
||||
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
};
|
||||
const std::string install_label =
|
||||
localization::install_windows_service[localization_language_index_];
|
||||
const std::string cancel_label =
|
||||
localization::cancel[localization_language_index_];
|
||||
const std::string ok_label = localization::ok[localization_language_index_];
|
||||
const float buttons_width = state == PortableServiceInstallState::succeeded
|
||||
? default_button_width(ok_label)
|
||||
: default_button_width(install_label) +
|
||||
style.ItemSpacing.x +
|
||||
default_button_width(cancel_label);
|
||||
ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(button_y);
|
||||
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
if (ImGui::Button(ok_label.c_str())) {
|
||||
show_portable_service_install_window_ = false;
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
}
|
||||
} else {
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(install_label.c_str())) {
|
||||
StartPortableWindowsServiceInstall();
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(cancel_label.c_str())) {
|
||||
show_portable_service_install_window_ = false;
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -66,7 +66,7 @@ int Render::ServerWindow() {
|
||||
ImGui::BeginChild(
|
||||
"ServerTitleBar",
|
||||
ImVec2(server_window_width_, server_window_title_bar_height_),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
@@ -140,7 +140,7 @@ int Render::RemoteClientInfoWindow() {
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiChildFlags_Borders,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
@@ -376,4 +376,4 @@ int Render::RemoteClientInfoWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include "layout.h"
|
||||
@@ -93,7 +92,7 @@ int Render::UpdateNotificationWindow() {
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
std::string title =
|
||||
localization::new_version_available[localization_language_index_] +
|
||||
": v" + latest_version_;
|
||||
": " + latest_version_;
|
||||
ImGui::Text("%s", title.c_str());
|
||||
ImGui::SetWindowFontScale(0.1f);
|
||||
|
||||
@@ -104,6 +103,7 @@ 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_Border, ImGuiWindowFlags_None);
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
// set text wrap position to current available width (accounts for
|
||||
// scrollbar)
|
||||
@@ -184,14 +184,7 @@ int Render::UpdateNotificationWindow() {
|
||||
localization::update[localization_language_index_].c_str())) {
|
||||
// open download page
|
||||
std::string url = "https://crossdesk.cn";
|
||||
#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());
|
||||
OpenUrl(url);
|
||||
show_update_notification_window_ = false;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -62,4 +62,4 @@ std::shared_ptr<spdlog::logger> get_logger() {
|
||||
|
||||
return g_logger;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,12 +1,98 @@
|
||||
#include "path_manager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#ifndef CROSSDESK_PORTABLE
|
||||
#define CROSSDESK_PORTABLE 0
|
||||
#endif
|
||||
|
||||
#if CROSSDESK_PORTABLE
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#elif !defined(_WIN32)
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#if CROSSDESK_PORTABLE
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
#ifdef _WIN32
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length =
|
||||
GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length)
|
||||
.parent_path();
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t size = 0;
|
||||
_NSGetExecutablePath(nullptr, &size);
|
||||
std::vector<char> buffer(size + 1);
|
||||
if (_NSGetExecutablePath(buffer.data(), &size) != 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path executable =
|
||||
std::filesystem::weakly_canonical(buffer.data(), ec);
|
||||
if (ec) {
|
||||
executable = buffer.data();
|
||||
}
|
||||
return executable.parent_path();
|
||||
#else
|
||||
std::vector<char> buffer(PATH_MAX);
|
||||
while (true) {
|
||||
ssize_t length = readlink("/proc/self/exe", buffer.data(),
|
||||
buffer.size() - 1);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
if (static_cast<size_t>(length) < buffer.size() - 1) {
|
||||
buffer[static_cast<size_t>(length)] = '\0';
|
||||
return std::filesystem::path(buffer.data()).parent_path();
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path GetPortableRootPath() {
|
||||
std::filesystem::path executable_dir = GetExecutableDirectory();
|
||||
if (!executable_dir.empty()) {
|
||||
return executable_dir;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path current = std::filesystem::current_path(ec);
|
||||
return ec ? std::filesystem::path(".") : current;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PathManager::PathManager(const std::string& app_name) : app_name_(app_name) {}
|
||||
|
||||
std::filesystem::path PathManager::GetConfigPath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "data";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name_;
|
||||
#elif __APPLE__
|
||||
@@ -14,9 +100,13 @@ std::filesystem::path PathManager::GetConfigPath() {
|
||||
#else
|
||||
return GetEnvOrDefault("XDG_CONFIG_HOME", GetHome() + "/.config") / app_name_;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path PathManager::GetCachePath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "data";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
return "cache";
|
||||
@@ -28,9 +118,13 @@ std::filesystem::path PathManager::GetCachePath() {
|
||||
#else
|
||||
return GetEnvOrDefault("XDG_CACHE_HOME", GetHome() + "/.cache") / app_name_;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path PathManager::GetLogPath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "logs";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
return GetKnownFolder(FOLDERID_LocalAppData) / app_name_ / "logs";
|
||||
#elif __APPLE__
|
||||
@@ -38,6 +132,7 @@ std::filesystem::path PathManager::GetLogPath() {
|
||||
#else
|
||||
return GetCachePath() / "logs";
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PathManager::CreateDirectories(const std::filesystem::path& p) {
|
||||
@@ -93,4 +188,4 @@ std::filesystem::path PathManager::GetEnvOrDefault(const char* env_var,
|
||||
|
||||
return std::filesystem::path(def);
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
#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
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* @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
|
||||
@@ -0,0 +1,507 @@
|
||||
#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
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* @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
|
||||
@@ -0,0 +1,250 @@
|
||||
#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 <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#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;
|
||||
}
|
||||
|
||||
if (!EnsurePipeWireRuntimeAvailable()) {
|
||||
LOG_ERROR("Wayland screen capturer requires PipeWire 0.3 runtime");
|
||||
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;
|
||||
portal_has_logical_size_ = false;
|
||||
portal_stream_width_ = 0;
|
||||
portal_stream_height_ = 0;
|
||||
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
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* @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 EnsurePipeWireRuntimeAvailable() const;
|
||||
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;
|
||||
bool portal_has_logical_size_ = false;
|
||||
uint32_t spa_video_format_ = 0;
|
||||
int frame_width_ = 0;
|
||||
int frame_height_ = 0;
|
||||
int frame_stride_ = 0;
|
||||
int portal_stream_width_ = 0;
|
||||
int portal_stream_height_ = 0;
|
||||
int logical_width_ = 0;
|
||||
int logical_height_ = 0;
|
||||
|
||||
std::vector<uint8_t> y_plane_;
|
||||
std::vector<uint8_t> uv_plane_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* @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
|
||||
@@ -0,0 +1,868 @@
|
||||
#include "screen_capturer_wayland.h"
|
||||
#include "screen_capturer_wayland_build.h"
|
||||
|
||||
#if CROSSDESK_WAYLAND_BUILD_ENABLED
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
struct PipeWireDynamicApi {
|
||||
void* library = nullptr;
|
||||
bool available = false;
|
||||
|
||||
decltype(&::pw_init) init = nullptr;
|
||||
decltype(&::pw_deinit) deinit = nullptr;
|
||||
decltype(&::pw_thread_loop_new) thread_loop_new = nullptr;
|
||||
decltype(&::pw_thread_loop_destroy) thread_loop_destroy = nullptr;
|
||||
decltype(&::pw_thread_loop_get_loop) thread_loop_get_loop = nullptr;
|
||||
decltype(&::pw_thread_loop_start) thread_loop_start = nullptr;
|
||||
decltype(&::pw_thread_loop_stop) thread_loop_stop = nullptr;
|
||||
decltype(&::pw_thread_loop_lock) thread_loop_lock = nullptr;
|
||||
decltype(&::pw_thread_loop_unlock) thread_loop_unlock = nullptr;
|
||||
decltype(&::pw_thread_loop_wait) thread_loop_wait = nullptr;
|
||||
decltype(&::pw_thread_loop_signal) thread_loop_signal = nullptr;
|
||||
decltype(&::pw_context_new) context_new = nullptr;
|
||||
decltype(&::pw_context_destroy) context_destroy = nullptr;
|
||||
decltype(&::pw_context_connect_fd) context_connect_fd = nullptr;
|
||||
decltype(&::pw_properties_new) properties_new = nullptr;
|
||||
decltype(&::pw_properties_set) properties_set = nullptr;
|
||||
decltype(&::pw_stream_new) stream_new = nullptr;
|
||||
decltype(&::pw_stream_add_listener) stream_add_listener = nullptr;
|
||||
decltype(&::pw_stream_state_as_string) stream_state_as_string = nullptr;
|
||||
decltype(&::pw_stream_connect) stream_connect = nullptr;
|
||||
decltype(&::pw_stream_update_params) stream_update_params = nullptr;
|
||||
decltype(&::pw_stream_set_active) stream_set_active = nullptr;
|
||||
decltype(&::pw_stream_disconnect) stream_disconnect = nullptr;
|
||||
decltype(&::pw_stream_destroy) stream_destroy = nullptr;
|
||||
decltype(&::pw_stream_dequeue_buffer) stream_dequeue_buffer = nullptr;
|
||||
decltype(&::pw_stream_queue_buffer) stream_queue_buffer = nullptr;
|
||||
decltype(&::pw_core_disconnect) core_disconnect = nullptr;
|
||||
decltype(&::pw_proxy_destroy) proxy_destroy = nullptr;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
bool LoadPipeWireSymbol(void* library, T* function, const char* symbol_name) {
|
||||
*function = reinterpret_cast<T>(dlsym(library, symbol_name));
|
||||
if (*function != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR("Unable to find PipeWire symbol {}", symbol_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
void UnloadPipeWireApi(PipeWireDynamicApi* api) {
|
||||
if (api->library != nullptr) {
|
||||
dlclose(api->library);
|
||||
}
|
||||
*api = PipeWireDynamicApi{};
|
||||
}
|
||||
|
||||
bool LoadPipeWireApi(PipeWireDynamicApi* api) {
|
||||
static constexpr const char* kPipeWireLibraries[] = {
|
||||
"libpipewire-0.3.so.0",
|
||||
"libpipewire-0.3.so",
|
||||
};
|
||||
|
||||
for (const char* library_name : kPipeWireLibraries) {
|
||||
api->library = dlopen(library_name, RTLD_LAZY | RTLD_LOCAL);
|
||||
if (api->library != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (api->library == nullptr) {
|
||||
LOG_WARN("PipeWire 0.3 runtime library is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadPipeWireSymbol(api->library, &api->init, "pw_init") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->deinit, "pw_deinit") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_new,
|
||||
"pw_thread_loop_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_destroy,
|
||||
"pw_thread_loop_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_get_loop,
|
||||
"pw_thread_loop_get_loop") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_start,
|
||||
"pw_thread_loop_start") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_stop,
|
||||
"pw_thread_loop_stop") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_lock,
|
||||
"pw_thread_loop_lock") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_unlock,
|
||||
"pw_thread_loop_unlock") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_wait,
|
||||
"pw_thread_loop_wait") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_signal,
|
||||
"pw_thread_loop_signal") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_new, "pw_context_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_destroy,
|
||||
"pw_context_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_connect_fd,
|
||||
"pw_context_connect_fd") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->properties_new,
|
||||
"pw_properties_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->properties_set,
|
||||
"pw_properties_set") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_new, "pw_stream_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_add_listener,
|
||||
"pw_stream_add_listener") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_state_as_string,
|
||||
"pw_stream_state_as_string") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_connect,
|
||||
"pw_stream_connect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_update_params,
|
||||
"pw_stream_update_params") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_set_active,
|
||||
"pw_stream_set_active") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_disconnect,
|
||||
"pw_stream_disconnect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_destroy,
|
||||
"pw_stream_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_dequeue_buffer,
|
||||
"pw_stream_dequeue_buffer") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_queue_buffer,
|
||||
"pw_stream_queue_buffer") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->core_disconnect,
|
||||
"pw_core_disconnect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->proxy_destroy,
|
||||
"pw_proxy_destroy")) {
|
||||
UnloadPipeWireApi(api);
|
||||
return false;
|
||||
}
|
||||
|
||||
api->available = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const PipeWireDynamicApi* GetPipeWireApi() {
|
||||
static PipeWireDynamicApi api;
|
||||
static std::once_flag once;
|
||||
std::call_once(once, []() { LoadPipeWireApi(&api); });
|
||||
return api.available ? &api : nullptr;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
double SnapLikelyFractionalScale(double observed_scale) {
|
||||
static constexpr double kCandidates[] = {
|
||||
1.0, 1.25, 1.3333333333, 1.5, 1.6666666667, 1.75, 2.0, 2.25, 2.5, 3.0};
|
||||
double best = observed_scale;
|
||||
double best_error = std::numeric_limits<double>::max();
|
||||
for (double candidate : kCandidates) {
|
||||
const double error = std::abs(candidate - observed_scale);
|
||||
if (error < best_error) {
|
||||
best = candidate;
|
||||
best_error = error;
|
||||
}
|
||||
}
|
||||
|
||||
return best_error <= 0.08 ? best : observed_scale;
|
||||
}
|
||||
|
||||
struct PipeWireTargetLookupState {
|
||||
const PipeWireDynamicApi* pipewire = nullptr;
|
||||
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) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pipewire || !core || !loop || node_id == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
PipeWireTargetLookupState state;
|
||||
state.pipewire = pipewire;
|
||||
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;
|
||||
state->pipewire->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;
|
||||
state->pipewire->thread_loop_signal(state->loop, false);
|
||||
};
|
||||
|
||||
pw_registry_add_listener(registry, ®istry_listener, ®istry_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) {
|
||||
pipewire->thread_loop_wait(loop);
|
||||
}
|
||||
|
||||
spa_hook_remove(®istry_listener);
|
||||
spa_hook_remove(&core_listener);
|
||||
pipewire->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::EnsurePipeWireRuntimeAvailable() const {
|
||||
return GetPipeWireApi() != nullptr;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
PipeWireConnectMode mode) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pipewire) {
|
||||
LOG_ERROR("PipeWire 0.3 runtime library is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pipewire_fd_ < 0 || pipewire_node_id_ == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pipewire_initialized_) {
|
||||
pipewire->init(nullptr, nullptr);
|
||||
pipewire_initialized_ = true;
|
||||
}
|
||||
|
||||
pw_thread_loop_ =
|
||||
pipewire->thread_loop_new("crossdesk-wayland-capture", nullptr);
|
||||
if (!pw_thread_loop_) {
|
||||
LOG_ERROR("Failed to create PipeWire thread loop");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pipewire->thread_loop_start(pw_thread_loop_) < 0) {
|
||||
LOG_ERROR("Failed to start PipeWire thread loop");
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
pipewire_thread_loop_started_ = true;
|
||||
|
||||
pipewire->thread_loop_lock(pw_thread_loop_);
|
||||
|
||||
pw_context_ = pipewire->context_new(
|
||||
pipewire->thread_loop_get_loop(pw_thread_loop_), nullptr, 0);
|
||||
if (!pw_context_) {
|
||||
LOG_ERROR("Failed to create PipeWire context");
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
|
||||
pw_core_ =
|
||||
pipewire->context_connect_fd(pw_context_, pipewire_fd_, nullptr, 0);
|
||||
if (!pw_core_) {
|
||||
LOG_ERROR("Failed to connect to PipeWire remote");
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
pipewire_fd_ = -1;
|
||||
|
||||
pw_properties* stream_props = pipewire->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");
|
||||
pipewire->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()) {
|
||||
pipewire->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_ =
|
||||
pipewire->stream_new(pw_core_, "CrossDesk Wayland Capture", stream_props);
|
||||
if (!pw_stream_) {
|
||||
LOG_ERROR("Failed to create PipeWire stream");
|
||||
pipewire->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;
|
||||
}
|
||||
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
LOG_INFO(
|
||||
"PipeWire stream state: {} -> {}",
|
||||
pipewire ? pipewire->stream_state_as_string(old_state) : "unknown",
|
||||
pipewire ? pipewire->stream_state_as_string(state) : "unknown");
|
||||
};
|
||||
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_) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (pipewire) {
|
||||
pipewire->stream_update_params(self->pw_stream_, params, param_count);
|
||||
}
|
||||
}
|
||||
self->pipewire_format_ready_.store(true);
|
||||
|
||||
int pointer_width =
|
||||
self->logical_width_ > 0 ? self->logical_width_ : self->frame_width_;
|
||||
int pointer_height = self->logical_height_ > 0 ? self->logical_height_
|
||||
: self->frame_height_;
|
||||
double observed_scale_x = pointer_width > 0
|
||||
? static_cast<double>(self->frame_width_) /
|
||||
static_cast<double>(pointer_width)
|
||||
: 1.0;
|
||||
double observed_scale_y = pointer_height > 0
|
||||
? static_cast<double>(self->frame_height_) /
|
||||
static_cast<double>(pointer_height)
|
||||
: 1.0;
|
||||
double snapped_scale = 1.0;
|
||||
bool derived_pointer_space = false;
|
||||
|
||||
if (!self->portal_has_logical_size_ && self->portal_stream_width_ > 0 &&
|
||||
self->portal_stream_height_ > 0 && self->frame_width_ > 0 &&
|
||||
self->frame_height_ > 0) {
|
||||
const double raw_scale_x =
|
||||
static_cast<double>(self->frame_width_) /
|
||||
static_cast<double>(self->portal_stream_width_);
|
||||
const double raw_scale_y =
|
||||
static_cast<double>(self->frame_height_) /
|
||||
static_cast<double>(self->portal_stream_height_);
|
||||
const double average_scale = (raw_scale_x + raw_scale_y) * 0.5;
|
||||
snapped_scale = SnapLikelyFractionalScale(average_scale);
|
||||
|
||||
const bool scales_are_consistent =
|
||||
std::abs(raw_scale_x - raw_scale_y) <= 0.05;
|
||||
const bool scale_was_snapped =
|
||||
std::abs(snapped_scale - average_scale) <= 0.08;
|
||||
if (scales_are_consistent && scale_was_snapped &&
|
||||
snapped_scale > 1.05) {
|
||||
pointer_width =
|
||||
std::max(1, static_cast<int>(std::floor(
|
||||
static_cast<double>(self->portal_stream_width_) *
|
||||
snapped_scale +
|
||||
1e-6)));
|
||||
pointer_height =
|
||||
std::max(1, static_cast<int>(std::floor(
|
||||
static_cast<double>(self->portal_stream_height_) *
|
||||
snapped_scale +
|
||||
1e-6)));
|
||||
observed_scale_x = pointer_width > 0
|
||||
? static_cast<double>(self->frame_width_) /
|
||||
static_cast<double>(pointer_width)
|
||||
: 1.0;
|
||||
observed_scale_y = pointer_height > 0
|
||||
? static_cast<double>(self->frame_height_) /
|
||||
static_cast<double>(pointer_height)
|
||||
: 1.0;
|
||||
derived_pointer_space = true;
|
||||
}
|
||||
}
|
||||
|
||||
self->UpdateDisplayGeometry(pointer_width, pointer_height);
|
||||
if (derived_pointer_space) {
|
||||
LOG_INFO(
|
||||
"PipeWire video format: {}, {}x{} stride={} (pointer space {}x{}, "
|
||||
"derived from portal stream {}x{} with compositor scale {:.4f}, "
|
||||
"effective scale {:.4f}x{:.4f})",
|
||||
PipeWireFormatName(self->spa_video_format_), self->frame_width_,
|
||||
self->frame_height_, self->frame_stride_, pointer_width,
|
||||
pointer_height, self->portal_stream_width_,
|
||||
self->portal_stream_height_, snapped_scale, observed_scale_x,
|
||||
observed_scale_y);
|
||||
} else {
|
||||
LOG_INFO(
|
||||
"PipeWire video format: {}, {}x{} stride={} (pointer space {}x{}, "
|
||||
"scale {:.4f}x{:.4f})",
|
||||
PipeWireFormatName(self->spa_video_format_), self->frame_width_,
|
||||
self->frame_height_, self->frame_stride_, pointer_width,
|
||||
pointer_height, observed_scale_x, observed_scale_y);
|
||||
}
|
||||
};
|
||||
events.process = [](void* userdata) {
|
||||
auto* self = static_cast<ScreenCapturerWayland*>(userdata);
|
||||
if (self) {
|
||||
self->HandlePipeWireBuffer();
|
||||
}
|
||||
};
|
||||
return events;
|
||||
}();
|
||||
|
||||
pipewire->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");
|
||||
pipewire->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 = pipewire->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));
|
||||
pipewire->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 PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
const bool need_lock =
|
||||
pipewire && pw_thread_loop_ &&
|
||||
(pw_stream_ != nullptr || pw_core_ != nullptr || pw_context_ != nullptr);
|
||||
if (need_lock) {
|
||||
pipewire->thread_loop_lock(pw_thread_loop_);
|
||||
}
|
||||
|
||||
if (pw_stream_ && pipewire) {
|
||||
pipewire->stream_set_active(pw_stream_, false);
|
||||
pipewire->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_ && pipewire) {
|
||||
pipewire->stream_destroy(pw_stream_);
|
||||
}
|
||||
pw_stream_ = nullptr;
|
||||
|
||||
if (pw_core_ && pipewire) {
|
||||
pipewire->core_disconnect(pw_core_);
|
||||
}
|
||||
pw_core_ = nullptr;
|
||||
|
||||
if (pw_context_ && pipewire) {
|
||||
pipewire->context_destroy(pw_context_);
|
||||
}
|
||||
pw_context_ = nullptr;
|
||||
|
||||
if (need_lock) {
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
}
|
||||
|
||||
if (pw_thread_loop_ && pipewire) {
|
||||
if (pipewire_thread_loop_started_) {
|
||||
pipewire->thread_loop_stop(pw_thread_loop_);
|
||||
pipewire_thread_loop_started_ = false;
|
||||
}
|
||||
pipewire->thread_loop_destroy(pw_thread_loop_);
|
||||
}
|
||||
pw_thread_loop_ = nullptr;
|
||||
pipewire_thread_loop_started_ = false;
|
||||
|
||||
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_ && pipewire) {
|
||||
pipewire->deinit();
|
||||
}
|
||||
pipewire_initialized_ = false;
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pw_stream_ || !pipewire) {
|
||||
return;
|
||||
}
|
||||
|
||||
pw_buffer* buffer = pipewire->stream_dequeue_buffer(pw_stream_);
|
||||
if (!buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto requeue = [&]() { pipewire->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
|
||||
@@ -0,0 +1,824 @@
|
||||
#include "screen_capturer_wayland.h"
|
||||
#include "screen_capturer_wayland_build.h"
|
||||
#include "wayland_portal_shared.h"
|
||||
|
||||
#if CROSSDESK_WAYLAND_BUILD_ENABLED
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#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);
|
||||
|
||||
portal_stream_width_ = stream_width;
|
||||
portal_stream_height_ = stream_height;
|
||||
portal_has_logical_size_ =
|
||||
logical_width > 0 && logical_height > 0;
|
||||
|
||||
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
|
||||
@@ -5,7 +5,9 @@
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "libyuv.h"
|
||||
@@ -13,11 +15,58 @@
|
||||
|
||||
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");
|
||||
@@ -29,6 +78,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -71,8 +121,15 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) {
|
||||
width_ = attr.width;
|
||||
height_ = attr.height;
|
||||
|
||||
if (width_ % 2 != 0 || height_ % 2 != 0) {
|
||||
LOG_ERROR("Width and height must be even numbers");
|
||||
if ((width_ & 1) != 0 || (height_ & 1) != 0) {
|
||||
LOG_WARN("X11 root size {}x{} is not even, aligning down to {}x{} for NV12",
|
||||
width_, height_, width_ & ~1, height_ & ~1);
|
||||
width_ &= ~1;
|
||||
height_ &= ~1;
|
||||
}
|
||||
|
||||
if (width_ <= 1 || height_ <= 1) {
|
||||
LOG_ERROR("Invalid capture size after alignment: {}x{}", width_, height_);
|
||||
return -2;
|
||||
}
|
||||
|
||||
@@ -82,6 +139,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -108,9 +170,24 @@ 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_) {
|
||||
if (!paused_) OnFrame();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
return 0;
|
||||
@@ -152,19 +229,44 @@ void ScreenCapturerX11::OnFrame() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor_index_ < 0 || monitor_index_ >= display_info_list_.size()) {
|
||||
LOG_ERROR("Invalid monitor index: {}", monitor_index_.load());
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
XImage* image = XGetImage(display_, root_, left_, top_, width_, height_,
|
||||
AllPlanes, ZPixmap);
|
||||
if (!image) return;
|
||||
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;
|
||||
|
||||
// if enable show cursor, draw cursor
|
||||
if (show_cursor_) {
|
||||
@@ -180,23 +282,41 @@ void ScreenCapturerX11::OnFrame() {
|
||||
}
|
||||
}
|
||||
|
||||
bool needs_copy = image->bytes_per_line != width_ * 4;
|
||||
std::vector<uint8_t> argb_buf;
|
||||
uint8_t* src_argb = nullptr;
|
||||
|
||||
if (needs_copy) {
|
||||
argb_buf.resize(width_ * height_ * 4);
|
||||
for (int y = 0; y < height_; ++y) {
|
||||
memcpy(&argb_buf[y * width_ * 4], image->data + y * image->bytes_per_line,
|
||||
width_ * 4);
|
||||
}
|
||||
src_argb = argb_buf.data();
|
||||
} else {
|
||||
src_argb = reinterpret_cast<uint8_t*>(image->data);
|
||||
if (image->bits_per_pixel != 32 || image->bytes_per_line <= 0) {
|
||||
LOG_WARN(
|
||||
"Unsupported X11 image layout: bits_per_pixel={}, bytes_per_line={}",
|
||||
image->bits_per_pixel, image->bytes_per_line);
|
||||
XDestroyImage(image);
|
||||
return;
|
||||
}
|
||||
|
||||
libyuv::ARGBToNV12(src_argb, width_ * 4, y_plane_.data(), width_,
|
||||
uv_plane_.data(), width_, width_, height_);
|
||||
const uint8_t* src_argb = reinterpret_cast<const uint8_t*>(image->data);
|
||||
const int src_stride_argb = image->bytes_per_line;
|
||||
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(width_) * static_cast<size_t>(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 =
|
||||
use_abgr_to_nv12_
|
||||
? libyuv::ABGRToNV12(src_argb, src_stride_argb, y_plane_.data(),
|
||||
width_, uv_plane_.data(), width_, width_,
|
||||
height_)
|
||||
: libyuv::ARGBToNV12(src_argb, src_stride_argb, y_plane_.data(),
|
||||
width_, uv_plane_.data(), width_, width_,
|
||||
height_);
|
||||
if (convert_ret != 0) {
|
||||
LOG_WARN("X11 {} failed: {}",
|
||||
use_abgr_to_nv12_ ? "ABGRToNV12" : "ARGBToNV12", convert_ret);
|
||||
XDestroyImage(image);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> nv12;
|
||||
nv12.reserve(y_plane_.size() + uv_plane_.size());
|
||||
@@ -205,7 +325,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);
|
||||
@@ -288,4 +408,34 @@ void ScreenCapturerX11::DrawCursor(XImage* image, int x, int y) {
|
||||
|
||||
XFree(cursor_image);
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
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 (x11_error != 0 || !probe_image) {
|
||||
LOG_WARN("X11 probe XGetImage failed: x11_error={}, image={}", x11_error,
|
||||
probe_image ? "valid" : "null");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool red_in_low_byte = (probe_image->red_mask & 0x000000FFu) != 0;
|
||||
const bool blue_in_low_byte = (probe_image->blue_mask & 0x000000FFu) != 0;
|
||||
use_abgr_to_nv12_ = red_in_low_byte && !blue_in_low_byte;
|
||||
|
||||
XDestroyImage(probe_image);
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -17,6 +17,7 @@ struct _XImage;
|
||||
typedef struct _XImage XImage;
|
||||
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
@@ -50,6 +51,7 @@ class ScreenCapturerX11 : public ScreenCapturer {
|
||||
|
||||
private:
|
||||
void DrawCursor(XImage* image, int x, int y);
|
||||
bool ProbeCapture();
|
||||
|
||||
private:
|
||||
Display* display_ = nullptr;
|
||||
@@ -68,9 +70,11 @@ class ScreenCapturerX11 : public ScreenCapturer {
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
int capture_error_count_ = 0;
|
||||
bool use_abgr_to_nv12_ = false;
|
||||
|
||||
std::vector<uint8_t> y_plane_;
|
||||
std::vector<uint8_t> uv_plane_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#ifdef _WIN32
|
||||
#include "screen_capturer_win.h"
|
||||
#elif __linux__
|
||||
#include "screen_capturer_x11.h"
|
||||
#include "screen_capturer_linux.h"
|
||||
#elif __APPLE__
|
||||
// #include "screen_capturer_avf.h"
|
||||
#include "screen_capturer_sck.h"
|
||||
@@ -27,7 +27,7 @@ class ScreenCapturerFactory {
|
||||
#ifdef _WIN32
|
||||
return new ScreenCapturerWin();
|
||||
#elif __linux__
|
||||
return new ScreenCapturerX11();
|
||||
return new ScreenCapturerLinux();
|
||||
#elif __APPLE__
|
||||
// return new ScreenCapturerAvf();
|
||||
return new ScreenCapturerSck();
|
||||
|
||||
@@ -244,6 +244,31 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
ReleaseDuplication();
|
||||
int current_monitor = monitor_index_.load();
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
EnumerateDisplays();
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("DXGI: no displays found while recreating duplication");
|
||||
return false;
|
||||
}
|
||||
if (current_monitor < 0 ||
|
||||
current_monitor >= static_cast<int>(display_info_list_.size())) {
|
||||
current_monitor = 0;
|
||||
monitor_index_ = 0;
|
||||
}
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
LOG_INFO("DXGI: recreated duplication for monitor {}",
|
||||
monitor_index_.load());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
@@ -254,6 +279,8 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
|
||||
void ScreenCapturerDxgi::CaptureLoop() {
|
||||
const int timeout_ms = 33;
|
||||
auto last_duplication_retry =
|
||||
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
@@ -261,6 +288,11 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
|
||||
if (!duplication_) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
|
||||
last_duplication_retry = now;
|
||||
RecreateDuplicationForCurrentMonitor();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
@@ -274,9 +306,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
// attempt to recreate duplication
|
||||
ReleaseDuplication();
|
||||
CreateDuplicationForMonitor(monitor_index_);
|
||||
RecreateDuplicationForCurrentMonitor();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -353,4 +383,4 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -50,6 +50,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
bool InitializeDxgi();
|
||||
void EnumerateDisplays();
|
||||
bool CreateDuplicationForMonitor(int monitor_index);
|
||||
bool RecreateDuplicationForCurrentMonitor();
|
||||
void CaptureLoop();
|
||||
void ReleaseDuplication();
|
||||
|
||||
@@ -78,4 +79,4 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -100,8 +100,7 @@ bool ScreenCapturerWgc::IsWgcSupported() {
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
int error = 0;
|
||||
if (inited_ == true) return error;
|
||||
if (inited_ == true) return 0;
|
||||
|
||||
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
|
||||
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
|
||||
@@ -112,8 +111,18 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
if (!IsWgcSupported()) {
|
||||
LOG_ERROR("WGC not supported");
|
||||
error = 2;
|
||||
return error;
|
||||
return 2;
|
||||
}
|
||||
|
||||
return RebuildSessions(monitor_index_);
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
|
||||
CleanUp();
|
||||
|
||||
if (!IsWgcSupported()) {
|
||||
LOG_ERROR("WGC not supported");
|
||||
return 2;
|
||||
}
|
||||
|
||||
monitor_ = GetPrimaryMonitor();
|
||||
@@ -125,6 +134,13 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (preferred_monitor_index < 0 ||
|
||||
preferred_monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
preferred_monitor_index = 0;
|
||||
}
|
||||
monitor_index_ = preferred_monitor_index;
|
||||
|
||||
int error = 0;
|
||||
for (int i = 0; i < display_info_list_.size(); i++) {
|
||||
const auto& display = display_info_list_[i];
|
||||
LOG_INFO(
|
||||
@@ -138,20 +154,28 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
sessions_.back().session_->RegisterObserver(this);
|
||||
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
|
||||
if (error != 0) {
|
||||
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
|
||||
CleanUp();
|
||||
return error;
|
||||
}
|
||||
sessions_[i].inited_ = true;
|
||||
inited_ = true;
|
||||
}
|
||||
|
||||
LOG_INFO("Default on monitor {}:{}", monitor_index_,
|
||||
display_info_list_[monitor_index_].name);
|
||||
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
if (initial_monitor_index_ < 0 ||
|
||||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
}
|
||||
inited_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Destroy() { return 0; }
|
||||
int ScreenCapturerWgc::Destroy() {
|
||||
CleanUp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
if (running_ == true) {
|
||||
@@ -160,13 +184,37 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
if (inited_ == false) {
|
||||
LOG_ERROR("Screen capturer not inited");
|
||||
return 4;
|
||||
const int ret = RebuildSessions(monitor_index_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Screen capturer not inited");
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = StartSessions(show_cursor);
|
||||
if (ret == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("WGC: start failed, rebuilding sessions");
|
||||
ret = RebuildSessions(monitor_index_);
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
return StartSessions(show_cursor);
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::StartSessions(bool show_cursor) {
|
||||
bool any_started = false;
|
||||
bool active_started = false;
|
||||
int last_error = 0;
|
||||
for (int i = 0; i < sessions_.size(); i++) {
|
||||
int active_monitor = monitor_index_;
|
||||
if (active_monitor < 0 ||
|
||||
active_monitor >= static_cast<int>(sessions_.size())) {
|
||||
active_monitor = 0;
|
||||
monitor_index_ = 0;
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(sessions_.size()); i++) {
|
||||
if (sessions_[i].inited_ == false) {
|
||||
LOG_ERROR("Session {} not inited", i);
|
||||
continue;
|
||||
@@ -182,16 +230,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i != 0) {
|
||||
if (i != active_monitor) {
|
||||
sessions_[i].session_->Pause();
|
||||
sessions_[i].paused_ = true;
|
||||
} else {
|
||||
sessions_[i].session_->Resume();
|
||||
sessions_[i].paused_ = false;
|
||||
}
|
||||
sessions_[i].running_ = true;
|
||||
any_started = true;
|
||||
if (i == active_monitor) {
|
||||
active_started = true;
|
||||
}
|
||||
}
|
||||
running_ = running_ || any_started;
|
||||
}
|
||||
|
||||
running_ = active_started;
|
||||
if (!active_started) {
|
||||
LOG_ERROR("WGC: active session did not start successfully");
|
||||
Stop();
|
||||
return last_error != 0 ? last_error : -1;
|
||||
}
|
||||
if (!any_started) {
|
||||
LOG_ERROR("WGC: no session started successfully");
|
||||
return last_error != 0 ? last_error : -1;
|
||||
@@ -349,13 +408,16 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
}
|
||||
|
||||
void ScreenCapturerWgc::CleanUp() {
|
||||
if (inited_) {
|
||||
for (auto& session : sessions_) {
|
||||
if (session.session_) {
|
||||
session.session_->Stop();
|
||||
}
|
||||
running_ = false;
|
||||
for (auto& session : sessions_) {
|
||||
if (session.session_) {
|
||||
session.session_->Stop();
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
sessions_.clear();
|
||||
display_info_list_.clear();
|
||||
gs_display_list.clear();
|
||||
monitor_ = nullptr;
|
||||
inited_ = false;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -40,6 +40,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
|
||||
protected:
|
||||
void CleanUp();
|
||||
int RebuildSessions(int preferred_monitor_index);
|
||||
int StartSessions(bool show_cursor);
|
||||
|
||||
private:
|
||||
HMONITOR monitor_;
|
||||
@@ -74,4 +76,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
std::mutex frame_mutex_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,13 @@
|
||||
#ifndef _SCREEN_CAPTURER_WIN_H_
|
||||
#define _SCREEN_CAPTURER_WIN_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
@@ -38,6 +43,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
bool impl_is_wgc_plugin_ = false;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data cb_;
|
||||
cb_desktop_data cb_orig_;
|
||||
@@ -47,9 +53,50 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::mutex alias_mutex_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
std::unordered_set<std::string> canonical_labels_;
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
|
||||
std::atomic<bool> post_secure_desktop_drop_logged_{false};
|
||||
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
|
||||
std::thread secure_capture_thread_;
|
||||
HANDLE secure_frame_mapping_ = nullptr;
|
||||
HANDLE secure_frame_ready_event_ = nullptr;
|
||||
uint8_t* secure_frame_view_ = nullptr;
|
||||
size_t secure_frame_view_size_ = 0;
|
||||
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
int secure_shared_left_ = 0;
|
||||
int secure_shared_top_ = 0;
|
||||
int secure_shared_width_ = 0;
|
||||
int secure_shared_height_ = 0;
|
||||
int secure_shared_fps_ = 0;
|
||||
bool secure_shared_show_cursor_ = true;
|
||||
std::string secure_shared_stage_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void SecureDesktopCaptureLoop();
|
||||
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
|
||||
std::string* display_name);
|
||||
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
|
||||
int width, int height,
|
||||
const std::string& stage,
|
||||
bool show_cursor, int fps,
|
||||
std::string* error_out);
|
||||
void StopSecureDesktopSharedCapture(DWORD session_id);
|
||||
bool OpenSecureDesktopSharedFrame(DWORD session_id, size_t min_size,
|
||||
std::string* error_out);
|
||||
bool ReadSecureDesktopSharedFrame(DWORD wait_ms,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* width_out, int* height_out,
|
||||
std::string* error_out);
|
||||
void CloseSecureDesktopSharedFrame();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* @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
|
||||
@@ -0,0 +1,30 @@
|
||||
#include <mutex>
|
||||
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_wgc.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void InitializePluginLogger() {
|
||||
static std::once_flag once;
|
||||
std::call_once(once, []() {
|
||||
crossdesk::PathManager path_manager("CrossDesk");
|
||||
crossdesk::InitLogger(path_manager.GetLogPath().string());
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
crossdesk::ScreenCapturer* CrossDeskCreateWgcCapturer() {
|
||||
InitializePluginLogger();
|
||||
return new crossdesk::ScreenCapturerWgc();
|
||||
}
|
||||
|
||||
void CrossDeskDestroyWgcCapturer(crossdesk::ScreenCapturer* capturer) {
|
||||
delete capturer;
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,10 @@
|
||||
|
||||
#include <Windows.Graphics.Capture.Interop.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#define CHECK_INIT \
|
||||
if (!is_initialized_) { \
|
||||
std::cout << "AE_NEED_INIT" << std::endl; \
|
||||
return 4; \
|
||||
}
|
||||
|
||||
#define CHECK_CLOSED \
|
||||
if (cleaned_.load() == true) { \
|
||||
throw winrt::hresult_error(RO_E_CLOSED); \
|
||||
}
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern "C" {
|
||||
@@ -41,7 +27,7 @@ int WgcSessionImpl::Initialize(HWND hwnd) {
|
||||
|
||||
target_.hwnd = hwnd;
|
||||
target_.is_window = true;
|
||||
return Initialize();
|
||||
return InitializeLocked();
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
||||
@@ -49,7 +35,7 @@ int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
||||
|
||||
target_.hmonitor = hmonitor;
|
||||
target_.is_window = false;
|
||||
return Initialize();
|
||||
return InitializeLocked();
|
||||
}
|
||||
|
||||
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
||||
@@ -60,68 +46,13 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
||||
int WgcSessionImpl::Start(bool show_cursor) {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
if (is_running_) return 0;
|
||||
|
||||
int error = 1;
|
||||
|
||||
CHECK_INIT;
|
||||
try {
|
||||
last_show_cursor_ = show_cursor;
|
||||
if (!capture_session_) {
|
||||
auto current_size = capture_item_.Size();
|
||||
capture_framepool_ =
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
CreateFreeThreaded(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, current_size);
|
||||
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
||||
capture_frame_size_ = current_size;
|
||||
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
||||
capture_close_trigger_ = capture_item_.Closed(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
||||
}
|
||||
|
||||
if (!capture_framepool_) throw std::exception();
|
||||
|
||||
is_running_ = true;
|
||||
|
||||
// we do not need to crate a thread to enter a message loop coz we use
|
||||
// CreateFreeThreaded instead of Create to create a capture frame pool,
|
||||
// we need to test the performance later
|
||||
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
|
||||
|
||||
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
||||
capture_session_.StartCapture();
|
||||
|
||||
error = 0;
|
||||
} catch (winrt::hresult_error) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
} catch (...) {
|
||||
return 86;
|
||||
}
|
||||
|
||||
return error;
|
||||
return StartLocked(show_cursor);
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Stop() {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
CHECK_INIT;
|
||||
|
||||
is_running_ = false;
|
||||
|
||||
// if (loop_.joinable()) loop_.join();
|
||||
|
||||
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
||||
|
||||
if (capture_session_) {
|
||||
capture_session_.Close();
|
||||
capture_session_ = nullptr;
|
||||
}
|
||||
|
||||
CleanUpLocked();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -130,7 +61,10 @@ int WgcSessionImpl::Pause() {
|
||||
|
||||
is_paused_ = true;
|
||||
|
||||
CHECK_INIT;
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -139,7 +73,10 @@ int WgcSessionImpl::Resume() {
|
||||
|
||||
is_paused_ = false;
|
||||
|
||||
CHECK_INIT;
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -176,10 +113,10 @@ auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||
interop_factory->CreateForWindow(
|
||||
winrt::check_hresult(interop_factory->CreateForWindow(
|
||||
hwnd,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
||||
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -188,10 +125,10 @@ auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||
interop_factory->CreateForMonitor(
|
||||
winrt::check_hresult(interop_factory->CreateForMonitor(
|
||||
hmonitor,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
||||
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -219,6 +156,104 @@ HRESULT WgcSessionImpl::CreateMappedTexture(
|
||||
d3d11_texture_mapped_.put());
|
||||
}
|
||||
|
||||
int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
if (!capture_item_) {
|
||||
LOG_ERROR("WGC: capture item is null");
|
||||
return 4;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!capture_session_) {
|
||||
auto current_size = capture_item_.Size();
|
||||
capture_framepool_ =
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
CreateFreeThreaded(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, current_size);
|
||||
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
||||
capture_frame_size_ = current_size;
|
||||
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
||||
capture_close_trigger_ = capture_item_.Closed(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
||||
}
|
||||
|
||||
if (!capture_framepool_ || !capture_session_) {
|
||||
throw std::exception();
|
||||
}
|
||||
|
||||
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
||||
capture_session_.StartCapture();
|
||||
is_running_ = true;
|
||||
return 0;
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
} catch (...) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
}
|
||||
}
|
||||
|
||||
int WgcSessionImpl::StartLocked(bool show_cursor) {
|
||||
if (is_running_) return 0;
|
||||
|
||||
last_show_cursor_ = show_cursor;
|
||||
if (!is_initialized_) {
|
||||
const int init_ret = InitializeLocked();
|
||||
if (init_ret != 0) {
|
||||
return init_ret;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = StartCaptureLocked(show_cursor);
|
||||
if (ret == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("WGC: start capture failed, rebuilding capture item");
|
||||
CleanUpLocked();
|
||||
ret = InitializeLocked();
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
return StartCaptureLocked(show_cursor);
|
||||
}
|
||||
|
||||
void WgcSessionImpl::StopLocked() {
|
||||
is_running_ = false;
|
||||
|
||||
// if (loop_.joinable()) loop_.join();
|
||||
|
||||
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
||||
if (capture_close_trigger_) capture_close_trigger_.revoke();
|
||||
|
||||
if (capture_session_) {
|
||||
try {
|
||||
capture_session_.Close();
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: capture session close failed");
|
||||
}
|
||||
capture_session_ = nullptr;
|
||||
}
|
||||
|
||||
if (capture_framepool_) {
|
||||
try {
|
||||
capture_framepool_.Close();
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: frame pool close failed");
|
||||
}
|
||||
capture_framepool_ = nullptr;
|
||||
}
|
||||
|
||||
d3d11_texture_mapped_ = nullptr;
|
||||
}
|
||||
|
||||
void WgcSessionImpl::OnFrame(
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
|
||||
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
|
||||
@@ -226,7 +261,7 @@ void WgcSessionImpl::OnFrame(
|
||||
|
||||
auto is_new_size = false;
|
||||
|
||||
{
|
||||
try {
|
||||
auto frame = sender.TryGetNextFrame();
|
||||
auto frame_size = frame.ContentSize();
|
||||
|
||||
@@ -240,60 +275,63 @@ void WgcSessionImpl::OnFrame(
|
||||
}
|
||||
|
||||
// copy to mapped texture
|
||||
{
|
||||
if (is_paused_) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
||||
frame_captured.get());
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE map_result;
|
||||
HRESULT hr = d3d11_device_context_->Map(
|
||||
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
||||
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
||||
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
||||
,
|
||||
&map_result);
|
||||
if (FAILED(hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData)},
|
||||
id_);
|
||||
}
|
||||
|
||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||
if (is_paused_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_new_size) {
|
||||
capture_framepool_.Recreate(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, capture_frame_size_);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
||||
frame_captured.get());
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE map_result;
|
||||
HRESULT hr = d3d11_device_context_->Map(
|
||||
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
||||
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
||||
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
||||
,
|
||||
&map_result);
|
||||
if (FAILED(hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData)},
|
||||
id_);
|
||||
}
|
||||
|
||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||
|
||||
if (is_new_size) {
|
||||
capture_framepool_.Recreate(
|
||||
d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, capture_frame_size_);
|
||||
}
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +339,13 @@ void WgcSessionImpl::OnClosed(
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&) {
|
||||
std::lock_guard locker(lock_);
|
||||
const bool was_running = is_running_;
|
||||
const bool was_paused = is_paused_;
|
||||
try {
|
||||
CleanUp();
|
||||
is_initialized_ = false;
|
||||
if (Initialize() == 0) {
|
||||
int ret = Start(last_show_cursor_);
|
||||
CleanUpLocked();
|
||||
is_paused_ = was_paused;
|
||||
if (InitializeLocked() == 0) {
|
||||
int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
|
||||
if (ret == 0) {
|
||||
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
|
||||
} else {
|
||||
@@ -320,11 +360,16 @@ void WgcSessionImpl::OnClosed(
|
||||
}
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Initialize() {
|
||||
int WgcSessionImpl::InitializeLocked() {
|
||||
if (is_initialized_) return 0;
|
||||
|
||||
d3d11_texture_mapped_ = nullptr;
|
||||
d3d11_device_context_ = nullptr;
|
||||
d3d11_direct_device_ = nullptr;
|
||||
capture_frame_size_ = {};
|
||||
|
||||
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
|
||||
std::cout << "AE_D3D_CREATE_DEVICE_FAILED" << std::endl;
|
||||
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -333,6 +378,10 @@ int WgcSessionImpl::Initialize() {
|
||||
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
|
||||
else
|
||||
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
|
||||
if (!capture_item_) {
|
||||
LOG_ERROR("WGC: create capture item returned null");
|
||||
return 86;
|
||||
}
|
||||
|
||||
// Set up
|
||||
auto d3d11_device =
|
||||
@@ -354,21 +403,18 @@ int WgcSessionImpl::Initialize() {
|
||||
void WgcSessionImpl::CleanUp() {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
auto expected = false;
|
||||
if (cleaned_.compare_exchange_strong(expected, true)) {
|
||||
capture_close_trigger_.revoke();
|
||||
capture_framepool_trigger_.revoke();
|
||||
CleanUpLocked();
|
||||
}
|
||||
|
||||
if (capture_framepool_) capture_framepool_.Close();
|
||||
void WgcSessionImpl::CleanUpLocked() {
|
||||
StopLocked();
|
||||
|
||||
if (capture_session_) capture_session_.Close();
|
||||
|
||||
capture_framepool_ = nullptr;
|
||||
capture_session_ = nullptr;
|
||||
capture_item_ = nullptr;
|
||||
|
||||
is_initialized_ = false;
|
||||
}
|
||||
capture_item_ = nullptr;
|
||||
d3d11_device_context_ = nullptr;
|
||||
d3d11_direct_device_ = nullptr;
|
||||
capture_frame_size_ = {};
|
||||
is_initialized_ = false;
|
||||
is_paused_ = false;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
|
||||
|
||||
@@ -68,8 +68,12 @@ class WgcSessionImpl : public WgcSession {
|
||||
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&);
|
||||
|
||||
int Initialize();
|
||||
int InitializeLocked();
|
||||
int StartLocked(bool show_cursor);
|
||||
int StartCaptureLocked(bool show_cursor);
|
||||
void StopLocked();
|
||||
void CleanUp();
|
||||
void CleanUpLocked();
|
||||
|
||||
// void message_func();
|
||||
|
||||
@@ -94,7 +98,6 @@ class WgcSessionImpl : public WgcSession {
|
||||
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
|
||||
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
|
||||
|
||||
std::atomic<bool> cleaned_ = false;
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
|
||||
capture_framepool_{nullptr};
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-04-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _INTERACTIVE_STATE_H_
|
||||
#define _INTERACTIVE_STATE_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool IsSecureDesktopInteractionRequired(
|
||||
const std::string& interactive_stage) {
|
||||
return interactive_stage == "lock-screen" ||
|
||||
interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop";
|
||||
}
|
||||
|
||||
inline bool ShouldNormalizeUnlockToUserDesktop(
|
||||
bool interactive_lock_screen_visible, const std::string& interactive_stage,
|
||||
bool session_locked, bool interactive_logon_ui_visible,
|
||||
bool interactive_secure_desktop_active, bool credential_ui_visible,
|
||||
bool password_box_visible, bool unlock_ui_visible,
|
||||
const std::string& last_session_event) {
|
||||
if (!interactive_lock_screen_visible && interactive_stage != "lock-screen") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (session_locked || interactive_logon_ui_visible ||
|
||||
interactive_secure_desktop_active || credential_ui_visible ||
|
||||
password_box_visible || unlock_ui_visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return last_session_event != "session-lock";
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <Windows.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "service_host.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::wstring GetExecutablePath() {
|
||||
wchar_t path[MAX_PATH] = {0};
|
||||
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH) {
|
||||
return L"";
|
||||
}
|
||||
return std::wstring(path, length);
|
||||
}
|
||||
|
||||
void PrintUsage() {
|
||||
std::cout
|
||||
<< "CrossDesk Windows service skeleton\n"
|
||||
<< " --service Run under the Windows Service Control Manager\n"
|
||||
<< " --console Run the service loop in console mode\n"
|
||||
<< " --install Install the service for the current executable\n"
|
||||
<< " --uninstall Remove the installed service\n"
|
||||
<< " --start Start the installed service\n"
|
||||
<< " --stop Stop the installed service\n"
|
||||
<< " --sas Ask the service to send Secure Attention Sequence\n"
|
||||
<< " --ping Ping the running service over named pipe IPC\n"
|
||||
<< " --status Query runtime status over named pipe IPC\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
crossdesk::CrossDeskServiceHost host;
|
||||
|
||||
if (argc <= 1) {
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string command = argv[1];
|
||||
if (command == "--service") {
|
||||
return host.RunAsService();
|
||||
}
|
||||
if (command == "--console") {
|
||||
return host.RunInConsole();
|
||||
}
|
||||
if (command == "--install") {
|
||||
std::wstring executable_path = GetExecutablePath();
|
||||
bool success = !executable_path.empty() &&
|
||||
crossdesk::InstallCrossDeskService(executable_path);
|
||||
std::cout << (success ? "install ok" : "install failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
if (command == "--uninstall") {
|
||||
bool success = crossdesk::UninstallCrossDeskService();
|
||||
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
if (command == "--start") {
|
||||
bool success = crossdesk::StartCrossDeskService();
|
||||
std::cout << (success ? "start ok" : "start failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
if (command == "--stop") {
|
||||
bool success = crossdesk::StopCrossDeskService();
|
||||
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
if (command == "--sas") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
if (command == "--ping") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
if (command == "--status") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-04-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SERVICE_HOST_H_
|
||||
#define _SERVICE_HOST_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr wchar_t kCrossDeskServiceName[] = L"CrossDeskService";
|
||||
inline constexpr wchar_t kCrossDeskServiceDisplayName[] = L"CrossDesk Service";
|
||||
inline constexpr wchar_t kCrossDeskServicePipeName[] =
|
||||
L"\\\\.\\pipe\\CrossDeskService";
|
||||
|
||||
class CrossDeskServiceHost {
|
||||
public:
|
||||
CrossDeskServiceHost();
|
||||
~CrossDeskServiceHost();
|
||||
|
||||
int RunAsService();
|
||||
int RunInConsole();
|
||||
|
||||
private:
|
||||
int RunServiceLoop(bool as_service);
|
||||
int InitializeRuntime();
|
||||
void ShutdownRuntime();
|
||||
void RequestStop();
|
||||
void ClientProcessMonitorLoop();
|
||||
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
|
||||
DWORD wait_hint);
|
||||
void IpcServerLoop();
|
||||
void RefreshSessionState();
|
||||
void EnsureSessionHelper();
|
||||
void ReapSessionHelper();
|
||||
void StopSessionHelper();
|
||||
bool LaunchSessionHelper(DWORD session_id);
|
||||
void ReapSecureInputHelper();
|
||||
void StopSecureInputHelper();
|
||||
bool LaunchSecureInputHelper(DWORD session_id,
|
||||
const std::string& interactive_stage);
|
||||
std::wstring GetSessionHelperPath() const;
|
||||
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
|
||||
std::wstring GetSecureInputHelperPath() const;
|
||||
std::wstring GetSecureInputHelperStopEventName(DWORD session_id) const;
|
||||
void ResetSessionHelperReportedStateLocked(const char* error,
|
||||
DWORD error_code);
|
||||
bool GetEffectiveSessionLockedLocked() const;
|
||||
bool IsHelperReportingLockScreenLocked() const;
|
||||
bool HasSecureInputUiLocked() const;
|
||||
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
|
||||
bool IsSasSecureDesktopGraceActiveLocked() const;
|
||||
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
|
||||
std::string ResolveInteractiveStageLocked() const;
|
||||
void RefreshSessionHelperReportedState();
|
||||
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
||||
std::string HandleIpcCommand(const std::string& command);
|
||||
std::string BuildStatusResponse();
|
||||
std::string SendSecureAttentionSequence();
|
||||
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
std::string SendSecureDesktopMouseInput(int x, int y, int wheel, int flag);
|
||||
|
||||
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
|
||||
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
|
||||
static DWORD WINAPI ServiceControlHandler(DWORD control, DWORD event_type,
|
||||
LPVOID event_data, LPVOID context);
|
||||
|
||||
private:
|
||||
SERVICE_STATUS_HANDLE status_handle_ = nullptr;
|
||||
SERVICE_STATUS service_status_{};
|
||||
HANDLE stop_event_ = nullptr;
|
||||
std::thread ipc_thread_;
|
||||
std::thread client_process_monitor_thread_;
|
||||
std::mutex state_mutex_;
|
||||
DWORD active_session_id_ = 0xFFFFFFFF;
|
||||
DWORD process_session_id_ = 0xFFFFFFFF;
|
||||
DWORD input_desktop_error_code_ = 0;
|
||||
DWORD session_helper_process_id_ = 0;
|
||||
DWORD session_helper_session_id_ = 0xFFFFFFFF;
|
||||
DWORD session_helper_exit_code_ = 0;
|
||||
DWORD session_helper_last_error_code_ = 0;
|
||||
DWORD session_helper_status_error_code_ = 0;
|
||||
DWORD session_helper_report_session_id_ = 0xFFFFFFFF;
|
||||
DWORD session_helper_report_process_id_ = 0;
|
||||
DWORD session_helper_report_input_desktop_error_code_ = 0;
|
||||
DWORD secure_input_helper_process_id_ = 0;
|
||||
DWORD secure_input_helper_session_id_ = 0xFFFFFFFF;
|
||||
DWORD secure_input_helper_exit_code_ = 0;
|
||||
DWORD secure_input_helper_last_error_code_ = 0;
|
||||
DWORD last_session_event_type_ = 0;
|
||||
DWORD last_session_event_session_id_ = 0xFFFFFFFF;
|
||||
ULONGLONG started_at_tick_ = 0;
|
||||
ULONGLONG last_sas_tick_ = 0;
|
||||
ULONGLONG session_helper_started_at_tick_ = 0;
|
||||
ULONGLONG session_helper_report_state_age_ms_ = 0;
|
||||
ULONGLONG session_helper_report_uptime_ms_ = 0;
|
||||
ULONGLONG secure_input_helper_started_at_tick_ = 0;
|
||||
ULONGLONG sas_secure_desktop_until_tick_ = 0;
|
||||
bool session_locked_ = false;
|
||||
bool logon_ui_visible_ = false;
|
||||
bool prelogin_ = false;
|
||||
bool secure_desktop_active_ = false;
|
||||
bool input_desktop_available_ = false;
|
||||
bool session_helper_running_ = false;
|
||||
bool session_helper_status_ok_ = false;
|
||||
bool session_helper_report_session_locked_ = false;
|
||||
bool session_helper_report_input_desktop_available_ = false;
|
||||
bool session_helper_report_lock_app_visible_ = false;
|
||||
bool session_helper_report_logon_ui_visible_ = false;
|
||||
bool session_helper_report_secure_desktop_active_ = false;
|
||||
bool session_helper_report_credential_ui_visible_ = false;
|
||||
bool session_helper_report_unlock_ui_visible_ = false;
|
||||
bool secure_input_helper_running_ = false;
|
||||
bool console_mode_ = false;
|
||||
bool sas_secure_desktop_seen_ = false;
|
||||
DWORD last_sas_error_code_ = 0;
|
||||
bool last_sas_success_ = false;
|
||||
HANDLE session_helper_process_handle_ = nullptr;
|
||||
HANDLE session_helper_stop_event_ = nullptr;
|
||||
HANDLE secure_input_helper_process_handle_ = nullptr;
|
||||
HANDLE secure_input_helper_stop_event_ = nullptr;
|
||||
std::string input_desktop_name_;
|
||||
std::string last_sas_error_;
|
||||
std::string session_helper_last_error_;
|
||||
std::string session_helper_status_error_;
|
||||
std::string session_helper_report_input_desktop_;
|
||||
std::string session_helper_report_interactive_stage_;
|
||||
std::string secure_input_helper_last_error_;
|
||||
std::string secure_input_helper_interactive_stage_;
|
||||
|
||||
static CrossDeskServiceHost* instance_;
|
||||
};
|
||||
|
||||
bool IsCrossDeskServiceInstalled();
|
||||
bool InstallCrossDeskService(const std::wstring& binary_path);
|
||||
bool UninstallCrossDeskService();
|
||||
bool StartCrossDeskService();
|
||||
bool StopCrossDeskService(DWORD timeout_ms = 5000);
|
||||
std::string QueryCrossDeskService(const std::string& command,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
int flag,
|
||||
DWORD timeout_ms = 1000);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-04-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SESSION_HELPER_SHARED_H_
|
||||
#define _SESSION_HELPER_SHARED_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] =
|
||||
L"\\\\.\\pipe\\CrossDeskSessionHelper-";
|
||||
inline constexpr wchar_t kCrossDeskSecureInputHelperPipePrefix[] =
|
||||
L"\\\\.\\pipe\\CrossDeskSecureInputHelper-";
|
||||
inline constexpr char kCrossDeskSessionHelperStatusCommand[] = "status";
|
||||
inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] =
|
||||
"keyboard:";
|
||||
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureStartCommandPrefix[] =
|
||||
"capture-start:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureStopCommand[] =
|
||||
"capture-stop";
|
||||
inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = 16 * 1024 * 1024;
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrame-";
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct CrossDeskSecureDesktopFrameHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
int32_t left;
|
||||
int32_t top;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
};
|
||||
|
||||
struct CrossDeskSecureDesktopSharedFrameHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
volatile uint32_t writing;
|
||||
uint32_t sequence;
|
||||
int32_t left;
|
||||
int32_t top;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
uint32_t buffer_size;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSessionHelperPipePrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSecureInputHelperPipePrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
inline std::wstring GetCrossDeskSecureDesktopFrameMappingName(
|
||||
DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSecureDesktopFrameMappingPrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
inline std::wstring GetCrossDeskSecureDesktopFrameReadyEventName(
|
||||
DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSecureDesktopFrameReadyEventPrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -452,11 +452,17 @@ static void MonitorThreadFunc() {
|
||||
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
|
||||
|
||||
XEvent event;
|
||||
constexpr int kEventPollIntervalMs = 20;
|
||||
while (g_monitoring.load()) {
|
||||
XNextEvent(g_x11_display, &event);
|
||||
if (event.type == event_base + XFixesSelectionNotify) {
|
||||
HandleClipboardChange();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kEventPollIntervalMs));
|
||||
}
|
||||
|
||||
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: a0001b1faf...bb0fae0617
@@ -0,0 +1,183 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/gui/toolbars/control_bar.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||
const std::string& expected, size_t min_count) {
|
||||
size_t count = 0;
|
||||
size_t pos = 0;
|
||||
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||
++count;
|
||||
pos += expected.size();
|
||||
}
|
||||
|
||||
if (count >= min_count) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " expected at least " << min_count
|
||||
<< " occurrences of: " << expected << ", found " << count
|
||||
<< "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectResetBeforeDisplayPopup(const std::string& value) {
|
||||
const std::string reset = "props->display_selectable_hovered_ = false;";
|
||||
const std::string popup = "ImGui::BeginPopup(\"display\")";
|
||||
const size_t reset_pos = value.find(reset);
|
||||
const size_t popup_pos = value.find(popup);
|
||||
|
||||
if (reset_pos != std::string::npos && popup_pos != std::string::npos &&
|
||||
reset_pos < popup_pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "control_bar.cpp must clear display_selectable_hovered_ before "
|
||||
"checking the display popup\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectResetBeforeShortcutPopup(const std::string& value) {
|
||||
const std::string reset = "props->shortcut_selectable_hovered_ = false;";
|
||||
const std::string popup = "ImGui::BeginPopup(\"shortcut\")";
|
||||
const size_t reset_pos = value.find(reset);
|
||||
const size_t popup_pos = value.find(popup);
|
||||
|
||||
if (reset_pos != std::string::npos && popup_pos != std::string::npos &&
|
||||
reset_pos < popup_pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "control_bar.cpp must clear shortcut_selectable_hovered_ before "
|
||||
"checking the shortcut popup\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"props->display_selectable_hovered_ = false;");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"ImGui::IsWindowHovered("
|
||||
"ImGuiHoveredFlags_RootAndChildWindows)");
|
||||
ok &= ExpectResetBeforeDisplayPopup(control_bar);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"props->shortcut_selectable_hovered_ =");
|
||||
ok &= ExpectResetBeforeShortcutPopup(control_bar);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"void ShowControlBarTooltip(const std::string& text)");
|
||||
ok &= ExpectContainsAtLeast("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip(", 10);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::select_display"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::send_shortcut"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->mouse_control_button_label_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->audio_capture_button_label_)");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::select_file"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->net_traffic_stats_button_label_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->fullscreen_button_label_)");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::release_mouse"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::control_mouse"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::audio_capture"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::mute[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::hide_net_traffic_stats"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::show_net_traffic_stats"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::exit_fullscreen"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::fullscreen"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::disconnect"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::expand_control_bar"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::collapse_control_bar"
|
||||
"[localization_language_index_]");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "macos_keyboard_modifier_state.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExpectEqual(const char* name, uint32_t actual, uint32_t expected) {
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected << "\n"
|
||||
<< " actual: " << actual << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
crossdesk::MacKeyboardModifierState state;
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectEqual("initial flags", state.flags(), 0);
|
||||
ok &= ExpectEqual("left shift down", state.Update(0xA0, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("shifted semicolon keeps shift",
|
||||
state.Update(0xBA, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("semicolon up keeps shift", state.Update(0xBA, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift down while left held",
|
||||
state.Update(0xA1, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("left shift up while right held", state.Update(0xA0, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift up clears shift", state.Update(0xA1, false),
|
||||
0);
|
||||
|
||||
ok &= ExpectEqual("left control down", state.Update(0xA2, true),
|
||||
crossdesk::kMacInjectedModifierControl);
|
||||
ok &= ExpectEqual("right alt adds option", state.Update(0xA5, true),
|
||||
crossdesk::kMacInjectedModifierControl |
|
||||
crossdesk::kMacInjectedModifierOption);
|
||||
ok &= ExpectEqual("left command adds command", state.Update(0x5B, true),
|
||||
crossdesk::kMacInjectedModifierControl |
|
||||
crossdesk::kMacInjectedModifierOption |
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("left control up leaves option command",
|
||||
state.Update(0xA2, false),
|
||||
crossdesk::kMacInjectedModifierOption |
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("right alt up leaves command", state.Update(0xA5, false),
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false),
|
||||
0);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "path_manager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <limits.h>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
#ifdef _WIN32
|
||||
wchar_t buffer[MAX_PATH] = {};
|
||||
DWORD length = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
||||
if (length == 0 || length == MAX_PATH) {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
#elif defined(__APPLE__)
|
||||
char buffer[PATH_MAX] = {};
|
||||
uint32_t size = sizeof(buffer);
|
||||
if (_NSGetExecutablePath(buffer, &size) != 0) {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::weakly_canonical(buffer).parent_path();
|
||||
#else
|
||||
char buffer[PATH_MAX] = {};
|
||||
ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
buffer[length] = '\0';
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ExpectEqual(const char* name,
|
||||
const std::filesystem::path& actual,
|
||||
const std::filesystem::path& expected) {
|
||||
if (actual.lexically_normal() == expected.lexically_normal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected.string() << "\n"
|
||||
<< " actual: " << actual.string() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path exe_dir = GetExecutableDirectory();
|
||||
if (exe_dir.empty()) {
|
||||
std::cerr << "failed to resolve executable directory\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
crossdesk::PathManager path_manager("CrossDesk");
|
||||
const std::filesystem::path expected_data = exe_dir / "data";
|
||||
const std::filesystem::path expected_logs = exe_dir / "logs";
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectEqual("config path", path_manager.GetConfigPath(), expected_data);
|
||||
ok &= ExpectEqual("cache path", path_manager.GetCachePath(), expected_data);
|
||||
ok &= ExpectEqual("log path", path_manager.GetLogPath(), expected_logs);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "scripts/windows/crossdesk.rc")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
bool ExpectActivationContext(const std::filesystem::path& manifest_path) {
|
||||
ACTCTXW context = {};
|
||||
context.cbSize = sizeof(context);
|
||||
std::wstring source = manifest_path.wstring();
|
||||
context.lpSource = source.c_str();
|
||||
|
||||
HANDLE activation_context = CreateActCtxW(&context);
|
||||
if (activation_context == INVALID_HANDLE_VALUE) {
|
||||
std::cerr << "CreateActCtxW failed for " << manifest_path.string()
|
||||
<< ", error=" << GetLastError() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
ReleaseActCtx(activation_context);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
|
||||
const std::string manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
const std::string debug_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk.manifest", manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"level=\"asInvoker\"");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
#ifdef _WIN32
|
||||
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
#endif
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/device_controller/mouse/windows/mouse_controller.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string mouse_controller = ReadFile(
|
||||
repo_root / "src/device_controller/mouse/windows/mouse_controller.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"INPUT ip = {0};");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SetCursorPos failed");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SendInput failed for mouse");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "interactive_state.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectTrue(const char* name, bool value) {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " expected true\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string render_h = ReadFile(repo_root / "src/gui/render.h");
|
||||
const std::string service_host =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||
const std::string service_host_h =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||
const std::string session_helper =
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectTrue("secure desktop input routing",
|
||||
crossdesk::IsSecureDesktopInteractionRequired(
|
||||
"secure-desktop"));
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"CanSendSecureAttentionSequence("
|
||||
"props->remote_interactive_stage_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ImGui::BeginDisabled();\n"
|
||||
" }\n"
|
||||
" if (ImGui::Selectable(sas_label.c_str()))");
|
||||
ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen");
|
||||
ok &= ExpectContains("render.h", render_h,
|
||||
"optimistic_windows_secure_desktop_until_tick_");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"kWindowsServiceSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"status->sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"json.value(\"sas_secure_desktop_grace_active\", false)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"status.sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"local_interactive_stage_ = \"secure-desktop\"");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"sas_secure_desktop_until_tick_");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"sas_secure_desktop_seen_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"kSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"IsSasSecureDesktopGraceActiveLocked()");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"UpdateSasSecureDesktopGraceLocked("
|
||||
"session_helper_report_interactive_stage_)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_seen_ = true");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_until_tick_ = 0");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_until_tick_ =");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"now + kSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"sas_secure_desktop_grace_active\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"raw_interactive_stage = ResolveInteractiveStageLocked()");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"kSessionHelperStatePollMs = 1000");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EVENT_SYSTEM_DESKTOPSWITCH");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SetWinEventHook(");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MsgWaitForMultipleObjects");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"WaitForSessionHelperStateChange(stop_event, "
|
||||
"desktop_switch_event)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"inaccessible_secure_input_desktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"desktop_info.error_code == ERROR_ACCESS_DENIED");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"secure_desktop_active = input_desktop_is_winlogon ||");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string service_host =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||
const std::string service_host_h =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||
const std::string session_helper =
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
const std::string targets =
|
||||
ReadFile(repo_root / "xmake/targets.lua");
|
||||
const std::string interactive_state =
|
||||
ReadFile(repo_root / "src/service/windows/interactive_state.h");
|
||||
const std::string render_callback =
|
||||
ReadFile(repo_root / "src/gui/render_callback.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string screen_capturer_h =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.h");
|
||||
const std::string screen_capturer_cpp =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"ParseSecureDesktopMouseIpcCommand");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"BuildSecureInputHelperMouseCommand");
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"target(\"crossdesk_session_helper\")");
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"add_files(\"scripts/windows/crossdesk.rc\")");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SetProcessDpiAwarenessContext(\n"
|
||||
" DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness();\n\n"
|
||||
" InitializeHelperLogger();");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"while (GetTickCount64() <= deadline_tick)");
|
||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||
"constexpr int kPipeConnectRetryCount = 3");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"BuildSecureInputHelperKeyboardCommand(");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"const std::string& interactive_stage");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"bool LaunchSecureInputHelper(DWORD session_id,\n"
|
||||
" const std::string& interactive_stage)");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"std::string secure_input_helper_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"SecureInputHelperDesktopForStage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\Winlogon\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\default\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ == interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ = interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_.clear()");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"LaunchSecureInputHelper(target_session_id, interactive_stage)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"secure_input_helper_stage\\\":\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"session_helper_report_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return SendSecureDesktopMouseInput");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"constexpr DWORD kWindowsServiceQueryTimeoutMs = 500");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"Local Windows service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop transient frame query failed");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"if (transient_error) {\n"
|
||||
" LOG_INFO(");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"IsTransientSecureDesktopInputFailure");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"Secure desktop keyboard injection transient failure");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MOUSEEVENTF_VIRTUALDESK");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"std::vector<INPUT> inputs");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SendInput(static_cast<UINT>(inputs.size())");
|
||||
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
"SetCursorPos(request.x, request.y)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"NormalizeAbsoluteMouseCoordinate");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadInteractiveDesktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"OpenInputDesktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"DesktopNameForInteractiveStage");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"interactive_stage == \"credential-ui\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"return L\"Winlogon\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"interactive_stage == \"lock-screen\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"return L\"Default\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadInteractiveDesktopForStage");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"switch_interactive_desktop_failed");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"Json BuildInputFailureJson");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"target_desktop\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"current_desktop\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"stage\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,\n"
|
||||
" &extended, &interactive_stage)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"InjectKeyboardInput(key_code, is_down, scan_code, extended,\n"
|
||||
" interactive_stage)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"InjectMouseInput(mouse_request)");
|
||||
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"winsta0\\\\default");
|
||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||
"startup_info.lpDesktop = const_cast<LPWSTR>(L\"winsta0\\\\Winlogon\")");
|
||||
ok &= ExpectContains("interactive_state.h", interactive_state,
|
||||
"interactive_stage == \"lock-screen\"");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"RemoteAction remote_action{};");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"previous_secure_desktop_interaction");
|
||||
ok &= ExpectNotContains(
|
||||
"render_callback.cpp", render_callback,
|
||||
"render->local_service_available_ &&\n"
|
||||
" IsSecureDesktopInteractionRequired(render->local_interactive_stage_)");
|
||||
ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h,
|
||||
"std::string secure_shared_stage_;");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"const std::string& stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_ == stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_ = stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_.clear()");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -1,209 +1,10 @@
|
||||
set_project("crossdesk")
|
||||
set_license("LGPL-3.0")
|
||||
|
||||
option("CROSSDESK_VERSION")
|
||||
set_default("0.0.0")
|
||||
set_showmenu(true)
|
||||
set_description("Set CROSSDESK_VERSION for build")
|
||||
option_end()
|
||||
includes("xmake/options.lua")
|
||||
includes("xmake/platform.lua")
|
||||
includes("xmake/targets.lua")
|
||||
|
||||
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", "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")
|
||||
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})
|
||||
if is_os("windows") then
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
end
|
||||
setup_options_and_dependencies()
|
||||
setup_platform_settings()
|
||||
setup_targets()
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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()
|
||||
|
||||
option("CROSSDESK_PORTABLE")
|
||||
set_default(false)
|
||||
set_showmenu(true)
|
||||
set_description("Build CrossDesk as a portable package that stores data beside the executable")
|
||||
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"))
|
||||
add_defines("CROSSDESK_PORTABLE=" .. (is_config("CROSSDESK_PORTABLE", 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
|
||||
@@ -0,0 +1,81 @@
|
||||
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")
|
||||
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
|
||||
@@ -0,0 +1,242 @@
|
||||
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("path_manager_portable_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_defines("CROSSDESK_PORTABLE=1")
|
||||
add_includedirs("src/path_manager")
|
||||
add_files("tests/path_manager_portable_test.cpp",
|
||||
"src/path_manager/path_manager.cpp")
|
||||
|
||||
target("macos_keyboard_modifier_state_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/device_controller")
|
||||
add_files("tests/macos_keyboard_modifier_state_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_manifest_resource_test.cpp")
|
||||
|
||||
target("windows_service_mouse_ipc_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_service_mouse_ipc_test.cpp")
|
||||
|
||||
target("windows_mouse_controller_safety_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_mouse_controller_safety_test.cpp")
|
||||
|
||||
target("windows_sas_guard_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/service/windows")
|
||||
add_files("tests/windows_sas_guard_test.cpp")
|
||||
|
||||
target("display_popup_hover_state_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/display_popup_hover_state_test.cpp")
|
||||
|
||||
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", "src/service/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", "src/service/windows",
|
||||
{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", "path_manager")
|
||||
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
|
||||
-- Keep the project on C++17 while C++/WinRT still falls back to
|
||||
-- MSVC's deprecated experimental coroutine header.
|
||||
add_defines("_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS")
|
||||
add_links("windowsapp")
|
||||
add_files("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")
|
||||
|
||||
target("crossdesk_service")
|
||||
set_kind("binary")
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_files("src/service/windows/main.cpp",
|
||||
"src/service/windows/service_host.cpp")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
|
||||
target("crossdesk_session_helper")
|
||||
set_kind("binary")
|
||||
add_packages("libyuv")
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
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_files("src/service/windows/service_host.cpp")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user