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
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 }}
+50 -12
View File
@@ -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:
@@ -103,12 +103,33 @@ jobs:
- name: Verify Linux 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
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
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
- name: Package
@@ -116,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
@@ -136,19 +163,20 @@ jobs:
runs-on: ${{ matrix.runner }}
env:
MACOSX_DEPLOYMENT_TARGET: "14.0"
EXPECTED_MACOS_SDK: "26.5"
strategy:
matrix:
include:
- arch: x64
binary_arch: x86_64
runner: macos-15-intel
cache-key: intel-macos14
runner: macos-26-intel
cache-key: intel-macos26-xcode26.6-min14
out-dir: ./build/macosx/x86_64/release/crossdesk
package_script: ./scripts/macosx/pkg_x64.sh
- arch: arm64
binary_arch: arm64
runner: macos-14
cache-key: arm-macos14
runner: macos-26
cache-key: arm-macos26-xcode26.6-min14
out-dir: ./build/macosx/arm64/release/crossdesk
package_script: ./scripts/macosx/pkg_arm64.sh
@@ -193,6 +221,17 @@ jobs:
echo "BUILD_DATE=${BUILD_DATE}" >> $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
uses: actions/cache@v5
with:
@@ -237,7 +276,7 @@ jobs:
exit 1
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
uses: actions/upload-artifact@v6
@@ -424,8 +463,7 @@ jobs:
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux, build-macos, build-windows-x64]
needs: [build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
+27 -8
View File
@@ -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,27 +80,39 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
依赖:
- [xmake](https://xmake.io/#/guide/installation)
- [cmake](https://cmake.org/download/)
- [cmake](https://cmake.org/download/) 3.21 及以上
### 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 \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-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 捕获时,再安装对应依赖:
```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,7 +124,11 @@ 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 -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
xmake b -vy crossdesk
```
@@ -121,7 +137,8 @@ amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`arm64 构建
启用 Wayland 和 DRM 的配置示例:
```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
```
@@ -136,6 +153,8 @@ xmake b -vy crossdesk
```
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
### 通用编译选项
@@ -158,7 +177,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 安装包
进入容器,下载工程后执行:
```
+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) |
| **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,27 +85,41 @@ 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
### 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 \
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-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:
```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,7 +131,12 @@ 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 -m release --USE_CUDA=false --cc=gcc-10 --cxx=g++-10 -y
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:
```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
```
@@ -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.
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 +186,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:
```
+25 -4
View File
@@ -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 \
@@ -93,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}" \
@@ -134,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 --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
+15 -8
View File
@@ -1,7 +1,9 @@
# CrossDesk Linux 构建镜像
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10
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 镜像:
```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.3Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
工具链版本 tag 只会在两个架构都验证成功后更新。
@@ -26,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 页面手动运行。为了避免首次发布
+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
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,9 +31,48 @@ 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
pkg-config --exists xft
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 \
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 \
"s/slint" \
+1 -1
View File
@@ -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"
+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 -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,
+20 -2
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
if [[ $# -lt 2 || $# -gt 3 ]]; then
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
exit 2
fi
APP_BUNDLE="$1"
EXPECTED_ARCH="$2"
EXPECTED_SDK_VERSION="${3:-}"
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
@@ -94,6 +95,23 @@ if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION"
exit 1
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"
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
@@ -1,5 +1,6 @@
#include "keyboard_capturer.h"
#include <cstdint>
#include <unordered_map>
#include "keyboard_converter.h"
@@ -10,6 +11,7 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') {
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
(void)proxy;
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
kCrossDeskInjectedKeyboardEvent) {
return event;
}
if (!g_on_key_action) {
return event;
}
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
}
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
kCrossDeskInjectedKeyboardEvent);
CGEventPost(kCGHIDEventTap, 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) {
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) {
+1 -1
View File
@@ -65,7 +65,7 @@ struct InteractionState {
bool start_keyboard_capturer_ = false;
bool show_cursor_ = 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 focus_on_stream_window_ = false;
bool audio_capture_ = false;
+73 -12
View File
@@ -30,6 +30,8 @@
#include "localization.h"
#include "platform.h"
#if _WIN32
#include <windows.h>
#include "platform/tray/win_tray.h"
#elif defined(__APPLE__)
#include "platform/tray/mac_tray.h"
@@ -1726,6 +1728,8 @@ void GuiApplication::BindStreamCallbacks() {
bool control, bool alt, bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
});
stream->on_keyboard_focus_changed(
[this](bool focused) { SetStreamKeyboardFocus(focused); });
}
void GuiApplication::BindServerCallbacks() {
@@ -1829,8 +1833,13 @@ void GuiApplication::Tick() {
}
HandleConnectionStatusChange();
HandlePendingPresenceProbe();
HandleConnectionTimeouts();
HandlePresenceProbeTimeout();
HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions();
UpdateLocalization();
@@ -1839,9 +1848,6 @@ void GuiApplication::Tick() {
SyncPlatformDialogs();
SyncStreamWindow();
SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#endif
}
void GuiApplication::UpdateLocalization() {
@@ -2228,6 +2234,7 @@ void GuiApplication::SyncXWaylandWindowActivation() {
Display* display = ui_->x11_focus_display;
const ::Window active_window = X11GetActiveWindow(display);
if (!active_window) {
SetStreamKeyboardFocus(false);
return;
}
@@ -2237,19 +2244,49 @@ void GuiApplication::SyncXWaylandWindowActivation() {
if (ui_->stream) {
(*ui_->stream)->set_window_active(false);
}
SetStreamKeyboardFocus(false);
return;
}
ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
(*ui_->stream)
->set_window_active(
X11WindowMatches(display, active_window, (*ui_->stream)->window()));
const bool stream_active =
X11WindowMatches(display, active_window, (*ui_->stream)->window());
(*ui_->stream)->set_window_active(stream_active);
SetStreamKeyboardFocus(stream_active);
} else {
SetStreamKeyboardFocus(false);
}
}
#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() {
bool has_sessions = false;
{
@@ -2306,6 +2343,7 @@ void GuiApplication::SyncStreamWindow() {
}
}
if (!has_sessions) {
SetStreamKeyboardFocus(false);
#if defined(__APPLE__)
SetStreamWindowFullscreen(false);
#endif
@@ -2787,14 +2825,25 @@ void GuiApplication::SelectStreamTab(int index) {
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
return;
}
focused_remote_id_ = ui_->tab_ids[index];
controlled_remote_id_ = focused_remote_id_;
const std::string selected_remote_id = ui_->tab_ids[index];
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_);
for (auto& [id, props] : remote_sessions_) {
if (props) {
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) {
@@ -2982,10 +3031,22 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
(void)alt;
(void)shift;
(void)meta;
if (auto props = SelectedSession()) {
controlled_remote_id_ = props->remote_id_;
focused_remote_id_ = props->remote_id_;
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ ||
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);
if (virtual_key >= 0) {
keyboard_.SendKeyCommand(virtual_key, pressed);
+2
View File
@@ -35,6 +35,8 @@ private:
void SyncConnectionDialog();
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncStreamKeyboardFocus();
void SetStreamKeyboardFocus(bool focused);
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
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_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_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
}
int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false;
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
return 0;
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
return 0;
}
#endif
if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events");
"keyboard capturer is nullptr, falling back to Slint keyboard events");
return 0;
}
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
},
&owner_);
if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events");
"Start keyboard capturer failed, falling back to Slint keyboard events");
} else {
LOG_INFO("Start keyboard capturer with native hook");
}
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}
int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
return 0;
}
+4
View File
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
// fullscreen action with an immediate, single-window fullscreen transition.
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.
bool SetStreamWindowFullscreen(bool fullscreen);
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) {
@autoreleasepool {
if (stream_window == nil || stream_fullscreen == fullscreen) {
+1 -46
View File
@@ -17,11 +17,6 @@ namespace crossdesk {
namespace {
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
void GuiRuntime::HandleConnectionStatusChange() {
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
show_offline_warning_window_ = true;
}
void GuiRuntime::HandleConnectionTimeouts() {
void GuiRuntime::HandlePresenceProbeTimeout() {
const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false;
@@ -121,44 +116,6 @@ void GuiRuntime::HandleConnectionTimeouts() {
show_offline_warning_window_ = true;
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(
@@ -455,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
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;
props->remember_password_ = remember_password;
+1 -1
View File
@@ -45,7 +45,7 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleConnectionTimeouts();
void HandlePresenceProbeTimeout();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
-4
View File
@@ -170,10 +170,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true;
props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) {
case ConnectionStatus::Connected: {
+3 -5
View File
@@ -58,7 +58,7 @@ struct FileTransferState {
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
Params params_;
PeerPtr *peer_ = nullptr;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
@@ -74,8 +74,6 @@ struct RemoteSession {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = 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 enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
@@ -160,6 +158,6 @@ struct 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 scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
callback keyboard-focus-changed(bool);
private property <bool> control-expanded: true;
private property <bool> display-menu-open: false;
@@ -486,6 +487,8 @@ export component StreamWindow inherits Window {
input-focus := FocusScope {
focus-on-click: true;
focus-gained => { root.keyboard-focus-changed(true); }
focus-lost => { root.keyboard-focus-changed(false); }
key-pressed(event) => {
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
accept
@@ -500,6 +503,9 @@ export component StreamWindow inherits Window {
&& self.mouse-x <= control.x + control.width
&& self.mouse-y >= control.y
&& self.mouse-y <= control.y + control.height;
if event.kind == PointerEventKind.down && !over-control {
input-focus.focus();
}
if event.kind == PointerEventKind.down
&& event.button == PointerEventButton.left
&& over-control {
+13 -7
View File
@@ -104,16 +104,22 @@ int main() {
"return localization::device_offline[language];");
ok &= ExpectContains("runtime_state.h", runtime_state_h,
"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,
"HandleConnectionTimeouts");
"HandlePresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kPresenceProbeTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"kConnectionAttemptTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"connection_attempt_started_at_");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"connection_attempt_started_at_");
ok &= ExpectNotContains("remote_session.h", remote_session_h,
"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,
"HandleServerControllerDisconnected");
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();
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;
transfer.name = "archive.zip";
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/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})
+5
View File
@@ -73,6 +73,11 @@ function setup_targets()
set_default(false)
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")
set_kind("binary")
set_default(false)