mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-04 00:30:16 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6c26511cd | |||
| c0b880e1eb | |||
| c10f1499f2 | |||
| d7a9c26ff9 | |||
| f809ad173c | |||
| 34155d0266 | |||
| 9d544788b0 | |||
| dd16e65b68 | |||
| 91eac61a90 | |||
| 941cc79af5 | |||
| 0d6ecfc730 |
@@ -26,8 +26,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_NAME: crossdesk/ubuntu22.04
|
||||
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
IMAGE_NAME: crossdesk/ubuntu20.04
|
||||
STACK_TAG: buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
CMAKE_VERSION: 3.31.6
|
||||
XMAKE_VERSION: 3.0.9
|
||||
RUST_VERSION: 1.92.0
|
||||
RUSTUP_VERSION: 1.28.2
|
||||
@@ -44,11 +45,11 @@ jobs:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
base_image: nvidia/cuda:12.6.3-devel-ubuntu20.04
|
||||
- arch: arm64
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
base_image: ubuntu:22.04
|
||||
base_image: ubuntu:20.04
|
||||
steps:
|
||||
- name: Set candidate tag
|
||||
id: image
|
||||
@@ -86,6 +87,7 @@ jobs:
|
||||
sbom: false
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.base_image }}
|
||||
CMAKE_VERSION=${{ env.CMAKE_VERSION }}
|
||||
XMAKE_VERSION=${{ env.XMAKE_VERSION }}
|
||||
RUST_VERSION=${{ env.RUST_VERSION }}
|
||||
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
|
||||
|
||||
+20
-10
@@ -29,12 +29,12 @@ jobs:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_amd64.sh
|
||||
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_arm64.sh
|
||||
|
||||
container:
|
||||
@@ -106,26 +106,30 @@ jobs:
|
||||
- name: Ensure Wayland build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
if pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2; then
|
||||
if pkg-config --exists dbus-1 libdrm && \
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h && \
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
libdbus-1-dev \
|
||||
libdrm-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libspa-0.2-dev
|
||||
libdrm-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2
|
||||
install-crossdesk-pipewire-sdk
|
||||
pkg-config --exists dbus-1 libdrm
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h
|
||||
|
||||
- name: Build CrossDesk
|
||||
env:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true --root -y
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true \
|
||||
--cc=gcc-10 --cxx=g++-10 --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
- name: Package
|
||||
@@ -133,13 +137,19 @@ jobs:
|
||||
chmod +x ${{ matrix.package_script }}
|
||||
${{ matrix.package_script }} ${LEGAL_VERSION}
|
||||
|
||||
- name: Verify packaged Ubuntu 22.04 glibc baseline
|
||||
- name: Verify Ubuntu 20.04 package dependency resolution
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
|
||||
apt-get update
|
||||
apt-get install --simulate "${PACKAGE_FILE}"
|
||||
|
||||
- name: Verify packaged Ubuntu 20.04 glibc baseline
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
|
||||
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
|
||||
install -d "${VERIFY_DIR}"
|
||||
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
|
||||
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35
|
||||
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.31
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
|
||||
@@ -32,7 +32,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
||||
|----------------|---------------------------|
|
||||
| **Windows** | Windows 10 及以上 (64 位) |
|
||||
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
|
||||
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
|
||||
| **Linux** | Ubuntu 20.04 及以上 |
|
||||
|
||||
## 使用
|
||||
|
||||
@@ -80,16 +80,18 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
|
||||
|
||||
依赖:
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖:
|
||||
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
|
||||
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
@@ -97,10 +99,19 @@ sudo apt-get install -y \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
|
||||
CMake 3.21 或更高版本,无需手动替换系统 CMake。
|
||||
|
||||
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
|
||||
因此上述命令同时安装并在后续配置中指定 GCC 10。
|
||||
|
||||
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
sudo apt-get install -y libdbus-1-dev libdrm-dev
|
||||
|
||||
# Ubuntu 22.04 及以上也可以直接安装系统开发包
|
||||
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
|
||||
```
|
||||
|
||||
下载子模块并编译 Release 版本:
|
||||
@@ -112,16 +123,25 @@ cd crossdesk
|
||||
# 已经克隆过仓库时执行这一行
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
|
||||
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
|
||||
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
|
||||
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
|
||||
|
||||
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
||||
|
||||
启用 Wayland 和 DRM 的配置示例:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
@@ -136,6 +156,8 @@ xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
|
||||
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
|
||||
|
||||
### 通用编译选项
|
||||
|
||||
@@ -158,7 +180,7 @@ xmake r crossdesk
|
||||
|
||||
### 无 CUDA 环境下的开发支持
|
||||
|
||||
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
|
||||
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包。
|
||||
|
||||
进入容器,下载工程后执行:
|
||||
```
|
||||
|
||||
+38
-9
@@ -32,7 +32,7 @@ CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkund
|
||||
|-----------|-----------------|
|
||||
| **Windows** | Windows 10 or later (64-bit) |
|
||||
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
|
||||
| **Linux** | Ubuntu 22.04 or later *(older versions can be built manually for compatibility)* |
|
||||
| **Linux** | Ubuntu 20.04 or later |
|
||||
|
||||
|
||||
## Usage
|
||||
@@ -85,16 +85,19 @@ If the remote Windows service is not installed, not running, or temporarily unav
|
||||
|
||||
Requirements:
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first:
|
||||
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages
|
||||
use Ubuntu 20.04/glibc 2.31 as their compatibility baseline. Install the base
|
||||
build dependencies first:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
@@ -102,10 +105,21 @@ sudo apt-get install -y \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
|
||||
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
|
||||
not required.
|
||||
|
||||
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
|
||||
by Slint's C++ API, so the commands above install GCC 10 and the configuration
|
||||
below selects it explicitly.
|
||||
|
||||
Install the optional dependencies when Wayland capture or DRM capture is enabled:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
sudo apt-get install -y libdbus-1-dev libdrm-dev
|
||||
|
||||
# Ubuntu 22.04 and later can alternatively use the distribution packages.
|
||||
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
|
||||
```
|
||||
|
||||
Clone the submodules and build a release binary:
|
||||
@@ -117,16 +131,28 @@ cd crossdesk
|
||||
# Run this when the repository has already been cloned
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
|
||||
# header-only SDK used during compilation. Skip this when the distribution's
|
||||
# PipeWire 0.3 development packages are already installed.
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
|
||||
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
|
||||
`--cc`/`--cxx` may still let dependency builds fall back to the system
|
||||
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
|
||||
configuration.
|
||||
|
||||
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
||||
|
||||
Example configuration with Wayland and DRM capture enabled:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
@@ -141,6 +167,9 @@ Build a Debian package after compiling a release binary on the matching architec
|
||||
```
|
||||
|
||||
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
|
||||
PipeWire is not a mandatory runtime dependency. CrossDesk detects the host's
|
||||
PipeWire 0.3 runtime dynamically and can still use X11 capture (and DRM when
|
||||
enabled at build time) when it is unavailable.
|
||||
|
||||
### Common build options
|
||||
|
||||
@@ -163,8 +192,8 @@ xmake r crossdesk
|
||||
|
||||
#### Development Without CUDA Environment
|
||||
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
|
||||
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
|
||||
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
|
||||
|
||||
After entering the container, download the project and run:
|
||||
```
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu20.04
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG CMAKE_VERSION=3.31.6
|
||||
ARG XMAKE_VERSION=3.0.9
|
||||
ARG RUST_VERSION=1.92.0
|
||||
ARG RUSTUP_VERSION=1.28.2
|
||||
|
||||
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
|
||||
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \
|
||||
org.opencontainers.image.description="Ubuntu 20.04 compatibility build environment for CrossDesk" \
|
||||
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
|
||||
org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
@@ -22,6 +23,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
XMAKE_GLOBALDIR=/data \
|
||||
XMAKE_ROOT=y \
|
||||
CUDA_PATH=/usr/local/cuda \
|
||||
CC=/usr/bin/gcc-10 \
|
||||
CXX=/usr/bin/g++-10 \
|
||||
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
@@ -35,13 +38,14 @@ RUN apt-get update \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
curl \
|
||||
dbus \
|
||||
dpkg-dev \
|
||||
file \
|
||||
flex \
|
||||
gettext \
|
||||
gcc-10 \
|
||||
g++-10 \
|
||||
git \
|
||||
libasound2-dev \
|
||||
libdbus-1-dev \
|
||||
@@ -52,11 +56,9 @@ RUN apt-get update \
|
||||
libfreetype6-dev \
|
||||
libgl1-mesa-dev \
|
||||
libmount-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libpulse-dev \
|
||||
libreadline-dev \
|
||||
libsndio-dev \
|
||||
libspa-0.2-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libx11-dev \
|
||||
@@ -95,6 +97,22 @@ RUN apt-get update \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Slint 1.17 requires CMake 3.21 or newer, while Ubuntu 20.04 provides 3.16.
|
||||
# PyPI publishes pinned manylinux2014 wheels for both amd64 and arm64.
|
||||
RUN python3 -m pip install --no-cache-dir "cmake==${CMAKE_VERSION}" \
|
||||
&& cmake --version | grep -F "cmake version ${CMAKE_VERSION}"
|
||||
|
||||
# Ubuntu 20.04 only ships PipeWire 0.2. Install the Ubuntu 22.04-era 0.3.48
|
||||
# headers as a build-only SDK. CrossDesk loads the host's PipeWire 0.3 runtime
|
||||
# with dlopen(), so no PipeWire shared library is copied into release packages.
|
||||
COPY docker/linux-build/install-pipewire-sdk.sh \
|
||||
/usr/local/bin/install-crossdesk-pipewire-sdk
|
||||
|
||||
RUN chmod 0755 /usr/local/bin/install-crossdesk-pipewire-sdk \
|
||||
&& install-crossdesk-pipewire-sdk \
|
||||
&& grep -F "#define PW_MICRO 48" \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h
|
||||
|
||||
RUN export GITHUB_ACTIONS=true \
|
||||
&& curl -fsSL https://xmake.io/shget.text \
|
||||
| bash -s -- "v${XMAKE_VERSION}" \
|
||||
@@ -136,7 +154,8 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
|
||||
# CrossDesk's exact dependency graph. Source files are intentionally excluded
|
||||
# from the Docker context; source-only changes therefore reuse this layer.
|
||||
RUN xmake repo -u --root \
|
||||
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true --root -y \
|
||||
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true \
|
||||
--cc=gcc-10 --cxx=g++-10 --root -y \
|
||||
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
|
||||
&& mkdir -p /workspace
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# CrossDesk Linux 构建镜像
|
||||
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
|
||||
Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA)以及根据项目
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10、
|
||||
CMake 3.31、xmake、Rust、Linux 开发库(包括 D-Bus、DRM,以及仅用于编译的
|
||||
PipeWire 0.3/SPA 头文件 SDK)以及根据项目
|
||||
`xmake.lua` 提前编译好的依赖。
|
||||
|
||||
## 发布方式
|
||||
@@ -11,14 +12,19 @@ Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA)以及根据项
|
||||
全部通过后再合并为同一个 multi-arch 镜像:
|
||||
|
||||
```text
|
||||
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
```
|
||||
|
||||
Docker 会根据运行机器自动选择正确架构。amd64 变体以
|
||||
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建
|
||||
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。
|
||||
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容
|
||||
Ubuntu 20.04。
|
||||
`nvidia/cuda:12.6.3-devel-ubuntu20.04` 为基础;arm64 变体不包含当前构建
|
||||
用不到的 CUDA SDK。两个变体都使用 Ubuntu 20.04/glibc 2.31,因此生成的
|
||||
单一 Linux 安装包可运行在 Ubuntu 20.04 及更高版本。
|
||||
|
||||
Ubuntu 20.04 仓库只提供 PipeWire 0.2。镜像通过
|
||||
`install-pipewire-sdk.sh` 安装固定版本的 PipeWire 0.3.48/SPA 头文件,但不
|
||||
安装或打包 PipeWire 动态库。CrossDesk 在运行时通过 `dlopen()` 使用宿主系统
|
||||
提供的 PipeWire 0.3:Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
|
||||
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
|
||||
|
||||
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
|
||||
工具链版本 tag 只会在两个架构都验证成功后更新。
|
||||
@@ -27,7 +33,7 @@ Ubuntu 20.04。
|
||||
|
||||
在仓库的 Actions secrets 中配置:
|
||||
|
||||
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名
|
||||
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu20.04` 的 Docker Hub 用户名
|
||||
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
|
||||
|
||||
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PIPEWIRE_VERSION="0.3.48"
|
||||
PIPEWIRE_ARCHIVE_SHA256="0d218be86b3d2b548c06259c47ad8d110ee1c09f071e17c4393eeef5c880fa6f"
|
||||
INSTALL_PREFIX="${1:-/opt/crossdesk-pipewire-sdk}"
|
||||
IFS='.' read -r PIPEWIRE_VERSION_MAJOR PIPEWIRE_VERSION_MINOR \
|
||||
PIPEWIRE_VERSION_MICRO <<< "${PIPEWIRE_VERSION}"
|
||||
|
||||
if [[ "${INSTALL_PREFIX}" != /* ]]; then
|
||||
echo "Install prefix must be an absolute path: ${INSTALL_PREFIX}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-pipewire-sdk.XXXXXX")"
|
||||
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||
|
||||
ARCHIVE_PATH="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||
SOURCE_DIR="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}"
|
||||
ARCHIVE_URL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${PIPEWIRE_VERSION}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
|
||||
--retry 3 "${ARCHIVE_URL}" -o "${ARCHIVE_PATH}"
|
||||
echo "${PIPEWIRE_ARCHIVE_SHA256} ${ARCHIVE_PATH}" | sha256sum --check -
|
||||
tar -xzf "${ARCHIVE_PATH}" -C "${WORK_DIR}"
|
||||
|
||||
PIPEWIRE_INCLUDE_DIR="${INSTALL_PREFIX}/include/pipewire-0.3/pipewire"
|
||||
SPA_INCLUDE_DIR="${INSTALL_PREFIX}/include/spa-0.2/spa"
|
||||
|
||||
while IFS= read -r -d '' header_path; do
|
||||
relative_path="${header_path#${SOURCE_DIR}/src/pipewire/}"
|
||||
install -D -m 0644 "${header_path}" \
|
||||
"${PIPEWIRE_INCLUDE_DIR}/${relative_path}"
|
||||
done < <(find "${SOURCE_DIR}/src/pipewire" -type f -name '*.h' -print0)
|
||||
|
||||
while IFS= read -r -d '' header_path; do
|
||||
relative_path="${header_path#${SOURCE_DIR}/spa/include/spa/}"
|
||||
install -D -m 0644 "${header_path}" \
|
||||
"${SPA_INCLUDE_DIR}/${relative_path}"
|
||||
done < <(find "${SOURCE_DIR}/spa/include/spa" -type f -name '*.h' -print0)
|
||||
|
||||
sed \
|
||||
-e "s/@PIPEWIRE_VERSION_MAJOR@/${PIPEWIRE_VERSION_MAJOR}/g" \
|
||||
-e "s/@PIPEWIRE_VERSION_MINOR@/${PIPEWIRE_VERSION_MINOR}/g" \
|
||||
-e "s/@PIPEWIRE_VERSION_MICRO@/${PIPEWIRE_VERSION_MICRO}/g" \
|
||||
-e 's/@PIPEWIRE_API_VERSION@/"0.3"/g' \
|
||||
"${SOURCE_DIR}/src/pipewire/version.h.in" \
|
||||
> "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
chmod 0644 "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
|
||||
test -f "${PIPEWIRE_INCLUDE_DIR}/pipewire.h"
|
||||
test -f "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
test -f "${SPA_INCLUDE_DIR}/param/video/format-utils.h"
|
||||
|
||||
echo "Installed PipeWire ${PIPEWIRE_VERSION} header SDK under ${INSTALL_PREFIX}"
|
||||
@@ -2,19 +2,22 @@
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED_ARCH="${EXPECTED_ARCH:-}"
|
||||
EXPECTED_CMAKE_VERSION="${EXPECTED_CMAKE_VERSION:-3.31.6}"
|
||||
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
|
||||
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
|
||||
BUILD_CC="${CC:-/usr/bin/gcc-10}"
|
||||
BUILD_CXX="${CXX:-/usr/bin/g++-10}"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then
|
||||
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
|
||||
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "20.04" ]]; then
|
||||
echo "Expected Ubuntu 20.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
|
||||
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then
|
||||
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2
|
||||
if [[ "${GLIBC_VERSION}" != "2.31" ]]; then
|
||||
echo "Expected glibc 2.31, found ${GLIBC_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -28,13 +31,23 @@ XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
|
||||
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
|
||||
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
|
||||
cargo --version >/dev/null
|
||||
cmake --version >/dev/null
|
||||
g++ --version >/dev/null
|
||||
cmake --version | grep -F "cmake version ${EXPECTED_CMAKE_VERSION}" >/dev/null
|
||||
"${BUILD_CC}" --version | grep -F 'gcc-10' >/dev/null
|
||||
"${BUILD_CXX}" --version | grep -F 'g++-10' >/dev/null
|
||||
printf '%s\n' \
|
||||
'#include <concepts>' \
|
||||
'#include <span>' \
|
||||
'template <typename T> concept TestConcept = true;' \
|
||||
'int main() {' \
|
||||
' int value = 0;' \
|
||||
' std::span<int> values(&value, 1);' \
|
||||
' static_assert(TestConcept<int>);' \
|
||||
' return values.empty();' \
|
||||
'}' \
|
||||
| "${BUILD_CXX}" -std=c++20 -fsyntax-only -x c++ -
|
||||
for pkg_config_package in \
|
||||
dbus-1 \
|
||||
libdrm \
|
||||
libpipewire-0.3 \
|
||||
libspa-0.2 \
|
||||
xft; do
|
||||
if ! pkg-config --exists "${pkg_config_package}"; then
|
||||
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
|
||||
@@ -42,6 +55,25 @@ for pkg_config_package in \
|
||||
fi
|
||||
done
|
||||
|
||||
for sdk_header in \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h \
|
||||
/opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; do
|
||||
if [[ ! -f "${sdk_header}" ]]; then
|
||||
echo "Required PipeWire build SDK header is missing: ${sdk_header}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' \
|
||||
'#include <pipewire/pipewire.h>' \
|
||||
'#include <spa/param/video/format-utils.h>' \
|
||||
'static_assert(PW_CHECK_VERSION(0, 3, 48));' \
|
||||
'int main() { return 0; }' \
|
||||
| "${BUILD_CXX}" -std=c++17 -fsyntax-only -x c++ \
|
||||
-I/opt/crossdesk-pipewire-sdk/include/pipewire-0.3 \
|
||||
-I/opt/crossdesk-pipewire-sdk/include/spa-0.2 -
|
||||
|
||||
for package_path in \
|
||||
"s/slint" \
|
||||
"l/libdatachannel" \
|
||||
|
||||
@@ -7,7 +7,7 @@ if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
fi
|
||||
|
||||
INPUT_PATH="$1"
|
||||
MAXIMUM_VERSION="${2:-2.35}"
|
||||
MAXIMUM_VERSION="${2:-2.31}"
|
||||
|
||||
verify_elf() {
|
||||
local elf_file="$1"
|
||||
|
||||
@@ -104,6 +104,15 @@ OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
|
||||
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
|
||||
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
|
||||
|
||||
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
|
||||
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
|
||||
# runtime while using the host runtime on newer Ubuntu releases.
|
||||
if readelf -d "$BUILD_BINARY" \
|
||||
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
|
||||
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Slint is intentionally built as a shared library. Keep that runtime private
|
||||
# to CrossDesk instead of relying on a non-existent distribution package.
|
||||
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
|
||||
@@ -147,7 +156,7 @@ Version: $DEB_VERSION
|
||||
Architecture: $DEBIAN_ARCH
|
||||
Maintainer: $MAINTAINER
|
||||
Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6,
|
||||
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
#define CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// SendInput copies dwExtraInfo into KBDLLHOOKSTRUCT. Tag CrossDesk-generated
|
||||
// keyboard input so the controller hook can ignore only its own injections
|
||||
// while still accepting input from accessibility tools or remote sessions.
|
||||
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
@@ -17,6 +18,40 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static KeyCode ResolveX11Keycode(Display* display, int key_code,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (!display) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it != vkCodeToX11KeySym.end()) {
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display, static_cast<KeySym>(key_it->second));
|
||||
if (x11_keycode != 0) {
|
||||
return x11_keycode;
|
||||
}
|
||||
}
|
||||
|
||||
// Some controllers can preserve the physical Windows scan code even when
|
||||
// they cannot resolve a Windows virtual-key value. Xorg's evdev keycodes use
|
||||
// the Linux input code plus the protocol-reserved offset of 8.
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min_keycode = 0;
|
||||
int max_keycode = 0;
|
||||
XDisplayKeycodes(display, &min_keycode, &max_keycode);
|
||||
const int x11_keycode = evdev_keycode + 8;
|
||||
if (x11_keycode < min_keycode || x11_keycode > max_keycode) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<KeyCode>(x11_keycode);
|
||||
}
|
||||
|
||||
static KeySym NormalizeKeySym(KeySym key_sym) {
|
||||
if (key_sym >= XK_a && key_sym <= XK_z) {
|
||||
return key_sym - XK_a + XK_A;
|
||||
@@ -62,7 +97,16 @@ KeyboardCapturer::KeyboardCapturer()
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
return;
|
||||
}
|
||||
|
||||
int event_base = 0;
|
||||
int error_base = 0;
|
||||
int major_version = 0;
|
||||
int minor_version = 0;
|
||||
x11_xtest_available_ =
|
||||
XTestQueryExtension(display_, &event_base, &error_base, &major_version,
|
||||
&minor_version) != 0;
|
||||
}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {
|
||||
@@ -70,8 +114,10 @@ KeyboardCapturer::~KeyboardCapturer() {
|
||||
CleanupWaylandPortal();
|
||||
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
x11_xtest_available_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +202,6 @@ int KeyboardCapturer::Unhook() {
|
||||
|
||||
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;
|
||||
@@ -181,12 +225,33 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vkCodeToX11KeySym.find(key_code) != vkCodeToX11KeySym.end()) {
|
||||
int x11_key_code = vkCodeToX11KeySym[key_code];
|
||||
KeyCode keycode = XKeysymToKeycode(display_, x11_key_code);
|
||||
XTestFakeKeyEvent(display_, keycode, is_down, CurrentTime);
|
||||
XFlush(display_);
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
if (!x11_xtest_available_) {
|
||||
LOG_ERROR("XTest extension not available for keyboard injection");
|
||||
return -2;
|
||||
}
|
||||
|
||||
const KeyCode x11_keycode =
|
||||
ResolveX11Keycode(display_, key_code, scan_code, extended);
|
||||
if (x11_keycode == 0) {
|
||||
LOG_WARN(
|
||||
"Cannot map remote keyboard event to X11 keycode, vk_code={}, "
|
||||
"scan_code={}, extended={}",
|
||||
key_code, scan_code, extended);
|
||||
return -3;
|
||||
}
|
||||
|
||||
if (!XTestFakeKeyEvent(display_, x11_keycode, is_down, CurrentTime)) {
|
||||
LOG_ERROR(
|
||||
"XTest keyboard injection failed, vk_code={}, scan_code={}, "
|
||||
"extended={}, x11_keycode={}, is_down={}",
|
||||
key_code, scan_code, extended, static_cast<int>(x11_keycode), is_down);
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Complete the request before reporting success to the keyboard-state
|
||||
// reconciler. This also makes X11 protocol errors observable immediately.
|
||||
XSync(display_, False);
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -49,6 +50,8 @@ class KeyboardCapturer : public DeviceController {
|
||||
unsigned long root_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
std::mutex x11_injection_mutex_;
|
||||
bool x11_xtest_available_ = false;
|
||||
bool use_wayland_portal_ = false;
|
||||
bool wayland_init_attempted_ = false;
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
|
||||
@@ -611,6 +611,7 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
|
||||
@@ -1,36 +1,17 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <hidusage.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
constexpr wchar_t kRawInputWindowClassName[] =
|
||||
L"CrossDeskKeyboardRawInputWindow";
|
||||
|
||||
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) {
|
||||
bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
@@ -46,58 +27,247 @@ static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if ((kbData->flags & LLKHF_INJECTED) != 0) {
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
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(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {}
|
||||
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
if (capture_thread_.joinable()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
keyboard_hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
|
||||
if (!keyboard_hook_) {
|
||||
LOG_ERROR("Failed to install keyboard hook");
|
||||
on_key_action_ = on_key_action;
|
||||
user_ptr_ = user_ptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
capture_start_complete_ = false;
|
||||
capture_start_succeeded_ = false;
|
||||
}
|
||||
|
||||
capture_thread_ = std::thread(&KeyboardCapturer::RawInputThreadMain, this);
|
||||
|
||||
std::unique_lock<std::mutex> lock(capture_state_mutex_);
|
||||
capture_start_condition_.wait(
|
||||
lock, [this] { return capture_start_complete_; });
|
||||
const bool capture_started = capture_start_succeeded_;
|
||||
lock.unlock();
|
||||
|
||||
if (!capture_started) {
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
if (keyboard_hook_) {
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
UnhookWindowsHookEx(keyboard_hook_);
|
||||
keyboard_hook_ = nullptr;
|
||||
DWORD capture_thread_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id = capture_thread_id_;
|
||||
}
|
||||
if (capture_thread_id != 0 &&
|
||||
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
|
||||
LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
|
||||
capture_thread_id, GetLastError());
|
||||
}
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
void KeyboardCapturer::RawInputThreadMain() {
|
||||
const DWORD thread_id = GetCurrentThreadId();
|
||||
|
||||
MSG message{};
|
||||
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
|
||||
const bool capture_started = CreateRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = thread_id;
|
||||
capture_start_succeeded_ = capture_started;
|
||||
capture_start_complete_ = true;
|
||||
}
|
||||
capture_start_condition_.notify_one();
|
||||
|
||||
if (!capture_started) {
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
|
||||
while (true) {
|
||||
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
|
||||
if (get_message_result <= 0) {
|
||||
if (get_message_result < 0) {
|
||||
LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
|
||||
"error={}",
|
||||
thread_id, GetLastError());
|
||||
}
|
||||
break;
|
||||
}
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
|
||||
DestroyRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
}
|
||||
LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::CreateRawInputWindow() {
|
||||
const HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
WNDCLASSEXW window_class{};
|
||||
window_class.cbSize = sizeof(window_class);
|
||||
window_class.lpfnWndProc = &KeyboardCapturer::RawInputWindowProc;
|
||||
window_class.hInstance = instance;
|
||||
window_class.lpszClassName = kRawInputWindowClassName;
|
||||
|
||||
if (RegisterClassExW(&window_class) == 0) {
|
||||
const DWORD error = GetLastError();
|
||||
if (error != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
LOG_WARN("Failed to register keyboard raw input window class, error={}",
|
||||
error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
raw_input_window_ = CreateWindowExW(
|
||||
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
instance, this);
|
||||
if (!raw_input_window_) {
|
||||
LOG_WARN("Failed to create keyboard raw input window, error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
RAWINPUTDEVICE keyboard_device{};
|
||||
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
||||
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
||||
keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
||||
keyboard_device.hwndTarget = raw_input_window_;
|
||||
if (!RegisterRawInputDevices(&keyboard_device, 1,
|
||||
sizeof(keyboard_device))) {
|
||||
LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
|
||||
DestroyWindow(raw_input_window_);
|
||||
raw_input_window_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
raw_input_registered_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void KeyboardCapturer::DestroyRawInputWindow() {
|
||||
if (raw_input_registered_) {
|
||||
RAWINPUTDEVICE keyboard_device{};
|
||||
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
||||
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
||||
keyboard_device.dwFlags = RIDEV_REMOVE;
|
||||
keyboard_device.hwndTarget = nullptr;
|
||||
if (!RegisterRawInputDevices(&keyboard_device, 1,
|
||||
sizeof(keyboard_device))) {
|
||||
LOG_WARN("Failed to unregister keyboard raw input, error={}",
|
||||
GetLastError());
|
||||
}
|
||||
raw_input_registered_ = false;
|
||||
}
|
||||
if (raw_input_window_) {
|
||||
DestroyWindow(raw_input_window_);
|
||||
raw_input_window_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardCapturer::RawInputWindowProc(
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
KeyboardCapturer* capturer = reinterpret_cast<KeyboardCapturer*>(
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE) {
|
||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
|
||||
capturer = static_cast<KeyboardCapturer*>(create->lpCreateParams);
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(capturer));
|
||||
} else if (message == WM_INPUT && capturer) {
|
||||
capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(l_param));
|
||||
} else if (message == WM_NCDESTROY) {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(window, message, w_param, l_param);
|
||||
}
|
||||
|
||||
void KeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
|
||||
RAWINPUT input{};
|
||||
UINT input_size = sizeof(input);
|
||||
const UINT bytes_read =
|
||||
GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size,
|
||||
sizeof(RAWINPUTHEADER));
|
||||
if (bytes_read == static_cast<UINT>(-1) ||
|
||||
bytes_read < sizeof(RAWINPUTHEADER) ||
|
||||
input.header.dwType != RIM_TYPEKEYBOARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RAWKEYBOARD& keyboard = input.data.keyboard;
|
||||
if (keyboard.VKey == 0xFF ||
|
||||
keyboard.ExtraInformation ==
|
||||
static_cast<ULONG>(kInjectedKeyboardInputMarker)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_down = false;
|
||||
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
|
||||
is_down = true;
|
||||
} else if (keyboard.Message != WM_KEYUP &&
|
||||
keyboard.Message != WM_SYSKEYUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
|
||||
UINT mapped_scan_code = keyboard.MakeCode;
|
||||
if ((keyboard.Flags & RI_KEY_E0) != 0) {
|
||||
mapped_scan_code |= 0xE000;
|
||||
} else if ((keyboard.Flags & RI_KEY_E1) != 0) {
|
||||
mapped_scan_code |= 0xE100;
|
||||
}
|
||||
if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
|
||||
return;
|
||||
}
|
||||
|
||||
int key_code = static_cast<int>(keyboard.VKey);
|
||||
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
|
||||
const UINT normalized =
|
||||
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized != 0) {
|
||||
key_code = static_cast<int>(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (on_key_action_) {
|
||||
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remote keyboard commands to the local machine.
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo =
|
||||
static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
@@ -113,7 +283,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
input.ki.wVk = static_cast<WORD>(key_code);
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
@@ -134,7 +304,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
const 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());
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -26,8 +30,25 @@ class KeyboardCapturer : public DeviceController {
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message,
|
||||
WPARAM w_param, LPARAM l_param);
|
||||
|
||||
void RawInputThreadMain();
|
||||
bool CreateRawInputWindow();
|
||||
void DestroyRawInputWindow();
|
||||
void HandleRawInput(HRAWINPUT raw_input_handle);
|
||||
|
||||
OnKeyAction on_key_action_ = nullptr;
|
||||
void* user_ptr_ = nullptr;
|
||||
HWND raw_input_window_ = nullptr;
|
||||
bool raw_input_registered_ = false;
|
||||
std::thread capture_thread_;
|
||||
DWORD capture_thread_id_ = 0;
|
||||
bool capture_start_complete_ = false;
|
||||
bool capture_start_succeeded_ = false;
|
||||
std::mutex capture_state_mutex_;
|
||||
std::condition_variable capture_start_condition_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -943,10 +943,6 @@ void GuiApplication::InitializeSettings() {
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (!getenv("SDL_AUDIODRIVER")) {
|
||||
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
||||
}
|
||||
|
||||
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||
// position. Use the session's XWayland compatibility server for the GUI so
|
||||
// auxiliary windows such as the controlled-side status window can be
|
||||
@@ -970,6 +966,14 @@ bool GuiApplication::InitializeSDL() {
|
||||
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
const char* active_video_driver = SDL_GetCurrentVideoDriver();
|
||||
// X11 permits the manual move/position operations used by the frameless
|
||||
// main window. Keep native Wayland decorations as a compatibility fallback.
|
||||
use_x11_custom_titlebar_ =
|
||||
active_video_driver != nullptr &&
|
||||
std::strcmp(active_video_driver, "x11") == 0;
|
||||
#endif
|
||||
|
||||
if (const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(0)) {
|
||||
screen_width_ = mode->w;
|
||||
@@ -1004,10 +1008,8 @@ void GuiApplication::InitializeUi() {
|
||||
#if _WIN32
|
||||
ui_->main->set_custom_titlebar(true);
|
||||
#elif defined(__linux__)
|
||||
if (use_xwayland_gui_) {
|
||||
ui_->main->set_custom_titlebar(true);
|
||||
ui_->main->set_wayland_titlebar(true);
|
||||
}
|
||||
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
|
||||
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
|
||||
#endif
|
||||
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
||||
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||
@@ -3040,9 +3042,8 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
|
||||
controlled_remote_id_ = props->remote_id_;
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
// Native hooks see the same physical key before Slint does. When a native
|
||||
// hook is active, forwarding the FocusScope callback as well would duplicate
|
||||
// the event on platforms whose hook does not consume the local key.
|
||||
// Native capture forwards the event independently. Slint keyboard events
|
||||
// are used only by platforms whose native capture backend is unavailable.
|
||||
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ private:
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
bool use_x11_custom_titlebar_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -256,18 +257,18 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
void *user_ptr) {
|
||||
if (user_ptr) {
|
||||
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
|
||||
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
||||
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
},
|
||||
&owner_);
|
||||
this);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native hook");
|
||||
LOG_INFO("Start keyboard capturer with native input");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -281,6 +282,7 @@ int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
|
||||
if (keyboard_capturer_) {
|
||||
keyboard_capturer_->Unhook();
|
||||
ClearCapturedKeyboardInput();
|
||||
LOG_INFO("Stop keyboard capturer");
|
||||
}
|
||||
return 0;
|
||||
@@ -410,6 +412,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_capturer_is_started_ = true;
|
||||
}
|
||||
if (owner_.keyboard_capturer_is_started_) {
|
||||
DrainCapturedKeyboardInput();
|
||||
owner_.keyboard_.SendHeartbeat(false);
|
||||
}
|
||||
} else if (owner_.keyboard_capturer_is_started_) {
|
||||
@@ -421,6 +424,36 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_.CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
|
||||
bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
|
||||
captured_keyboard_inputs_.pop_front();
|
||||
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
|
||||
}
|
||||
captured_keyboard_inputs_.push_back(
|
||||
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
std::deque<CapturedKeyboardInput> inputs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
input.scan_code, input.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionDeviceManager::ClearCapturedKeyboardInput() {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
captured_keyboard_inputs_.clear();
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -53,6 +55,18 @@ public:
|
||||
const std::vector<DisplayInfo> &display_info_list() const;
|
||||
|
||||
private:
|
||||
struct CapturedKeyboardInput {
|
||||
int key_code = 0;
|
||||
bool is_down = false;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
void QueueCapturedKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended);
|
||||
void DrainCapturedKeyboardInput();
|
||||
void ClearCapturedKeyboardInput();
|
||||
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
@@ -63,6 +77,8 @@ private:
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -45,8 +45,8 @@ int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
bool *extended_out) {
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (!scan_code_out || !extended_out) {
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
@@ -77,8 +77,8 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
stage ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
const RemoteAction &action) {
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||
const RemoteAction& action) {
|
||||
return response.is_object() &&
|
||||
response.value("error", std::string()) == "send_input_failed" &&
|
||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||
@@ -87,9 +87,9 @@ bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
|
||||
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
|
||||
|
||||
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
@@ -106,13 +106,13 @@ void KeyboardController::ForceReleasePressedKeys() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
pressed_keys.reserve(pressed_keys_.size());
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
pressed_keys.push_back(key);
|
||||
}
|
||||
pressed_keys_.clear();
|
||||
}
|
||||
|
||||
for (const PressedKey &key : pressed_keys) {
|
||||
for (const PressedKey& key : pressed_keys) {
|
||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
SendHeartbeat(true);
|
||||
@@ -130,7 +130,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
size_t index = 0;
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
if (index >= kMaxKeyboardStateKeys) {
|
||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||
pressed_keys_.size());
|
||||
@@ -223,9 +223,10 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
||||
LOG_INFO("Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -245,15 +246,15 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
extended);
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
const int key_code = static_cast<int>(action.k.key_value);
|
||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||
const bool injected =
|
||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
auto& state = remote_states_[remote_id];
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (is_down && injected) {
|
||||
state.pressed_keys[key_code] =
|
||||
@@ -263,13 +264,13 @@ void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
std::vector<PressedKey> keys_to_press;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
auto& state = remote_states_[remote_id];
|
||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
@@ -283,24 +284,24 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const size_t count =
|
||||
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
const auto &key = action.ks.pressed_keys[index];
|
||||
const auto& key = action.ks.pressed_keys[index];
|
||||
const int key_code = static_cast<int>(key.key_value);
|
||||
desired_keys[key_code] =
|
||||
PressedKey{key_code, key.scan_code, key.extended};
|
||||
}
|
||||
for (const auto &[key_code, key] : state.pressed_keys) {
|
||||
for (const auto& [key_code, key] : state.pressed_keys) {
|
||||
if (desired_keys.find(key_code) == desired_keys.end()) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
}
|
||||
for (const auto &[key_code, key] : desired_keys) {
|
||||
for (const auto& [key_code, key] : desired_keys) {
|
||||
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
|
||||
keys_to_press.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const PressedKey &key : keys_to_release) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
@@ -309,7 +310,7 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PressedKey &key : keys_to_press) {
|
||||
for (const PressedKey& key : keys_to_press) {
|
||||
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
||||
@@ -317,8 +318,8 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason) {
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
@@ -326,7 +327,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
if (state_it == remote_states_.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[_, key] : state_it->second.pressed_keys) {
|
||||
for (const auto& [_, key] : state_it->second.pressed_keys) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
remote_states_.erase(state_it);
|
||||
@@ -336,7 +337,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKey &key : keys_to_release) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +347,7 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
for (const auto &[remote_id, state] : remote_states_) {
|
||||
for (const auto& [remote_id, state] : remote_states_) {
|
||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||
state.last_seen_tick != 0 &&
|
||||
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
||||
@@ -354,9 +355,9 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::string &remote_id : timed_out_remotes) {
|
||||
for (const std::string& remote_id : timed_out_remotes) {
|
||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -601,6 +601,15 @@ export component MainWindow inherits Window {
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 4px;
|
||||
// Measure the ID using the preferred field size.
|
||||
// The visible text can then scale only on systems
|
||||
// whose font metrics would otherwise clip it.
|
||||
local-id-width-probe := Text {
|
||||
visible: false;
|
||||
text: root.local-id;
|
||||
font-size: ImGuiFontStyle.field-value;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
Text {
|
||||
x: 8px;
|
||||
// Compensate for the font's ascender-heavy
|
||||
@@ -610,7 +619,9 @@ export component MainWindow inherits Window {
|
||||
height: parent.height - 4px;
|
||||
text: root.local-id;
|
||||
color: #20242a;
|
||||
font-size: ImGuiFontStyle.field-value;
|
||||
font-size: max(18px,
|
||||
ImGuiFontStyle.field-value *
|
||||
(self.width / max(self.width, local-id-width-probe.preferred-width)));
|
||||
letter-spacing: 1px;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -828,6 +829,8 @@ InputInjectionResult InjectKeyboardInput(
|
||||
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(
|
||||
crossdesk::kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: 00aad6aa37...50ca51ecc1
@@ -0,0 +1,52 @@
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
int main() {
|
||||
Display* receiver = XOpenDisplay(nullptr);
|
||||
if (!receiver) {
|
||||
std::fprintf(stderr, "open display failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int screen = DefaultScreen(receiver);
|
||||
Window window = XCreateSimpleWindow(receiver, RootWindow(receiver, screen),
|
||||
0, 0, 100, 100, 0, 0, 0);
|
||||
XSelectInput(receiver, window, KeyPressMask | KeyReleaseMask);
|
||||
XMapWindow(receiver, window);
|
||||
XSync(receiver, False);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
XSetInputFocus(receiver, window, RevertToParent, CurrentTime);
|
||||
XSync(receiver, False);
|
||||
|
||||
crossdesk::KeyboardCapturer keyboard;
|
||||
// Exercise the scan-code fallback: the controller supplied no usable VK,
|
||||
// but did preserve the set-1 scan code for the A key.
|
||||
const int down = keyboard.SendKeyboardCommand(0, true, 0x1E, false);
|
||||
const int up = keyboard.SendKeyboardCommand(0, false, 0x1E, false);
|
||||
|
||||
bool saw_down = false;
|
||||
bool saw_up = false;
|
||||
const auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::seconds(2);
|
||||
while (std::chrono::steady_clock::now() < deadline &&
|
||||
(!saw_down || !saw_up)) {
|
||||
while (XPending(receiver) > 0) {
|
||||
XEvent event{};
|
||||
XNextEvent(receiver, &event);
|
||||
saw_down = saw_down || event.type == KeyPress;
|
||||
saw_up = saw_up || event.type == KeyRelease;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
XDestroyWindow(receiver, window);
|
||||
XCloseDisplay(receiver);
|
||||
std::printf("down_ret=%d up_ret=%d saw_down=%d saw_up=%d\n", down, up,
|
||||
saw_down, saw_up);
|
||||
return down == 0 && up == 0 && saw_down && saw_up ? 0 : 2;
|
||||
}
|
||||
Vendored
+17
-1
@@ -9,10 +9,13 @@ package("slint")
|
||||
|
||||
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
|
||||
|
||||
add_deps("cmake")
|
||||
add_deps("cmake~slint >=3.21 <4.0", {host = true, private = true, system = false})
|
||||
add_deps("rust 1.92.0", {host = true, private = true, system = false})
|
||||
|
||||
on_load(function(package)
|
||||
if package:is_plat("linux") then
|
||||
package:add("deps", "fontconfig", {private = true, system = false})
|
||||
end
|
||||
package:add("includedirs", "include/slint")
|
||||
if package:is_plat("windows") then
|
||||
package:add("links", "slint_cpp.dll")
|
||||
@@ -22,6 +25,13 @@ package("slint")
|
||||
end)
|
||||
|
||||
on_install("windows", "linux", "macosx", function(package)
|
||||
for _, dep in ipairs(package:orderdeps()) do
|
||||
if dep:name() == "cmake" and dep:label() == "slint" then
|
||||
os.addenv("PATH", dep:installdir("bin"))
|
||||
import("core.cache.detectcache"):set2("find_program", "cmake", nil)
|
||||
break
|
||||
end
|
||||
end
|
||||
local configs = {
|
||||
"-DSLINT_BUILD_TESTING=OFF",
|
||||
"-DSLINT_BUILD_EXAMPLES=OFF",
|
||||
@@ -39,6 +49,12 @@ package("slint")
|
||||
"-DSLINT_STYLE=fluent",
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
}
|
||||
if package:is_plat("windows") then
|
||||
local rc = assert(package:build_getenv("mrc"),
|
||||
"failed to find the Windows resource compiler")
|
||||
rc = rc:gsub("\\", "/")
|
||||
table.insert(configs, "-DCMAKE_RC_COMPILER=" .. rc)
|
||||
end
|
||||
import("package.tools.cmake").install(package, configs)
|
||||
end)
|
||||
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ function setup_options_and_dependencies()
|
||||
add_defines("CROSSDESK_DEBUG")
|
||||
end
|
||||
|
||||
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||
add_requires("spdlog 1.14.1", {system = false})
|
||||
add_requires("slint 1.17.1", {configs = {shared = true}})
|
||||
add_requires("libsdl3 3.2.26", {configs = {shared = false}})
|
||||
|
||||
+3
-1
@@ -64,10 +64,12 @@ function setup_platform_settings()
|
||||
"/usr/local/include/pipewire-0.3",
|
||||
"/usr/include/pipewire",
|
||||
"/usr/local/include/pipewire",
|
||||
"/opt/crossdesk-pipewire-sdk/include/pipewire-0.3",
|
||||
"/usr/include/spa-0.2",
|
||||
"/usr/local/include/spa-0.2",
|
||||
"/usr/include/spa",
|
||||
"/usr/local/include/spa"
|
||||
"/usr/local/include/spa",
|
||||
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
|
||||
}, {system = true})
|
||||
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
|
||||
add_includedirs(include_dir, {system = true})
|
||||
|
||||
+9
-1
@@ -197,6 +197,14 @@ function setup_targets()
|
||||
"src/device_controller/keyboard/linux", {public = true})
|
||||
end
|
||||
|
||||
if is_os("linux") then
|
||||
target("linux_keyboard_x11_integration_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_deps("device_controller")
|
||||
add_files("tests/linux_keyboard_x11_integration_test.cpp")
|
||||
end
|
||||
|
||||
target("thumbnail")
|
||||
set_kind("object")
|
||||
add_packages("libyuv", "openssl3")
|
||||
@@ -307,7 +315,7 @@ function setup_targets()
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files(crossdesk_windows_resource)
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_includedirs("src/common", "src/service/windows", {public = true})
|
||||
end
|
||||
|
||||
target("crossdesk")
|
||||
|
||||
Reference in New Issue
Block a user