Compare commits

...

12 Commits

Author SHA1 Message Date
dijunkun a7897a003d ci: update Linux build image to Ubuntu 20.04 2026-07-30 12:32:46 +08:00
dijunkun d2478006ba ci: build Linux releases on Ubuntu 20.04 for compatibility 2026-07-30 12:32:16 +08:00
dijunkun d32974dc18 ci: add Linux capture build dependencies 2026-07-28 17:58:39 +08:00
dijunkun efd8b7c0ff fix: let ICE drive connection failure state 2026-07-28 17:43:25 +08:00
dijunkun 383452ed3b ci: enable Wayland for Linux release builds 2026-07-28 17:25:49 +08:00
dijunkun 2570e57958 ci: use macOS 26 SDK for release artifacts 2026-07-28 16:29:32 +08:00
dijunkun eed90dad34 revert: keep macOS main window titlebar opaque 2026-07-28 16:29:32 +08:00
dijunkun c53ce2a581 fix: prevent video callback crash during transport teardown 2026-07-28 15:26:13 +08:00
dijunkun 708c39bb99 fix: keep macOS main window titlebar opaque 2026-07-28 14:42:06 +08:00
dijunkun cee249a25b fix: align DTLS handling with SRTP negotiation 2026-07-28 14:11:17 +08:00
dijunkun 7c3b13409c fix: restore remote keyboard control in Slint client, refs #91 2026-07-28 14:11:16 +08:00
dijunkun 7ff6c8a4ad ci: add Linux build image workflow 2026-07-21 22:35:02 +08:00
31 changed files with 576 additions and 146 deletions
+6 -4
View File
@@ -26,8 +26,9 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
IMAGE_NAME: crossdesk/ubuntu22.04 IMAGE_NAME: crossdesk/ubuntu20.04
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0 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 XMAKE_VERSION: 3.0.9
RUST_VERSION: 1.92.0 RUST_VERSION: 1.92.0
RUSTUP_VERSION: 1.28.2 RUSTUP_VERSION: 1.28.2
@@ -44,11 +45,11 @@ jobs:
- arch: amd64 - arch: amd64
platform: linux/amd64 platform: linux/amd64
runner: ubuntu-24.04 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 - arch: arm64
platform: linux/arm64 platform: linux/arm64
runner: ubuntu-24.04-arm runner: ubuntu-24.04-arm
base_image: ubuntu:22.04 base_image: ubuntu:20.04
steps: steps:
- name: Set candidate tag - name: Set candidate tag
id: image id: image
@@ -86,6 +87,7 @@ jobs:
sbom: false sbom: false
build-args: | build-args: |
BASE_IMAGE=${{ matrix.base_image }} BASE_IMAGE=${{ matrix.base_image }}
CMAKE_VERSION=${{ env.CMAKE_VERSION }}
XMAKE_VERSION=${{ env.XMAKE_VERSION }} XMAKE_VERSION=${{ env.XMAKE_VERSION }}
RUST_VERSION=${{ env.RUST_VERSION }} RUST_VERSION=${{ env.RUST_VERSION }}
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }} RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
+50 -12
View File
@@ -29,12 +29,12 @@ jobs:
include: include:
- arch: amd64 - arch: amd64
runner: ubuntu-24.04 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 package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64 - arch: arm64
runner: ubuntu-24.04-arm 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 package_script: ./scripts/linux/pkg_arm64.sh
container: container:
@@ -103,12 +103,33 @@ jobs:
- name: Verify Linux build image - name: Verify Linux build image
run: verify-crossdesk-build-image run: verify-crossdesk-build-image
- name: Ensure Wayland build dependencies
shell: bash
run: |
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
rm -rf /var/lib/apt/lists/*
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 - name: Build CrossDesk
env: env:
CUDA_PATH: /usr/local/cuda CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data XMAKE_GLOBALDIR: /data
run: | run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=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 xmake b -vy --root crossdesk
- name: Package - name: Package
@@ -116,13 +137,19 @@ jobs:
chmod +x ${{ matrix.package_script }} chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION} ${{ 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: | run: |
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb" PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}" VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
install -d "${VERIFY_DIR}" install -d "${VERIFY_DIR}"
dpkg-deb --extract "${PACKAGE_FILE}" "${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 - name: Upload artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
@@ -136,19 +163,20 @@ jobs:
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
env: env:
MACOSX_DEPLOYMENT_TARGET: "14.0" MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
strategy: strategy:
matrix: matrix:
include: include:
- arch: x64 - arch: x64
binary_arch: x86_64 binary_arch: x86_64
runner: macos-15-intel runner: macos-26-intel
cache-key: intel-macos14 cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64 - arch: arm64
binary_arch: arm64 binary_arch: arm64
runner: macos-14 runner: macos-26
cache-key: arm-macos14 cache-key: arm-macos26-xcode26.6-min14
out-dir: ./build/macosx/arm64/release/crossdesk out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh package_script: ./scripts/macosx/pkg_arm64.sh
@@ -193,6 +221,17 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Select Xcode 26.6
shell: bash
run: |
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
xcodebuild -version
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
if [[ "${SDK_VERSION}" != "${EXPECTED_MACOS_SDK}" ]]; then
echo "Unexpected macOS SDK: expected ${EXPECTED_MACOS_SDK}, got ${SDK_VERSION}" >&2
exit 1
fi
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
@@ -237,7 +276,7 @@ jobs:
exit 1 exit 1
fi fi
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" ./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
@@ -424,8 +463,7 @@ jobs:
release: release:
name: Publish Release name: Publish Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: needs: [build-linux, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
+27 -8
View File
@@ -32,7 +32,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|----------------|---------------------------| |----------------|---------------------------|
| **Windows** | Windows 10 及以上 (64 位) | | **Windows** | Windows 10 及以上 (64 位) |
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 | | **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) | | **Linux** | Ubuntu 20.04 及以上 |
## 使用 ## 使用
@@ -80,27 +80,39 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
依赖: 依赖:
- [xmake](https://xmake.io/#/guide/installation) - [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) - [cmake](https://cmake.org/download/) 3.21 及以上
### Linux ### Linux
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖: Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
```bash ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ 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 \ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev libasound2-dev libsndio-dev libpulse-dev
# Ubuntu 20.04 自带的 CMake 3.16 不满足 Slint 1.17 的要求
python3 -m pip install --user cmake==3.31.6
export PATH="$HOME/.local/bin:$PATH"
``` ```
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
因此上述命令同时安装并在后续配置中指定 GCC 10。
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖: 需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
```bash ```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 版本: 下载子模块并编译 Release 版本:
@@ -112,7 +124,11 @@ cd crossdesk
# 已经克隆过仓库时执行这一行 # 已经克隆过仓库时执行这一行
git submodule update --init --recursive 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 -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -121,7 +137,8 @@ amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`arm64 构建
启用 Wayland 和 DRM 的配置示例: 启用 Wayland 和 DRM 的配置示例:
```bash ```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -136,6 +153,8 @@ xmake b -vy crossdesk
``` ```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so` 打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项 ### 通用编译选项
@@ -158,7 +177,7 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持 ### 无 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 安装包
进入容器,下载工程后执行: 进入容器,下载工程后执行:
``` ```
+32 -9
View File
@@ -32,7 +32,7 @@ CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkund
|-----------|-----------------| |-----------|-----------------|
| **Windows** | Windows 10 or later (64-bit) | | **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 | | **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 ## Usage
@@ -85,27 +85,41 @@ If the remote Windows service is not installed, not running, or temporarily unav
Requirements: Requirements:
- [xmake](https://xmake.io/#/guide/installation) - [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/) - [cmake](https://cmake.org/download/) 3.21 or later
### Linux ### 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 ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ 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 \ libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \ libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \ libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \ libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
libasound2-dev libsndio-dev libpulse-dev libasound2-dev libsndio-dev libpulse-dev
# Ubuntu 20.04's CMake 3.16 is too old for Slint 1.17.
python3 -m pip install --user cmake==3.31.6
export PATH="$HOME/.local/bin:$PATH"
``` ```
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: Install the optional dependencies when Wayland capture or DRM capture is enabled:
```bash ```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: Clone the submodules and build a release binary:
@@ -117,7 +131,12 @@ cd crossdesk
# Run this when the repository has already been cloned # Run this when the repository has already been cloned
git submodule update --init --recursive 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 -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -126,7 +145,8 @@ The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64
Example configuration with Wayland and DRM capture enabled: Example configuration with Wayland and DRM capture enabled:
```bash ```bash
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
--cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk xmake b -vy crossdesk
``` ```
@@ -141,6 +161,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. 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 ### Common build options
@@ -163,8 +186,8 @@ xmake r crossdesk
#### Development Without CUDA Environment #### 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. 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 comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup. 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: After entering the container, download the project and run:
``` ```
+25 -4
View File
@@ -1,15 +1,16 @@
# syntax=docker/dockerfile:1.7 # 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} FROM ${BASE_IMAGE}
ARG TARGETARCH ARG TARGETARCH
ARG CMAKE_VERSION=3.31.6
ARG XMAKE_VERSION=3.0.9 ARG XMAKE_VERSION=3.0.9
ARG RUST_VERSION=1.92.0 ARG RUST_VERSION=1.92.0
ARG RUSTUP_VERSION=1.28.2 ARG RUSTUP_VERSION=1.28.2
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \ 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.source="https://github.com/kunkundi/crossdesk" \
org.opencontainers.image.licenses="GPL-3.0-only" org.opencontainers.image.licenses="GPL-3.0-only"
@@ -22,6 +23,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
XMAKE_GLOBALDIR=/data \ XMAKE_GLOBALDIR=/data \
XMAKE_ROOT=y \ XMAKE_ROOT=y \
CUDA_PATH=/usr/local/cuda \ 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} PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
@@ -35,13 +38,14 @@ RUN apt-get update \
build-essential \ build-essential \
ca-certificates \ ca-certificates \
ccache \ ccache \
cmake \
curl \ curl \
dbus \ dbus \
dpkg-dev \ dpkg-dev \
file \ file \
flex \ flex \
gettext \ gettext \
gcc-10 \
g++-10 \
git \ git \
libasound2-dev \ libasound2-dev \
libdbus-1-dev \ libdbus-1-dev \
@@ -93,6 +97,22 @@ RUN apt-get update \
zlib1g-dev \ zlib1g-dev \
&& rm -rf /var/lib/apt/lists/* && 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 \ RUN export GITHUB_ACTIONS=true \
&& curl -fsSL https://xmake.io/shget.text \ && curl -fsSL https://xmake.io/shget.text \
| bash -s -- "v${XMAKE_VERSION}" \ | bash -s -- "v${XMAKE_VERSION}" \
@@ -134,7 +154,8 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
# CrossDesk's exact dependency graph. Source files are intentionally excluded # CrossDesk's exact dependency graph. Source files are intentionally excluded
# from the Docker context; source-only changes therefore reuse this layer. # from the Docker context; source-only changes therefore reuse this layer.
RUN xmake repo -u --root \ RUN xmake repo -u --root \
&& xmake f -m release --USE_CUDA=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 \ && rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
&& mkdir -p /workspace && mkdir -p /workspace
+15 -8
View File
@@ -1,7 +1,9 @@
# CrossDesk Linux 构建镜像 # CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake 这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。 CMake 3.31、xmake、Rust、Linux 开发库(包括 D-Bus、DRM,以及仅用于编译的
PipeWire 0.3/SPA 头文件 SDK)以及根据项目
`xmake.lua` 提前编译好的依赖。
## 发布方式 ## 发布方式
@@ -10,14 +12,19 @@ Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
全部通过后再合并为同一个 multi-arch 镜像: 全部通过后再合并为同一个 multi-arch 镜像:
```text ```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 变体以 Docker 会根据运行机器自动选择正确架构。amd64 变体以
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建 `nvidia/cuda:12.6.3-devel-ubuntu20.04` 为基础;arm64 变体不包含当前构建
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。 用不到的 CUDA SDK。两个变体都使用 Ubuntu 20.04/glibc 2.31,因此生成的
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容 单一 Linux 安装包可运行在 Ubuntu 20.04 及更高版本。
Ubuntu 20.04。
Ubuntu 20.04 仓库只提供 PipeWire 0.2。镜像通过
`install-pipewire-sdk.sh` 安装固定版本的 PipeWire 0.3.48/SPA 头文件,但不
安装或打包 PipeWire 动态库。CrossDesk 在运行时通过 `dlopen()` 使用宿主系统
提供的 PipeWire 0.3Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的 每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
工具链版本 tag 只会在两个架构都验证成功后更新。 工具链版本 tag 只会在两个架构都验证成功后更新。
@@ -26,7 +33,7 @@ Ubuntu 20.04。
在仓库的 Actions secrets 中配置: 在仓库的 Actions secrets 中配置:
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名 - `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu20.04` 的 Docker Hub 用户名
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token - `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布 工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
+55
View File
@@ -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}"
+49 -7
View File
@@ -2,19 +2,22 @@
set -euo pipefail set -euo pipefail
EXPECTED_ARCH="${EXPECTED_ARCH:-}" EXPECTED_ARCH="${EXPECTED_ARCH:-}"
EXPECTED_CMAKE_VERSION="${EXPECTED_CMAKE_VERSION:-3.31.6}"
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}" EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}" 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 # shellcheck disable=SC1091
source /etc/os-release source /etc/os-release
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "20.04" ]]; then
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2 echo "Expected Ubuntu 20.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
exit 1 exit 1
fi fi
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')" GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then if [[ "${GLIBC_VERSION}" != "2.31" ]]; then
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2 echo "Expected glibc 2.31, found ${GLIBC_VERSION}" >&2
exit 1 exit 1
fi fi
@@ -28,9 +31,48 @@ XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
cargo --version >/dev/null cargo --version >/dev/null
cmake --version >/dev/null cmake --version | grep -F "cmake version ${EXPECTED_CMAKE_VERSION}" >/dev/null
g++ --version >/dev/null "${BUILD_CC}" --version | grep -F 'gcc-10' >/dev/null
pkg-config --exists xft "${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 \
xft; do
if ! pkg-config --exists "${pkg_config_package}"; then
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
exit 1
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 \ for package_path in \
"s/slint" \ "s/slint" \
+1 -1
View File
@@ -7,7 +7,7 @@ if [[ $# -lt 1 || $# -gt 2 ]]; then
fi fi
INPUT_PATH="$1" INPUT_PATH="$1"
MAXIMUM_VERSION="${2:-2.35}" MAXIMUM_VERSION="${2:-2.31}"
verify_elf() { verify_elf() {
local elf_file="$1" local elf_file="$1"
+10 -1
View File
@@ -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 -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin" 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 # Slint is intentionally built as a shared library. Keep that runtime private
# to CrossDesk instead of relying on a non-existent distribution package. # to CrossDesk instead of relying on a non-existent distribution package.
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \ SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
@@ -147,7 +156,7 @@ Version: $DEB_VERSION
Architecture: $DEBIAN_ARCH Architecture: $DEBIAN_ARCH
Maintainer: $MAINTAINER Maintainer: $MAINTAINER
Description: $DESCRIPTION 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, libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
+20 -2
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
if [[ $# -ne 2 ]]; then if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2 echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2 exit 2
fi fi
APP_BUNDLE="$1" APP_BUNDLE="$1"
EXPECTED_ARCH="$2" EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk" APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks" FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist" INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
@@ -94,6 +95,23 @@ if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION"
exit 1 exit 1
fi fi
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
verify_sdk_version() {
local macho="$1"
local actual_sdk=""
actual_sdk="$(otool -l "$macho" | awk '
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
build_version && $1 == "sdk" { print $2; exit }')"
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
return 1
fi
}
verify_sdk_version "$APP_EXECUTABLE"
verify_sdk_version "$SLINT_LIBRARY"
fi
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)" echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map> #include <unordered_map>
#include "keyboard_converter.h" #include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr; static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr; static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk; static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) { static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') { if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) { CGEventRef event, void* userInfo) {
(void)proxy; (void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) { if (!g_on_key_action) {
return event; return event;
} }
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
} }
CGEventSetFlags(event, ToCGEventFlags(injected_flags)); CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event); CGEventPost(kCGHIDEventTap, event);
CFRelease(event); CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release"); LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1; return -1;
} }
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, fn_release_event); CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event); CFRelease(fn_release_event);
} }
@@ -49,6 +49,9 @@ static bool PreferSideSpecificVkInjection(int key_code) {
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) { if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam); KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if ((kbData->flags & LLKHF_INJECTED) != 0) {
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
const int key_code = NormalizeModifierVkCode(kbData); const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) { if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
+1 -1
View File
@@ -65,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false; bool start_keyboard_capturer_ = false;
bool show_cursor_ = false; bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false; bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false; bool keyboard_capturer_uses_window_events_ = false;
bool foucs_on_main_window_ = false; bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false; bool focus_on_stream_window_ = false;
bool audio_capture_ = false; bool audio_capture_ = false;
+73 -12
View File
@@ -30,6 +30,8 @@
#include "localization.h" #include "localization.h"
#include "platform.h" #include "platform.h"
#if _WIN32 #if _WIN32
#include <windows.h>
#include "platform/tray/win_tray.h" #include "platform/tray/win_tray.h"
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include "platform/tray/mac_tray.h" #include "platform/tray/mac_tray.h"
@@ -1726,6 +1728,8 @@ void GuiApplication::BindStreamCallbacks() {
bool control, bool alt, bool shift, bool meta) { bool control, bool alt, bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta); SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
}); });
stream->on_keyboard_focus_changed(
[this](bool focused) { SetStreamKeyboardFocus(focused); });
} }
void GuiApplication::BindServerCallbacks() { void GuiApplication::BindServerCallbacks() {
@@ -1829,8 +1833,13 @@ void GuiApplication::Tick() {
} }
HandleConnectionStatusChange(); HandleConnectionStatusChange();
HandlePendingPresenceProbe(); HandlePendingPresenceProbe();
HandleConnectionTimeouts(); HandlePresenceProbeTimeout();
HandleWindowsServiceIntegration(); HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions(); devices_.UpdateInteractions();
UpdateLocalization(); UpdateLocalization();
@@ -1839,9 +1848,6 @@ void GuiApplication::Tick() {
SyncPlatformDialogs(); SyncPlatformDialogs();
SyncStreamWindow(); SyncStreamWindow();
SyncServerWindow(); SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#endif
} }
void GuiApplication::UpdateLocalization() { void GuiApplication::UpdateLocalization() {
@@ -2228,6 +2234,7 @@ void GuiApplication::SyncXWaylandWindowActivation() {
Display* display = ui_->x11_focus_display; Display* display = ui_->x11_focus_display;
const ::Window active_window = X11GetActiveWindow(display); const ::Window active_window = X11GetActiveWindow(display);
if (!active_window) { if (!active_window) {
SetStreamKeyboardFocus(false);
return; return;
} }
@@ -2237,19 +2244,49 @@ void GuiApplication::SyncXWaylandWindowActivation() {
if (ui_->stream) { if (ui_->stream) {
(*ui_->stream)->set_window_active(false); (*ui_->stream)->set_window_active(false);
} }
SetStreamKeyboardFocus(false);
return; return;
} }
ui_->main->set_window_active( ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window())); X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) { if (ui_->stream) {
(*ui_->stream) const bool stream_active =
->set_window_active( X11WindowMatches(display, active_window, (*ui_->stream)->window());
X11WindowMatches(display, active_window, (*ui_->stream)->window())); (*ui_->stream)->set_window_active(stream_active);
SetStreamKeyboardFocus(stream_active);
} else {
SetStreamKeyboardFocus(false);
} }
} }
#endif #endif
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
if (focus_on_stream_window_ == focused) {
return;
}
focus_on_stream_window_ = focused;
if (!focused) {
keyboard_.ForceReleasePressedKeys();
}
}
void GuiApplication::SyncStreamKeyboardFocus() {
if (!ui_ || !ui_->stream || !(*ui_->stream)->window().is_visible()) {
SetStreamKeyboardFocus(false);
return;
}
#if _WIN32
const HWND stream_hwnd = (*ui_->stream)->window().win32_hwnd();
SetStreamKeyboardFocus(stream_hwnd != nullptr &&
GetForegroundWindow() == stream_hwnd);
#elif defined(__APPLE__)
SetStreamKeyboardFocus(IsStreamWindowActive());
#endif
}
void GuiApplication::SyncStreamWindow() { void GuiApplication::SyncStreamWindow() {
bool has_sessions = false; bool has_sessions = false;
{ {
@@ -2306,6 +2343,7 @@ void GuiApplication::SyncStreamWindow() {
} }
} }
if (!has_sessions) { if (!has_sessions) {
SetStreamKeyboardFocus(false);
#if defined(__APPLE__) #if defined(__APPLE__)
SetStreamWindowFullscreen(false); SetStreamWindowFullscreen(false);
#endif #endif
@@ -2787,14 +2825,25 @@ void GuiApplication::SelectStreamTab(int index) {
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) { if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
return; return;
} }
focused_remote_id_ = ui_->tab_ids[index]; const std::string selected_remote_id = ui_->tab_ids[index];
controlled_remote_id_ = focused_remote_id_; if (!controlled_remote_id_.empty() &&
controlled_remote_id_ != selected_remote_id) {
keyboard_.ForceReleasePressedKeys();
}
focused_remote_id_ = selected_remote_id;
controlled_remote_id_ = selected_remote_id;
std::shared_lock lock(remote_sessions_mutex_); std::shared_lock lock(remote_sessions_mutex_);
for (auto& [id, props] : remote_sessions_) { for (auto& [id, props] : remote_sessions_) {
if (props) { if (props) {
props->tab_selected_ = id == focused_remote_id_; props->tab_selected_ = id == focused_remote_id_;
} }
} }
const auto selected = remote_sessions_.find(selected_remote_id);
start_keyboard_capturer_ = selected != remote_sessions_.end() &&
selected->second &&
selected->second->control_mouse_ &&
selected->second->connection_status_.load() ==
ConnectionStatus::Connected;
} }
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) { void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
@@ -2982,10 +3031,22 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
(void)alt; (void)alt;
(void)shift; (void)shift;
(void)meta; (void)meta;
if (auto props = SelectedSession()) { auto props = SelectedSession();
controlled_remote_id_ = props->remote_id_; if (!props || !props->peer_ || !props->control_mouse_ ||
focused_remote_id_ = props->remote_id_; props->connection_status_.load() != ConnectionStatus::Connected) {
return;
} }
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.
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
return;
}
const int virtual_key = SlintKeyToWindowsVk(text); const int virtual_key = SlintKeyToWindowsVk(text);
if (virtual_key >= 0) { if (virtual_key >= 0) {
keyboard_.SendKeyCommand(virtual_key, pressed); keyboard_.SendKeyCommand(virtual_key, pressed);
+2
View File
@@ -35,6 +35,8 @@ private:
void SyncConnectionDialog(); void SyncConnectionDialog();
void SyncPlatformDialogs(); void SyncPlatformDialogs();
void SyncStreamWindow(); void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow(); void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__) #if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation(); void SyncXWaylandWindowActivation();
+2 -1
View File
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && if (keyboard_capturer_is_started_ &&
keyboard_capturer_uses_window_events_ &&
focus_on_stream_window_ && stream_window_ && focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) { SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event); ProcessKeyboardEvent(event);
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
} }
int SessionDeviceManager::StartKeyboardCapturer() { int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__ #ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) { if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
return 0; return 0;
} }
#endif #endif
#if defined(__linux__) && !defined(__APPLE__) #if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) { if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend"); LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0; return 0;
} }
#endif #endif
if (!keyboard_capturer_) { if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events"); "keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0; return 0;
} }
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}, },
&owner_); &owner_);
if (hook_ret != 0) { if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN( LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events"); "Start keyboard capturer failed, falling back to Slint keyboard events");
} else { } else {
LOG_INFO("Start keyboard capturer with native hook"); LOG_INFO("Start keyboard capturer with native hook");
} }
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
} }
int SessionDeviceManager::StopKeyboardCapturer() { int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) { if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0; return 0;
} }
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition. // fullscreen action with an immediate, single-window fullscreen transition.
bool ConfigureStreamWindowLiveResize(); bool ConfigureStreamWindowLiveResize();
// Returns whether the configured stream window is currently the active AppKit
// key window.
bool IsStreamWindowActive();
// Enters or leaves the stream window's animation-free fullscreen mode. // Enters or leaves the stream window's animation-free fullscreen mode.
bool SetStreamWindowFullscreen(bool fullscreen); bool SetStreamWindowFullscreen(bool fullscreen);
bool IsStreamWindowFullscreen(); bool IsStreamWindowFullscreen();
+7
View File
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
} }
} }
bool IsStreamWindowActive() {
@autoreleasepool {
return stream_window != nil && [NSApp isActive] &&
[stream_window isKeyWindow];
}
}
bool SetStreamWindowFullscreen(bool fullscreen) { bool SetStreamWindowFullscreen(bool fullscreen) {
@autoreleasepool { @autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) { if (stream_window == nil || stream_fullscreen == fullscreen) {
+1 -46
View File
@@ -17,11 +17,6 @@ namespace crossdesk {
namespace { namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5); constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
bool IsConnectionAttemptPending(ConnectionStatus status) {
return status == ConnectionStatus::Connecting ||
status == ConnectionStatus::Gathering;
}
} // namespace } // namespace
void GuiRuntime::HandleConnectionStatusChange() { void GuiRuntime::HandleConnectionStatusChange() {
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
} }
void GuiRuntime::HandleConnectionTimeouts() { void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false; bool presence_probe_timed_out = false;
@@ -121,44 +116,6 @@ void GuiRuntime::HandleConnectionTimeouts() {
show_offline_warning_window_ = true; show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id); LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
} }
bool rejoin_state_changed = false;
for (auto& [_, props] : remote_sessions_) {
if (!props || !props->connection_attempt_active_.load()) {
continue;
}
const ConnectionStatus status = props->connection_status_.load();
if (!IsConnectionAttemptPending(status)) {
props->connection_attempt_active_.store(false);
continue;
}
if (now - props->connection_attempt_started_at_ <
kConnectionAttemptTimeout) {
continue;
}
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
static_cast<int>(status));
props->connection_attempt_active_.store(false);
props->connection_established_ = false;
props->rejoin_ = false;
props->connection_status_.store(ConnectionStatus::Failed);
focused_remote_id_ = props->remote_id_;
show_connection_status_window_ = true;
rejoin_state_changed = true;
}
if (rejoin_state_changed) {
need_to_rejoin_ = false;
for (const auto& [_, props] : remote_sessions_) {
if (props && props->rejoin_) {
need_to_rejoin_ = true;
break;
}
}
}
} }
void GuiRuntime::HandleServerControllerDisconnected( void GuiRuntime::HandleServerControllerDisconnected(
@@ -455,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
auto props = remote_sessions_[remote_id]; auto props = remote_sessions_[remote_id];
if (!props->connection_established_) { if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting); props->connection_status_.store(ConnectionStatus::Connecting);
props->connection_attempt_active_.store(true);
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
show_connection_status_window_ = true; show_connection_status_window_ = true;
props->remember_password_ = remember_password; props->remember_password_ = remember_password;
+1 -1
View File
@@ -45,7 +45,7 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleRecentConnections(); void HandleRecentConnections();
void HandleConnectionStatusChange(); void HandleConnectionStatusChange();
void HandlePendingPresenceProbe(); void HandlePendingPresenceProbe();
void HandleConnectionTimeouts(); void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id, void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason); const char* reason);
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
-4
View File
@@ -170,10 +170,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->is_client_mode_ = true; runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true; runtime->show_connection_status_window_ = true;
props->connection_status_.store(status); props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
+3 -5
View File
@@ -58,7 +58,7 @@ struct FileTransferState {
// connection, media, input, window and transfer data that share one lifetime. // connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession { struct RemoteSession {
Params params_; Params params_;
PeerPtr *peer_ = nullptr; PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio"; std::string audio_label_ = "control_audio";
std::string data_label_ = "data"; std::string data_label_ = "data";
std::string mouse_label_ = "mouse"; std::string mouse_label_ = "mouse";
@@ -74,8 +74,6 @@ struct RemoteSession {
SignalStatus signal_status_ = SignalStatus::SignalClosed; SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false; bool connection_established_ = false;
bool rejoin_ = false; bool rejoin_ = false;
std::atomic<bool> connection_attempt_active_ = false;
std::chrono::steady_clock::time_point connection_attempt_started_at_;
bool net_traffic_stats_button_pressed_ = false; bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true; bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false; bool mouse_controller_is_started_ = false;
@@ -160,6 +158,6 @@ struct RemoteSession {
using RemoteSessionPtr = std::shared_ptr<RemoteSession>; using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail } // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_ #endif // CROSSDESK_GUI_REMOTE_SESSION_H_
+6
View File
@@ -163,6 +163,7 @@ export component StreamWindow inherits Window {
callback pointer-input(PointerEventButton, PointerEventKind, length, length); callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length); callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool); callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true; private property <bool> control-expanded: true;
private property <bool> display-menu-open: false; private property <bool> display-menu-open: false;
@@ -486,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope { input-focus := FocusScope {
focus-on-click: true; focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => { key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta); root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept accept
@@ -500,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width && self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y && self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height; && self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left && event.button == PointerEventButton.left
&& over-control { && over-control {
+13 -7
View File
@@ -104,16 +104,22 @@ int main() {
"return localization::device_offline[language];"); "return localization::device_offline[language];");
ok &= ExpectContains("runtime_state.h", runtime_state_h, ok &= ExpectContains("runtime_state.h", runtime_state_h,
"pending_presence_probe_started_at_"); "pending_presence_probe_started_at_");
ok &= ExpectContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleConnectionTimeouts"); "HandlePresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kPresenceProbeTimeout"); "kPresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectNotContains("remote_session.h", remote_session_h,
"kConnectionAttemptTimeout"); "connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_"); "connection_attempt_active_");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ConnectionStatus::Failed");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"props->connection_status_.store(status);");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"case ConnectionStatus::Failed:");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp, ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleServerControllerDisconnected"); "HandleServerControllerDisconnected");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp, ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
+20
View File
@@ -187,6 +187,26 @@ int main() {
stream->invoke_toggle_maximize_stream_window(); stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested); assert(maximize_requested);
bool keyboard_focus_changed = false;
bool keyboard_input_received = false;
stream->on_keyboard_focus_changed(
[&](bool focused) { keyboard_focus_changed = focused; });
stream->on_key_input(
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
keyboard_input_received = std::string(text) == "a" && pressed;
});
stream->window().set_size(
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
stream->window().dispatch_pointer_press_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_pointer_release_event(
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
slint::PointerEventButton::Left);
stream->window().dispatch_key_press_event("a");
assert(keyboard_focus_changed);
assert(keyboard_input_received);
crossdesk::ui::FileTransferEntry transfer; crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip"; transfer.name = "archive.zip";
transfer.status = "Sending"; transfer.status = "Sending";
+120
View File
@@ -0,0 +1,120 @@
#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 / "submodules/minirtc/src/transport/ice_transport_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();
}
std::string ExtractSection(const std::string& source, const std::string& begin,
const std::string& end) {
const size_t begin_pos = source.find(begin);
if (begin_pos == std::string::npos) {
return {};
}
const size_t end_pos = source.find(end, begin_pos + begin.size());
if (end_pos == std::string::npos) {
return {};
}
return source.substr(begin_pos, end_pos - begin_pos);
}
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 unsafe 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 controller = ReadFile(
repo_root /
"submodules/minirtc/src/transport/ice_transport_controller.cpp");
const std::string destroy = ExtractSection(
controller, "void IceTransportController::Destroy()",
"uint32_t IceTransportController::AddVideoSendChannel");
const std::string send_video = ExtractSection(
controller, "int IceTransportController::SendVideo(",
"void IceTransportController::MaybeDegradeResolutionOnEncodeTime");
const std::string on_video_encoded = ExtractSection(
controller, "int IceTransportController::OnVideoEncoded(",
"int IceTransportController::SendAudio(");
bool ok = true;
ok &= ExpectContains("Destroy", destroy,
"std::unique_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("Destroy", destroy,
"senders.swap(stream_senders_);");
ok &= ExpectContains("Destroy", destroy,
"codecs.push_back(std::move(context->codec));");
ok &= ExpectContains("Destroy", destroy, "senders.clear();");
ok &= ExpectContains("Destroy", destroy, "codecs.clear();");
ok &= ExpectNotContains("Destroy", destroy,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<IceTransportController> weak_self");
ok &= ExpectContains("SendVideo", send_video,
"std::weak_ptr<StreamContext> weak_context");
ok &= ExpectContains(
"SendVideo", send_video,
"[weak_self, weak_context, channel_name, queue_delay_ms");
ok &= ExpectContains("SendVideo", send_video,
"return self->OnVideoEncoded(");
ok &= ExpectNotContains("SendVideo", send_video,
"[this, channel_name, context");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"if (!is_running_.load())");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"std::shared_lock lock(stream_senders_mutex_);");
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
"it->second != context");
return ok ? 0 : 1;
}
+3 -1
View File
@@ -64,10 +64,12 @@ function setup_platform_settings()
"/usr/local/include/pipewire-0.3", "/usr/local/include/pipewire-0.3",
"/usr/include/pipewire", "/usr/include/pipewire",
"/usr/local/include/pipewire", "/usr/local/include/pipewire",
"/opt/crossdesk-pipewire-sdk/include/pipewire-0.3",
"/usr/include/spa-0.2", "/usr/include/spa-0.2",
"/usr/local/include/spa-0.2", "/usr/local/include/spa-0.2",
"/usr/include/spa", "/usr/include/spa",
"/usr/local/include/spa" "/usr/local/include/spa",
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
}, {system = true}) }, {system = true})
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
add_includedirs(include_dir, {system = true}) add_includedirs(include_dir, {system = true})
+5
View File
@@ -73,6 +73,11 @@ function setup_targets()
set_default(false) set_default(false)
add_files("tests/connection_status_protocol_test.cpp") add_files("tests/connection_status_protocol_test.cpp")
target("video_callback_lifetime_test")
set_kind("binary")
set_default(false)
add_files("tests/video_callback_lifetime_test.cpp")
target("windows_manifest_resource_test") target("windows_manifest_resource_test")
set_kind("binary") set_kind("binary")
set_default(false) set_default(false)