mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-19 17:46:27 +08:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7897a003d | ||
|
|
d2478006ba | ||
|
|
d32974dc18 | ||
|
|
efd8b7c0ff | ||
|
|
383452ed3b | ||
|
|
2570e57958 | ||
|
|
eed90dad34 | ||
|
|
c53ce2a581 | ||
|
|
708c39bb99 | ||
|
|
cee249a25b | ||
|
|
7c3b13409c | ||
|
|
e3d15f8f9a | ||
|
|
a6e8038286 | ||
|
|
765452e3c9 | ||
|
|
7cd0d6a351 | ||
|
|
c01b52eac5 | ||
|
|
737cf9b558 | ||
|
|
7ff6c8a4ad |
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.3:Ubuntu 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 页面手动运行。为了避免首次发布
|
||||||
|
|||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PIPEWIRE_VERSION="0.3.48"
|
||||||
|
PIPEWIRE_ARCHIVE_SHA256="0d218be86b3d2b548c06259c47ad8d110ee1c09f071e17c4393eeef5c880fa6f"
|
||||||
|
INSTALL_PREFIX="${1:-/opt/crossdesk-pipewire-sdk}"
|
||||||
|
IFS='.' read -r PIPEWIRE_VERSION_MAJOR PIPEWIRE_VERSION_MINOR \
|
||||||
|
PIPEWIRE_VERSION_MICRO <<< "${PIPEWIRE_VERSION}"
|
||||||
|
|
||||||
|
if [[ "${INSTALL_PREFIX}" != /* ]]; then
|
||||||
|
echo "Install prefix must be an absolute path: ${INSTALL_PREFIX}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-pipewire-sdk.XXXXXX")"
|
||||||
|
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||||
|
|
||||||
|
ARCHIVE_PATH="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||||
|
SOURCE_DIR="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}"
|
||||||
|
ARCHIVE_URL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${PIPEWIRE_VERSION}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||||
|
|
||||||
|
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
|
||||||
|
--retry 3 "${ARCHIVE_URL}" -o "${ARCHIVE_PATH}"
|
||||||
|
echo "${PIPEWIRE_ARCHIVE_SHA256} ${ARCHIVE_PATH}" | sha256sum --check -
|
||||||
|
tar -xzf "${ARCHIVE_PATH}" -C "${WORK_DIR}"
|
||||||
|
|
||||||
|
PIPEWIRE_INCLUDE_DIR="${INSTALL_PREFIX}/include/pipewire-0.3/pipewire"
|
||||||
|
SPA_INCLUDE_DIR="${INSTALL_PREFIX}/include/spa-0.2/spa"
|
||||||
|
|
||||||
|
while IFS= read -r -d '' header_path; do
|
||||||
|
relative_path="${header_path#${SOURCE_DIR}/src/pipewire/}"
|
||||||
|
install -D -m 0644 "${header_path}" \
|
||||||
|
"${PIPEWIRE_INCLUDE_DIR}/${relative_path}"
|
||||||
|
done < <(find "${SOURCE_DIR}/src/pipewire" -type f -name '*.h' -print0)
|
||||||
|
|
||||||
|
while IFS= read -r -d '' header_path; do
|
||||||
|
relative_path="${header_path#${SOURCE_DIR}/spa/include/spa/}"
|
||||||
|
install -D -m 0644 "${header_path}" \
|
||||||
|
"${SPA_INCLUDE_DIR}/${relative_path}"
|
||||||
|
done < <(find "${SOURCE_DIR}/spa/include/spa" -type f -name '*.h' -print0)
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s/@PIPEWIRE_VERSION_MAJOR@/${PIPEWIRE_VERSION_MAJOR}/g" \
|
||||||
|
-e "s/@PIPEWIRE_VERSION_MINOR@/${PIPEWIRE_VERSION_MINOR}/g" \
|
||||||
|
-e "s/@PIPEWIRE_VERSION_MICRO@/${PIPEWIRE_VERSION_MICRO}/g" \
|
||||||
|
-e 's/@PIPEWIRE_API_VERSION@/"0.3"/g' \
|
||||||
|
"${SOURCE_DIR}/src/pipewire/version.h.in" \
|
||||||
|
> "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||||
|
chmod 0644 "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||||
|
|
||||||
|
test -f "${PIPEWIRE_INCLUDE_DIR}/pipewire.h"
|
||||||
|
test -f "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||||
|
test -f "${SPA_INCLUDE_DIR}/param/video/format-utils.h"
|
||||||
|
|
||||||
|
echo "Installed PipeWire ${PIPEWIRE_VERSION} header SDK under ${INSTALL_PREFIX}"
|
||||||
@@ -2,19 +2,22 @@
|
|||||||
set -euo pipefail
|
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" \
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,6 +1,7 @@
|
|||||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -64,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;
|
||||||
@@ -119,8 +120,11 @@ struct StreamWindowState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ServerWindowState {
|
struct ServerWindowState {
|
||||||
bool need_to_create_server_window_ = false;
|
// Transport callbacks run outside the UI thread. Keep the two lifecycle
|
||||||
bool need_to_destroy_server_window_ = false;
|
// requests atomic so a disconnect cannot be lost while the Slint timer is
|
||||||
|
// deciding whether to create or destroy the controlled-side window.
|
||||||
|
std::atomic<bool> need_to_create_server_window_{false};
|
||||||
|
std::atomic<bool> need_to_destroy_server_window_{false};
|
||||||
bool server_window_created_ = false;
|
bool server_window_created_ = false;
|
||||||
bool server_window_inited_ = false;
|
bool server_window_inited_ = false;
|
||||||
int server_window_width_default_ = 250;
|
int server_window_width_default_ = 250;
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -56,7 +58,7 @@ bool HasNonEmptyEnvironmentVariable(const char* name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ScopedUnsetEnvironmentVariable {
|
class ScopedUnsetEnvironmentVariable {
|
||||||
public:
|
public:
|
||||||
explicit ScopedUnsetEnvironmentVariable(const char* name) : name_(name) {
|
explicit ScopedUnsetEnvironmentVariable(const char* name) : name_(name) {
|
||||||
if (const char* value = std::getenv(name_)) {
|
if (const char* value = std::getenv(name_)) {
|
||||||
original_value_ = value;
|
original_value_ = value;
|
||||||
@@ -77,7 +79,7 @@ public:
|
|||||||
ScopedUnsetEnvironmentVariable& operator=(
|
ScopedUnsetEnvironmentVariable& operator=(
|
||||||
const ScopedUnsetEnvironmentVariable&) = delete;
|
const ScopedUnsetEnvironmentVariable&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const char* name_;
|
const char* name_;
|
||||||
std::optional<std::string> original_value_;
|
std::optional<std::string> original_value_;
|
||||||
};
|
};
|
||||||
@@ -101,8 +103,7 @@ private:
|
|||||||
&bytes_after, &data) == Success &&
|
&bytes_after, &data) == Success &&
|
||||||
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
|
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
|
||||||
data) {
|
data) {
|
||||||
const ::Window active_window =
|
const ::Window active_window = *reinterpret_cast<const ::Window*>(data);
|
||||||
*reinterpret_cast<const ::Window*>(data);
|
|
||||||
XFree(data);
|
XFree(data);
|
||||||
return active_window;
|
return active_window;
|
||||||
}
|
}
|
||||||
@@ -165,9 +166,8 @@ bool X11WindowMatches(Display* display, ::Window x11_window,
|
|||||||
int root_x = 0;
|
int root_x = 0;
|
||||||
int root_y = 0;
|
int root_y = 0;
|
||||||
::Window child = 0;
|
::Window child = 0;
|
||||||
if (!XTranslateCoordinates(display, x11_window,
|
if (!XTranslateCoordinates(display, x11_window, DefaultRootWindow(display), 0,
|
||||||
DefaultRootWindow(display), 0, 0, &root_x,
|
0, &root_x, &root_y, &child)) {
|
||||||
&root_y, &child)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,17 +269,15 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
|
|||||||
size_t line_start = 0;
|
size_t line_start = 0;
|
||||||
while (line_start < markdown.size()) {
|
while (line_start < markdown.size()) {
|
||||||
const size_t newline = markdown.find('\n', line_start);
|
const size_t newline = markdown.find('\n', line_start);
|
||||||
const size_t line_end = newline == std::string_view::npos
|
const size_t line_end =
|
||||||
? markdown.size()
|
newline == std::string_view::npos ? markdown.size() : newline;
|
||||||
: newline;
|
|
||||||
std::string_view line = markdown.substr(line_start, line_end - line_start);
|
std::string_view line = markdown.substr(line_start, line_end - line_start);
|
||||||
if (!line.empty() && line.back() == '\r') {
|
if (!line.empty() && line.back() == '\r') {
|
||||||
line.remove_suffix(1);
|
line.remove_suffix(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::all_of(line.begin(), line.end(), [](unsigned char ch) {
|
if (std::all_of(line.begin(), line.end(),
|
||||||
return std::isspace(ch) != 0;
|
[](unsigned char ch) { return std::isspace(ch) != 0; })) {
|
||||||
})) {
|
|
||||||
section_gap = !blocks.empty();
|
section_gap = !blocks.empty();
|
||||||
if (newline == std::string_view::npos) {
|
if (newline == std::string_view::npos) {
|
||||||
break;
|
break;
|
||||||
@@ -306,11 +304,11 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
|
|||||||
|
|
||||||
const std::string block_markdown(line);
|
const std::string block_markdown(line);
|
||||||
ParsedReleaseNoteBlock block;
|
ParsedReleaseNoteBlock block;
|
||||||
if (const auto parsed =
|
if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) {
|
||||||
slint::StyledText::from_markdown(block_markdown)) {
|
|
||||||
block.content = *parsed;
|
block.content = *parsed;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN("Failed to parse a release-note Markdown block; using plain text");
|
LOG_WARN(
|
||||||
|
"Failed to parse a release-note Markdown block; using plain text");
|
||||||
block.content = slint::StyledText::from_plain_text(block_markdown);
|
block.content = slint::StyledText::from_plain_text(block_markdown);
|
||||||
}
|
}
|
||||||
block.section_gap = section_gap;
|
block.section_gap = section_gap;
|
||||||
@@ -958,13 +956,14 @@ bool GuiApplication::InitializeSDL() {
|
|||||||
const bool video_driver_allows_x11 =
|
const bool video_driver_allows_x11 =
|
||||||
requested_video_driver == nullptr || requested_video_driver[0] == '\0' ||
|
requested_video_driver == nullptr || requested_video_driver[0] == '\0' ||
|
||||||
std::strcmp(requested_video_driver, "x11") == 0;
|
std::strcmp(requested_video_driver, "x11") == 0;
|
||||||
use_xwayland_gui_ =
|
use_xwayland_gui_ = IsWaylandSession() &&
|
||||||
IsWaylandSession() && HasNonEmptyEnvironmentVariable("DISPLAY") &&
|
HasNonEmptyEnvironmentVariable("DISPLAY") &&
|
||||||
video_driver_allows_x11;
|
video_driver_allows_x11;
|
||||||
if (use_xwayland_gui_) {
|
if (use_xwayland_gui_) {
|
||||||
setenv("SDL_VIDEODRIVER", "x11", 1);
|
setenv("SDL_VIDEODRIVER", "x11", 1);
|
||||||
LOG_INFO("Wayland session detected; using XWayland for positioned GUI "
|
LOG_INFO(
|
||||||
"windows while retaining Wayland portal capture and input");
|
"Wayland session detected; using XWayland for positioned GUI "
|
||||||
|
"windows while retaining Wayland portal capture and input");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
||||||
@@ -1011,8 +1010,7 @@ void GuiApplication::InitializeUi() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
||||||
for (const auto& parsed :
|
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||||
ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
|
||||||
ui::ReleaseNoteBlock block;
|
ui::ReleaseNoteBlock block;
|
||||||
block.content = parsed.content;
|
block.content = parsed.content;
|
||||||
block.section_gap = parsed.section_gap;
|
block.section_gap = parsed.section_gap;
|
||||||
@@ -1159,10 +1157,10 @@ void GuiApplication::InitializeUi() {
|
|||||||
#endif
|
#endif
|
||||||
BindStreamCallbacks();
|
BindStreamCallbacks();
|
||||||
(*ui_->stream)->show();
|
(*ui_->stream)->show();
|
||||||
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
PositionWindowAtCenter(
|
||||||
kStreamWindowLogicalWidth,
|
(*ui_->stream)->window(), ui_->main->window(),
|
||||||
StreamWindowLogicalHeight(
|
kStreamWindowLogicalWidth,
|
||||||
(*ui_->stream)->get_custom_titlebar()));
|
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()));
|
||||||
ui_->main->hide();
|
ui_->main->hide();
|
||||||
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
||||||
ui_->server.emplace(ui::ServerWindow::create());
|
ui_->server.emplace(ui::ServerWindow::create());
|
||||||
@@ -1730,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() {
|
||||||
@@ -1833,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();
|
||||||
@@ -1843,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() {
|
||||||
@@ -2078,6 +2080,11 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
}
|
}
|
||||||
model.push_back(std::move(item));
|
model.push_back(std::move(item));
|
||||||
}
|
}
|
||||||
|
// LoadThumbnail already orders connections by last update time. Keep that
|
||||||
|
// order within each presence group while placing online devices first.
|
||||||
|
std::stable_partition(
|
||||||
|
model.begin(), model.end(),
|
||||||
|
[](const ui::RecentConnection& connection) { return connection.online; });
|
||||||
ui_->recent_model->set_vector(std::move(model));
|
ui_->recent_model->set_vector(std::move(model));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2227,29 +2234,59 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto process_id =
|
if (const auto process_id = X11GetWindowProcessId(display, active_window);
|
||||||
X11GetWindowProcessId(display, active_window);
|
|
||||||
process_id && *process_id != static_cast<unsigned long>(getpid())) {
|
process_id && *process_id != static_cast<unsigned long>(getpid())) {
|
||||||
ui_->main->set_window_active(false);
|
ui_->main->set_window_active(false);
|
||||||
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(
|
X11WindowMatches(display, active_window, (*ui_->stream)->window());
|
||||||
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;
|
||||||
{
|
{
|
||||||
@@ -2284,8 +2321,7 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
if (!ui_->stream) {
|
if (!ui_->stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(*ui_->stream)
|
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
|
||||||
->set_window_maximized((*ui_->stream)->window().is_maximized());
|
|
||||||
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
||||||
@@ -2297,16 +2333,17 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (ui_->stream_initial_position_attempts > 0) {
|
if (ui_->stream_initial_position_attempts > 0) {
|
||||||
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
if (PositionWindowAtCenter(
|
||||||
kStreamWindowLogicalWidth,
|
(*ui_->stream)->window(), ui_->main->window(),
|
||||||
StreamWindowLogicalHeight(
|
kStreamWindowLogicalWidth,
|
||||||
(*ui_->stream)->get_custom_titlebar()))) {
|
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()))) {
|
||||||
--ui_->stream_initial_position_attempts;
|
--ui_->stream_initial_position_attempts;
|
||||||
} else {
|
} else {
|
||||||
ui_->stream_initial_position_attempts = 0;
|
ui_->stream_initial_position_attempts = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!has_sessions) {
|
if (!has_sessions) {
|
||||||
|
SetStreamKeyboardFocus(false);
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
SetStreamWindowFullscreen(false);
|
SetStreamWindowFullscreen(false);
|
||||||
#endif
|
#endif
|
||||||
@@ -2534,7 +2571,22 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::SyncServerWindow() {
|
void GuiApplication::SyncServerWindow() {
|
||||||
if (need_to_create_server_window_ && !ui_->server) {
|
// The connection map is authoritative. Lifecycle flags record callback
|
||||||
|
// intent, but callbacks for different controllers can cross each other, so
|
||||||
|
// the final decision must reflect the current connected-controller set.
|
||||||
|
need_to_create_server_window_.exchange(false, std::memory_order_acq_rel);
|
||||||
|
need_to_destroy_server_window_.exchange(false, std::memory_order_acq_rel);
|
||||||
|
bool has_connected_controller = false;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
|
has_connected_controller =
|
||||||
|
std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||||
|
[](const auto& entry) {
|
||||||
|
return entry.second == ConnectionStatus::Connected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_connected_controller && !ui_->server) {
|
||||||
ui_->server.emplace(ui::ServerWindow::create());
|
ui_->server.emplace(ui::ServerWindow::create());
|
||||||
RegisterFontAwesome((*ui_->server)->window());
|
RegisterFontAwesome((*ui_->server)->window());
|
||||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||||
@@ -2554,15 +2606,13 @@ void GuiApplication::SyncServerWindow() {
|
|||||||
ui_->server_initial_position_attempts = 2;
|
ui_->server_initial_position_attempts = 2;
|
||||||
server_window_created_ = true;
|
server_window_created_ = true;
|
||||||
server_window_inited_ = true;
|
server_window_inited_ = true;
|
||||||
need_to_create_server_window_ = false;
|
|
||||||
}
|
}
|
||||||
if (need_to_destroy_server_window_ && ui_->server) {
|
if (!has_connected_controller && ui_->server) {
|
||||||
(*ui_->server)->hide();
|
(*ui_->server)->hide();
|
||||||
ui_->server.reset();
|
ui_->server.reset();
|
||||||
server_window_created_ = false;
|
server_window_created_ = false;
|
||||||
server_window_inited_ = false;
|
server_window_inited_ = false;
|
||||||
ui_->server_initial_position_attempts = 0;
|
ui_->server_initial_position_attempts = 0;
|
||||||
need_to_destroy_server_window_ = false;
|
|
||||||
}
|
}
|
||||||
if (!ui_->server) {
|
if (!ui_->server) {
|
||||||
return;
|
return;
|
||||||
@@ -2775,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) {
|
||||||
@@ -2863,9 +2924,8 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
|
|||||||
: 0.0f;
|
: 0.0f;
|
||||||
const float available_height =
|
const float available_height =
|
||||||
size.height / scale - titlebar_height -
|
size.height / scale - titlebar_height -
|
||||||
(fullscreen_button_pressed_
|
(fullscreen_button_pressed_ ? 0.0f
|
||||||
? 0.0f
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
||||||
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
|
||||||
float render_width = available_width;
|
float render_width = available_width;
|
||||||
float render_height = available_height;
|
float render_height = available_height;
|
||||||
float offset_x = 0;
|
float offset_x = 0;
|
||||||
@@ -2939,9 +2999,8 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
|
|||||||
: 0.0f;
|
: 0.0f;
|
||||||
const float height =
|
const float height =
|
||||||
size.height / scale - titlebar_height -
|
size.height / scale - titlebar_height -
|
||||||
(fullscreen_button_pressed_
|
(fullscreen_button_pressed_ ? 0.0f
|
||||||
? 0.0f
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
||||||
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2972,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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
|
// SDL represents key auto-repeat as additional key-down events. Forward
|
||||||
return 0;
|
// them so the fallback path has the same long-press behavior as native hooks.
|
||||||
}
|
|
||||||
|
|
||||||
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
||||||
if (key_code < 0) {
|
if (key_code < 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -412,4 +410,4 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,5 +1,3 @@
|
|||||||
#include "runtime/gui_runtime.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -11,18 +9,15 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
|
#include "platform.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
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);
|
} // namespace
|
||||||
bool IsConnectionAttemptPending(ConnectionStatus status) {
|
|
||||||
return status == ConnectionStatus::Connecting ||
|
|
||||||
status == ConnectionStatus::Gathering;
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void GuiRuntime::HandleConnectionStatusChange() {
|
void GuiRuntime::HandleConnectionStatusChange() {
|
||||||
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
||||||
@@ -31,7 +26,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
|
|||||||
j["type"] = "recent_connections_presence";
|
j["type"] = "recent_connections_presence";
|
||||||
j["user_id"] = client_id_;
|
j["user_id"] = client_id_;
|
||||||
j["devices"] = nlohmann::json::array();
|
j["devices"] = nlohmann::json::array();
|
||||||
for (const auto &id : recent_connection_ids_) {
|
for (const auto& id : recent_connection_ids_) {
|
||||||
std::string pure_id = id;
|
std::string pure_id = id;
|
||||||
size_t pos_y = pure_id.find('Y');
|
size_t pos_y = pure_id.find('Y');
|
||||||
size_t pos_n = pure_id.find('N');
|
size_t pos_n = pure_id.find('N');
|
||||||
@@ -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,48 +116,67 @@ 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;
|
void GuiRuntime::HandleServerControllerDisconnected(
|
||||||
for (auto &[_, props] : remote_sessions_) {
|
const std::string& remote_id, const char* reason) {
|
||||||
if (!props || !props->connection_attempt_active_.load()) {
|
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConnectionStatus status = props->connection_status_.load();
|
bool has_connected_controller = false;
|
||||||
if (!IsConnectionAttemptPending(status)) {
|
bool has_web_controller = false;
|
||||||
props->connection_attempt_active_.store(false);
|
std::string remaining_controller_id;
|
||||||
continue;
|
{
|
||||||
}
|
std::unique_lock lock(connection_status_mutex_);
|
||||||
|
connection_status_.erase(remote_id);
|
||||||
if (now - props->connection_attempt_started_at_ <
|
connection_host_names_.erase(remote_id);
|
||||||
kConnectionAttemptTimeout) {
|
for (const auto& [id, status] : connection_status_) {
|
||||||
continue;
|
if (status != ConnectionStatus::Connected) {
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
|
has_connected_controller = true;
|
||||||
static_cast<int>(status));
|
has_web_controller =
|
||||||
props->connection_attempt_active_.store(false);
|
has_web_controller || id.find("web") != std::string::npos;
|
||||||
props->connection_established_ = false;
|
if (remaining_controller_id.empty()) {
|
||||||
props->rejoin_ = false;
|
remaining_controller_id = id;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
show_cursor_ = has_web_controller;
|
||||||
|
if (has_connected_controller) {
|
||||||
|
remote_client_id_ = remaining_controller_id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
need_to_create_server_window_.store(false, std::memory_order_release);
|
||||||
|
need_to_destroy_server_window_.store(true, std::memory_order_release);
|
||||||
|
is_server_mode_ = false;
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
if (IsWaylandSession()) {
|
||||||
|
// Keep Wayland capture session warm to avoid black screen on subsequent
|
||||||
|
// reconnects.
|
||||||
|
start_screen_capturer_ = true;
|
||||||
|
LOG_INFO(
|
||||||
|
"Keeping Wayland screen capturer running after disconnect to "
|
||||||
|
"preserve reconnect stability");
|
||||||
|
} else {
|
||||||
|
start_screen_capturer_ = false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
start_screen_capturer_ = false;
|
||||||
|
#endif
|
||||||
|
start_speaker_capturer_ = false;
|
||||||
|
start_mouse_controller_ = false;
|
||||||
|
start_keyboard_capturer_ = false;
|
||||||
|
remote_client_id_.clear();
|
||||||
|
if (audio_capture_) {
|
||||||
|
devices_.StopSpeakerCapturer();
|
||||||
|
audio_capture_ = false;
|
||||||
|
}
|
||||||
|
devices_.ResetToInitialDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
|
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||||
const char *password,
|
const char* password,
|
||||||
bool remember_password) {
|
bool remember_password) {
|
||||||
if (!signal_connected_ || !peer_) {
|
if (!signal_connected_ || !peer_) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -221,7 +235,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
||||||
remote_host_name, password,
|
remote_host_name, password,
|
||||||
thumbnail = thumbnail_]() {
|
thumbnail = thumbnail_]() {
|
||||||
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width,
|
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
|
||||||
video_height, remote_id, remote_host_name,
|
video_height, remote_id, remote_host_name,
|
||||||
password);
|
password);
|
||||||
});
|
});
|
||||||
@@ -255,7 +269,7 @@ void GuiRuntime::CloseAllRemoteSessions() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// std::shared_lock lock(remote_sessions_mutex_);
|
// std::shared_lock lock(remote_sessions_mutex_);
|
||||||
for (auto &it : remote_sessions_) {
|
for (auto& it : remote_sessions_) {
|
||||||
auto props = it.second;
|
auto props = it.second;
|
||||||
CloseRemoteSession(props);
|
CloseRemoteSession(props);
|
||||||
}
|
}
|
||||||
@@ -279,7 +293,7 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto &thread : threads_to_join) {
|
for (auto& thread : threads_to_join) {
|
||||||
if (thread.joinable()) {
|
if (thread.joinable()) {
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
@@ -300,9 +314,8 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession>
|
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||||
GuiRuntime::FindRemoteSession(
|
const std::string& remote_id) {
|
||||||
const std::string &remote_id) {
|
|
||||||
if (remote_id.empty()) {
|
if (remote_id.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -315,8 +328,7 @@ GuiRuntime::FindRemoteSession(
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||||
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|
||||||
bool remember_password, bool bypass_presence_check) {
|
bool remember_password, bool bypass_presence_check) {
|
||||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||||
int ret =
|
int ret =
|
||||||
@@ -336,19 +348,17 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
focused_remote_id_ = remote_id;
|
focused_remote_id_ = remote_id;
|
||||||
|
|
||||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||||
bool exists =
|
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||||
(remote_sessions_.find(remote_id) != remote_sessions_.end());
|
|
||||||
// shared_lock.unlock();
|
// shared_lock.unlock();
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
PeerPtr *peer_to_init = nullptr;
|
PeerPtr* peer_to_init = nullptr;
|
||||||
std::string local_id;
|
std::string local_id;
|
||||||
|
|
||||||
{
|
{
|
||||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||||
remote_sessions_[remote_id] =
|
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
|
||||||
std::make_shared<RemoteSession>();
|
|
||||||
auto props = remote_sessions_[remote_id];
|
auto props = remote_sessions_[remote_id];
|
||||||
props->local_id_ = "C-" + std::string(client_id_);
|
props->local_id_ = "C-" + std::string(client_id_);
|
||||||
props->remote_id_ = remote_id;
|
props->remote_id_ = remote_id;
|
||||||
@@ -371,7 +381,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &display_info : devices_.display_info_list()) {
|
for (const auto& display_info : devices_.display_info_list()) {
|
||||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||||
}
|
}
|
||||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||||
@@ -402,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;
|
||||||
@@ -429,4 +437,4 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -8,16 +8,16 @@
|
|||||||
#include "features/devices/session_device_manager.h"
|
#include "features/devices/session_device_manager.h"
|
||||||
#include "features/file_transfer/file_transfer_manager.h"
|
#include "features/file_transfer/file_transfer_manager.h"
|
||||||
#include "features/input/keyboard_controller.h"
|
#include "features/input/keyboard_controller.h"
|
||||||
|
#include "features/settings/settings_manager.h"
|
||||||
#include "runtime/gui_state.h"
|
#include "runtime/gui_state.h"
|
||||||
#include "runtime/peer_event_handler.h"
|
#include "runtime/peer_event_handler.h"
|
||||||
#include "features/settings/settings_manager.h"
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||||
class GuiRuntime : protected gui_detail::GuiState {
|
class GuiRuntime : protected gui_detail::GuiState {
|
||||||
protected:
|
protected:
|
||||||
using FileTransferState = gui_detail::FileTransferState;
|
using FileTransferState = gui_detail::FileTransferState;
|
||||||
using RemoteSession = gui_detail::RemoteSession;
|
using RemoteSession = gui_detail::RemoteSession;
|
||||||
|
|
||||||
@@ -32,35 +32,35 @@ protected:
|
|||||||
GuiRuntime();
|
GuiRuntime();
|
||||||
~GuiRuntime();
|
~GuiRuntime();
|
||||||
|
|
||||||
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len);
|
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
|
||||||
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len);
|
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
|
||||||
|
|
||||||
int CreateConnectionPeer();
|
int CreateConnectionPeer();
|
||||||
int ConnectTo(const std::string &remote_id, const char *password,
|
int ConnectTo(const std::string& remote_id, const char* password,
|
||||||
bool remember_password, bool bypass_presence_check = false);
|
bool remember_password, bool bypass_presence_check = false);
|
||||||
int RequestSingleDevicePresence(const std::string &remote_id,
|
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||||
const char *password, bool remember_password);
|
const char* password, bool remember_password);
|
||||||
|
|
||||||
void UpdateLabels();
|
void UpdateLabels();
|
||||||
void HandleRecentConnections();
|
void HandleRecentConnections();
|
||||||
void HandleConnectionStatusChange();
|
void HandleConnectionStatusChange();
|
||||||
void HandlePendingPresenceProbe();
|
void HandlePendingPresenceProbe();
|
||||||
void HandleConnectionTimeouts();
|
void HandlePresenceProbeTimeout();
|
||||||
|
void HandleServerControllerDisconnected(const std::string& remote_id,
|
||||||
|
const char* reason);
|
||||||
void HandleWindowsServiceIntegration();
|
void HandleWindowsServiceIntegration();
|
||||||
|
|
||||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||||
void CloseAllRemoteSessions();
|
void CloseAllRemoteSessions();
|
||||||
void ResetRemoteSessionResources(
|
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
|
||||||
std::shared_ptr<RemoteSession> props);
|
|
||||||
void WaitForThumbnailSaveTasks();
|
void WaitForThumbnailSaveTasks();
|
||||||
std::shared_ptr<RemoteSession>
|
std::shared_ptr<RemoteSession> FindRemoteSession(
|
||||||
FindRemoteSession(const std::string &remote_id);
|
const std::string& remote_id);
|
||||||
|
|
||||||
void ResetRemoteServiceStatus(RemoteSession &props);
|
void ResetRemoteServiceStatus(RemoteSession& props);
|
||||||
void ApplyRemoteServiceStatus(RemoteSession &props,
|
void ApplyRemoteServiceStatus(RemoteSession& props,
|
||||||
const ServiceStatus &status);
|
const ServiceStatus& status);
|
||||||
RemoteUnlockState
|
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
|
||||||
GetRemoteUnlockState(const RemoteSession &props) const;
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||||
#endif
|
#endif
|
||||||
@@ -82,7 +82,7 @@ protected:
|
|||||||
KeyboardController keyboard_;
|
KeyboardController keyboard_;
|
||||||
PeerEventHandler peer_events_;
|
PeerEventHandler peer_events_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class ClipboardController;
|
friend class ClipboardController;
|
||||||
friend class SessionDeviceManager;
|
friend class SessionDeviceManager;
|
||||||
friend class FileTransferManager;
|
friend class FileTransferManager;
|
||||||
@@ -91,6 +91,6 @@ private:
|
|||||||
friend class PeerEventHandler;
|
friend class PeerEventHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {}
|
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
|
||||||
|
|
||||||
void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||||
void *user_data) {
|
void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime || !message || size == 0) {
|
if (!runtime || !message || size == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
|||||||
std::string type = j["type"].get<std::string>();
|
std::string type = j["type"].get<std::string>();
|
||||||
if (type == "presence") {
|
if (type == "presence") {
|
||||||
if (j.contains("devices") && j["devices"].is_array()) {
|
if (j.contains("devices") && j["devices"].is_array()) {
|
||||||
for (auto &dev : j["devices"]) {
|
for (auto& dev : j["devices"]) {
|
||||||
if (!dev.is_object()) {
|
if (!dev.is_object()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -88,11 +88,10 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
|
size_t user_id_size, void* user_data) {
|
||||||
size_t user_id_size, void *user_data) {
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,13 +149,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||||
const char *user_id,
|
const char* user_id,
|
||||||
const size_t user_id_size,
|
const size_t user_id_size,
|
||||||
void *user_data) {
|
void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime)
|
if (!runtime) return;
|
||||||
return;
|
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id(user_id, user_id_size);
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
@@ -172,113 +170,112 @@ 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: {
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
{
|
{
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
remote_action.i.display_num =
|
remote_action.i.display_num =
|
||||||
runtime->devices_.display_info_list().size();
|
runtime->devices_.display_info_list().size();
|
||||||
remote_action.i.display_list =
|
remote_action.i.display_list =
|
||||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||||
remote_action.i.left =
|
remote_action.i.left =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.top =
|
remote_action.i.top =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.right =
|
remote_action.i.right =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.bottom =
|
remote_action.i.bottom =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
runtime->devices_.display_info_list()[i].name);
|
runtime->devices_.display_info_list()[i].name);
|
||||||
remote_action.i.display_list[i] = (char *)malloc(
|
remote_action.i.display_list[i] = (char*)malloc(
|
||||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||||
strncpy(remote_action.i.display_list[i],
|
strncpy(remote_action.i.display_list[i],
|
||||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||||
runtime->devices_.display_info_list()[i].name.length());
|
runtime->devices_.display_info_list()[i].name.length());
|
||||||
remote_action.i.display_list
|
remote_action.i
|
||||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
.display_list[i][runtime->devices_.display_info_list()[i]
|
||||||
remote_action.i.left[i] =
|
.name.length()] = '\0';
|
||||||
runtime->devices_.display_info_list()[i].left;
|
remote_action.i.left[i] =
|
||||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
runtime->devices_.display_info_list()[i].left;
|
||||||
remote_action.i.right[i] =
|
remote_action.i.top[i] =
|
||||||
runtime->devices_.display_info_list()[i].right;
|
runtime->devices_.display_info_list()[i].top;
|
||||||
remote_action.i.bottom[i] =
|
remote_action.i.right[i] =
|
||||||
runtime->devices_.display_info_list()[i].bottom;
|
runtime->devices_.display_info_list()[i].right;
|
||||||
|
remote_action.i.bottom[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string host_name = GetHostName();
|
||||||
|
remote_action.type = ControlType::host_infomation;
|
||||||
|
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||||
|
host_name.size());
|
||||||
|
remote_action.i.host_name[host_name.size()] = '\0';
|
||||||
|
remote_action.i.host_name_size = host_name.size();
|
||||||
|
|
||||||
|
std::string msg = remote_action.to_json();
|
||||||
|
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||||
|
runtime->control_data_label_.c_str());
|
||||||
|
remote_action_codec::Free(remote_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string host_name = GetHostName();
|
if (!runtime->need_to_create_stream_window_ &&
|
||||||
remote_action.type = ControlType::host_infomation;
|
!runtime->remote_sessions_.empty()) {
|
||||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
runtime->need_to_create_stream_window_ = true;
|
||||||
remote_action.i.host_name[host_name.size()] = '\0';
|
}
|
||||||
remote_action.i.host_name_size = host_name.size();
|
props->connection_established_ = true;
|
||||||
|
runtime->start_keyboard_capturer_ = true;
|
||||||
std::string msg = remote_action.to_json();
|
break;
|
||||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
|
||||||
runtime->control_data_label_.c_str());
|
|
||||||
remote_action_codec::Free(remote_action);
|
|
||||||
}
|
}
|
||||||
|
case ConnectionStatus::Disconnected:
|
||||||
if (!runtime->need_to_create_stream_window_ &&
|
case ConnectionStatus::Failed:
|
||||||
!runtime->remote_sessions_.empty()) {
|
case ConnectionStatus::Closed: {
|
||||||
runtime->need_to_create_stream_window_ = true;
|
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||||
}
|
"connection_closed");
|
||||||
props->connection_established_ = true;
|
|
||||||
runtime->start_keyboard_capturer_ = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::Disconnected:
|
|
||||||
case ConnectionStatus::Failed:
|
|
||||||
case ConnectionStatus::Closed: {
|
|
||||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
|
||||||
"connection_closed");
|
|
||||||
props->connection_established_ = false;
|
|
||||||
props->enable_mouse_control_ = false;
|
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
|
||||||
props->front_frame_.reset();
|
|
||||||
props->back_frame_.reset();
|
|
||||||
props->video_width_ = 0;
|
|
||||||
props->video_height_ = 0;
|
|
||||||
props->video_size_ = 0;
|
|
||||||
props->render_rect_dirty_ = true;
|
|
||||||
props->stream_cleanup_pending_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime->focus_on_stream_window_ = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::IncorrectPassword: {
|
|
||||||
runtime->password_validating_ = false;
|
|
||||||
runtime->password_validating_time_++;
|
|
||||||
if (runtime->connect_button_pressed_) {
|
|
||||||
runtime->connect_button_pressed_ = false;
|
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
runtime->connect_button_label_ =
|
props->enable_mouse_control_ = false;
|
||||||
localization::connect[runtime->localization_language_index_];
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
|
props->front_frame_.reset();
|
||||||
|
props->back_frame_.reset();
|
||||||
|
props->video_width_ = 0;
|
||||||
|
props->video_height_ = 0;
|
||||||
|
props->video_size_ = 0;
|
||||||
|
props->render_rect_dirty_ = true;
|
||||||
|
props->stream_cleanup_pending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime->focus_on_stream_window_ = false;
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
case ConnectionStatus::IncorrectPassword: {
|
||||||
}
|
runtime->password_validating_ = false;
|
||||||
case ConnectionStatus::NoSuchTransmissionId:
|
runtime->password_validating_time_++;
|
||||||
case ConnectionStatus::RemoteUnavailable: {
|
if (runtime->connect_button_pressed_) {
|
||||||
if (runtime->connect_button_pressed_) {
|
runtime->connect_button_pressed_ = false;
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
runtime->connect_button_label_ =
|
runtime->connect_button_label_ =
|
||||||
localization::connect[runtime->localization_language_index_];
|
localization::connect[runtime->localization_language_index_];
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
case ConnectionStatus::NoSuchTransmissionId:
|
||||||
}
|
case ConnectionStatus::RemoteUnavailable: {
|
||||||
default:
|
if (runtime->connect_button_pressed_) {
|
||||||
break;
|
props->connection_established_ = false;
|
||||||
|
runtime->connect_button_label_ =
|
||||||
|
localization::connect[runtime->localization_language_index_];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runtime->is_client_mode_ = false;
|
runtime->is_client_mode_ = false;
|
||||||
@@ -289,155 +286,107 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
runtime->last_windows_service_status_tick_ = 0;
|
runtime->last_windows_service_status_tick_ = 0;
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
RemoteAction remote_action;
|
|
||||||
remote_action.i.display_num =
|
|
||||||
runtime->devices_.display_info_list().size();
|
|
||||||
remote_action.i.display_list =
|
|
||||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
|
||||||
remote_action.i.left =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.top =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.right =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.bottom =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
|
||||||
runtime->devices_.display_info_list()[i].name);
|
|
||||||
remote_action.i.display_list[i] = (char *)malloc(
|
|
||||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
|
||||||
strncpy(remote_action.i.display_list[i],
|
|
||||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
|
||||||
runtime->devices_.display_info_list()[i].name.length());
|
|
||||||
remote_action.i.display_list
|
|
||||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
|
||||||
remote_action.i.left[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].left;
|
|
||||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
|
||||||
remote_action.i.right[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].right;
|
|
||||||
remote_action.i.bottom[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string host_name = GetHostName();
|
|
||||||
remote_action.type = ControlType::host_infomation;
|
|
||||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
|
||||||
remote_action.i.host_name[host_name.size()] = '\0';
|
|
||||||
remote_action.i.host_name_size = host_name.size();
|
|
||||||
|
|
||||||
std::string msg = remote_action.to_json();
|
|
||||||
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
|
||||||
runtime->control_data_label_.c_str());
|
|
||||||
remote_action_codec::Free(remote_action);
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime->need_to_create_server_window_ = true;
|
|
||||||
runtime->is_server_mode_ = true;
|
|
||||||
runtime->start_screen_capturer_ = true;
|
|
||||||
runtime->start_speaker_capturer_ = true;
|
|
||||||
runtime->remote_client_id_ = remote_id;
|
|
||||||
runtime->start_mouse_controller_ = true;
|
|
||||||
{
|
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
|
||||||
if (std::all_of(runtime->connection_status_.begin(),
|
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
|
||||||
return kv.first.find("web") != std::string::npos;
|
|
||||||
})) {
|
|
||||||
runtime->show_cursor_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::Disconnected:
|
|
||||||
case ConnectionStatus::Failed:
|
|
||||||
case ConnectionStatus::Closed: {
|
|
||||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
|
||||||
"connection_closed");
|
|
||||||
bool all_disconnected = false;
|
|
||||||
{
|
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
|
||||||
all_disconnected =
|
|
||||||
std::all_of(runtime->connection_status_.begin(),
|
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
|
||||||
return kv.second == ConnectionStatus::Closed ||
|
|
||||||
kv.second == ConnectionStatus::Failed ||
|
|
||||||
kv.second == ConnectionStatus::Disconnected;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (all_disconnected) {
|
|
||||||
runtime->need_to_destroy_server_window_ = true;
|
|
||||||
runtime->is_server_mode_ = false;
|
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
|
||||||
if (IsWaylandSession()) {
|
|
||||||
// Keep Wayland capture session warm to avoid black screen on
|
|
||||||
// subsequent reconnects.
|
|
||||||
runtime->start_screen_capturer_ = true;
|
|
||||||
LOG_INFO("Keeping Wayland screen capturer running after "
|
|
||||||
"disconnect to preserve reconnect stability");
|
|
||||||
} else {
|
|
||||||
runtime->start_screen_capturer_ = false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
runtime->start_screen_capturer_ = false;
|
|
||||||
#endif
|
|
||||||
runtime->start_speaker_capturer_ = false;
|
|
||||||
runtime->start_mouse_controller_ = false;
|
|
||||||
runtime->start_keyboard_capturer_ = false;
|
|
||||||
runtime->remote_client_id_ = "";
|
|
||||||
if (props)
|
|
||||||
props->connection_established_ = false;
|
|
||||||
if (runtime->audio_capture_) {
|
|
||||||
runtime->devices_.StopSpeakerCapturer();
|
|
||||||
runtime->audio_capture_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
RemoteAction remote_action;
|
||||||
runtime->connection_status_.erase(remote_id);
|
remote_action.i.display_num =
|
||||||
runtime->connection_host_names_.erase(remote_id);
|
runtime->devices_.display_info_list().size();
|
||||||
}
|
remote_action.i.display_list =
|
||||||
runtime->devices_.ResetToInitialDisplay();
|
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||||
}
|
remote_action.i.left =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.top =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.right =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.bottom =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||||
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
|
runtime->devices_.display_info_list()[i].name);
|
||||||
|
remote_action.i.display_list[i] = (char*)malloc(
|
||||||
|
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||||
|
strncpy(remote_action.i.display_list[i],
|
||||||
|
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||||
|
runtime->devices_.display_info_list()[i].name.length());
|
||||||
|
remote_action.i
|
||||||
|
.display_list[i][runtime->devices_.display_info_list()[i]
|
||||||
|
.name.length()] = '\0';
|
||||||
|
remote_action.i.left[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].left;
|
||||||
|
remote_action.i.top[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].top;
|
||||||
|
remote_action.i.right[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].right;
|
||||||
|
remote_action.i.bottom[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].bottom;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
std::string host_name = GetHostName();
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
remote_action.type = ControlType::host_infomation;
|
||||||
if (std::all_of(runtime->connection_status_.begin(),
|
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
host_name.size());
|
||||||
return kv.first.find("web") == std::string::npos;
|
remote_action.i.host_name[host_name.size()] = '\0';
|
||||||
})) {
|
remote_action.i.host_name_size = host_name.size();
|
||||||
runtime->show_cursor_ = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
std::string msg = remote_action.to_json();
|
||||||
}
|
int ret =
|
||||||
default:
|
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
||||||
break;
|
runtime->control_data_label_.c_str());
|
||||||
|
remote_action_codec::Free(remote_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime->need_to_destroy_server_window_.store(
|
||||||
|
false, std::memory_order_release);
|
||||||
|
runtime->need_to_create_server_window_.store(true,
|
||||||
|
std::memory_order_release);
|
||||||
|
runtime->is_server_mode_ = true;
|
||||||
|
runtime->start_screen_capturer_ = true;
|
||||||
|
runtime->start_speaker_capturer_ = true;
|
||||||
|
runtime->remote_client_id_ = remote_id;
|
||||||
|
runtime->start_mouse_controller_ = true;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||||
|
if (std::all_of(runtime->connection_status_.begin(),
|
||||||
|
runtime->connection_status_.end(),
|
||||||
|
[](const auto& kv) {
|
||||||
|
return kv.first.find("web") != std::string::npos;
|
||||||
|
})) {
|
||||||
|
runtime->show_cursor_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ConnectionStatus::Disconnected:
|
||||||
|
case ConnectionStatus::Failed:
|
||||||
|
case ConnectionStatus::Closed: {
|
||||||
|
runtime->HandleServerControllerDisconnected(remote_id,
|
||||||
|
"connection_closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnNetStatusReport(
|
void PeerEventHandler::OnNetStatusReport(
|
||||||
const char *client_id, size_t client_id_size, TraversalMode mode,
|
const char* client_id, size_t client_id_size, TraversalMode mode,
|
||||||
const XNetTrafficStats *net_traffic_stats, const char *user_id,
|
const XNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||||
const size_t user_id_size, void *user_data) {
|
const size_t user_id_size, void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
||||||
std::string id, password;
|
std::string id, password;
|
||||||
const char *at_pos = strchr(client_id, '@');
|
const char* at_pos = strchr(client_id, '@');
|
||||||
if (at_pos == nullptr) {
|
if (at_pos == nullptr) {
|
||||||
id = client_id;
|
id = client_id;
|
||||||
password.clear();
|
password.clear();
|
||||||
@@ -510,4 +459,4 @@ void PeerEventHandler::OnNetStatusReport(
|
|||||||
props->net_traffic_stats_ = *net_traffic_stats;
|
props->net_traffic_stats_ = *net_traffic_stats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -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_
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
private property <bool> menu-open: false;
|
private property <bool> menu-open: false;
|
||||||
in-out property <bool> settings-open: false;
|
in-out property <bool> settings-open: false;
|
||||||
|
private property <length> settings-scroll-y: 0px;
|
||||||
in-out property <bool> self-host-settings-open: false;
|
in-out property <bool> self-host-settings-open: false;
|
||||||
in-out property <bool> about-open: false;
|
in-out property <bool> about-open: false;
|
||||||
in-out property <bool> update-open: root.update-available;
|
in-out property <bool> update-open: root.update-available;
|
||||||
@@ -318,6 +319,13 @@ export component MainWindow inherits Window {
|
|||||||
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
|
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
|
||||||
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
|
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
|
||||||
|
|
||||||
|
changed settings-open => {
|
||||||
|
if root.settings-open {
|
||||||
|
root.settings-scroll-y = 0px;
|
||||||
|
focus-sink.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reset-remote-id => {
|
reset-remote-id => {
|
||||||
root.remote-id-input = "";
|
root.remote-id-input = "";
|
||||||
remote-id-editor.focus();
|
remote-id-editor.focus();
|
||||||
@@ -1388,9 +1396,9 @@ export component MainWindow inherits Window {
|
|||||||
TouchArea { clicked => { focus-sink.focus(); } }
|
TouchArea { clicked => { focus-sink.focus(); } }
|
||||||
DialogSurface {
|
DialogSurface {
|
||||||
x: root.compact-language ? 219px : 190px;
|
x: root.compact-language ? 219px : 190px;
|
||||||
y: 9px;
|
y: 60px;
|
||||||
width: root.compact-language ? 202px : 269px;
|
width: root.compact-language ? 202px : 269px;
|
||||||
height: 432px;
|
height: 330px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -1405,55 +1413,86 @@ export component MainWindow inherits Window {
|
|||||||
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
|
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
|
||||||
}
|
}
|
||||||
|
|
||||||
Text { x: 8px; y: 31px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
settings-scroll := ScrollView {
|
||||||
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
x: 0px;
|
||||||
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
y: 21px;
|
||||||
|
width: parent.width;
|
||||||
|
height: 265px;
|
||||||
|
viewport-y <=> root.settings-scroll-y;
|
||||||
|
|
||||||
Text { x: 8px; y: 61px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
settings-content := VerticalLayout {
|
||||||
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 60px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
|
// Reserve space for the overlaid vertical scrollbar.
|
||||||
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
width: settings-scroll.width;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
spacing: 0px;
|
||||||
|
|
||||||
Text { x: 8px; y: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
Rectangle {
|
||||||
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 90px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
|
height: 30px;
|
||||||
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
height: 30px;
|
||||||
|
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
|
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text { x: 8px; y: 121px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
||||||
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 120px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
|
CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
|
||||||
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
|
||||||
|
|
||||||
Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 181px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 181px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 207px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 211px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 211px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 237px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
CompactButton { x: 8px; y: 241px; width: root.compact-language ? 70px : 220px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 241px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 267px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 271px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 271px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 297px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 301px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 301px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 327px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 331px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 331px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
|
|
||||||
Rectangle { x: 7px; y: 357px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
|
|
||||||
Text { x: 8px; y: 361px; width: root.compact-language ? 108px : 168px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
CompactButton { x: root.compact-language ? 120px : 180px; y: 361px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
|
|
||||||
|
|
||||||
CompactButton { x: root.compact-language ? 63px : 89px; y: 396px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
|
|
||||||
CompactButton { x: root.compact-language ? 104px : 130px; y: 396px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1492,7 +1531,6 @@ export component MainWindow inherits Window {
|
|||||||
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
|
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
|
||||||
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
||||||
}
|
}
|
||||||
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: root.compact-language ? 100px : 173px;
|
x: root.compact-language ? 100px : 173px;
|
||||||
@@ -1506,7 +1544,6 @@ export component MainWindow inherits Window {
|
|||||||
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
||||||
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
||||||
}
|
}
|
||||||
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: root.compact-language ? 100px : 173px;
|
x: root.compact-language ? 100px : 173px;
|
||||||
@@ -1520,7 +1557,6 @@ export component MainWindow inherits Window {
|
|||||||
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
||||||
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
||||||
}
|
}
|
||||||
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
|
||||||
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
|
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||||
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
|
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -508,7 +514,10 @@ export component StreamWindow inherits Window {
|
|||||||
&& (event.kind == PointerEventKind.up
|
&& (event.kind == PointerEventKind.up
|
||||||
|| event.kind == PointerEventKind.cancel) {
|
|| event.kind == PointerEventKind.cancel) {
|
||||||
root.end-control-drag();
|
root.end-control-drag();
|
||||||
} else if !root.control-dragging {
|
// The control bar overlays the remote video. Events in
|
||||||
|
// its full rectangle (including gaps between buttons)
|
||||||
|
// are local UI input and must never reach the peer.
|
||||||
|
} else if !root.control-dragging && !over-control {
|
||||||
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule submodules/minirtc updated: da4b0f2e9a...00aad6aa37
@@ -19,7 +19,7 @@ std::filesystem::path FindRepoRoot() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ReadFile(const std::filesystem::path &path) {
|
std::string ReadFile(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {};
|
return {};
|
||||||
@@ -30,8 +30,8 @@ std::string ReadFile(const std::filesystem::path &path) {
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContains(const char *name, const std::string &value,
|
bool ExpectContains(const char* name, const std::string& value,
|
||||||
const std::string &expected) {
|
const std::string& expected) {
|
||||||
if (value.find(expected) != std::string::npos) {
|
if (value.find(expected) != std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -40,8 +40,8 @@ bool ExpectContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectNotContains(const char *name, const std::string &value,
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
const std::string &unexpected) {
|
const std::string& unexpected) {
|
||||||
if (value.find(unexpected) == std::string::npos) {
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,10 @@ int main() {
|
|||||||
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
|
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
|
||||||
const std::string connection_runtime_cpp =
|
const std::string connection_runtime_cpp =
|
||||||
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
|
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
|
||||||
|
const std::string peer_event_handler_cpp =
|
||||||
|
ReadFile(repo_root / "src/gui/runtime/peer_event_handler.cpp");
|
||||||
|
const std::string application_state_h =
|
||||||
|
ReadFile(repo_root / "src/gui/application/application_state.h");
|
||||||
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
||||||
@@ -81,6 +85,11 @@ int main() {
|
|||||||
"\"Remote unavailable\"");
|
"\"Remote unavailable\"");
|
||||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||||
"ConnectionStatus::RemoteUnavailable");
|
"ConnectionStatus::RemoteUnavailable");
|
||||||
|
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||||
|
"IsCurrentPeerConnection");
|
||||||
|
ok &= ExpectContains(
|
||||||
|
"peer_connection.cpp", peer_connection,
|
||||||
|
"on_connection_status_(ConnectionStatus::Closed, remote_user_id.data()");
|
||||||
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||||
"\"Device offline\"");
|
"\"Device offline\"");
|
||||||
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||||
@@ -95,16 +104,30 @@ 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 &= 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,
|
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||||
"kConnectionAttemptTimeout");
|
"HandleServerControllerDisconnected");
|
||||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
|
||||||
"connection_attempt_started_at_");
|
"HandleServerControllerDisconnected(remote_id");
|
||||||
|
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||||
|
"ControllerHeartbeat");
|
||||||
|
ok &= ExpectContains("application_state.h", application_state_h,
|
||||||
|
"std::atomic<bool> need_to_destroy_server_window_");
|
||||||
|
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ std::filesystem::path FindRepoRoot() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ReadFile(const std::filesystem::path &path) {
|
std::string ReadFile(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {};
|
return {};
|
||||||
@@ -28,8 +28,8 @@ std::string ReadFile(const std::filesystem::path &path) {
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContains(const char *name, const std::string &value,
|
bool ExpectContains(const char* name, const std::string& value,
|
||||||
const std::string &expected) {
|
const std::string& expected) {
|
||||||
if (value.find(expected) != std::string::npos) {
|
if (value.find(expected) != std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -37,8 +37,8 @@ bool ExpectContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectNotContains(const char *name, const std::string &value,
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
const std::string &unexpected) {
|
const std::string& unexpected) {
|
||||||
if (value.find(unexpected) == std::string::npos) {
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -46,8 +46,8 @@ bool ExpectNotContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContainsAtLeast(const char *name, const std::string &value,
|
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||||
const std::string &expected, size_t min_count) {
|
const std::string& expected, size_t min_count) {
|
||||||
size_t count = 0;
|
size_t count = 0;
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||||
@@ -62,7 +62,7 @@ bool ExpectContainsAtLeast(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
const std::filesystem::path repo_root = FindRepoRoot();
|
const std::filesystem::path repo_root = FindRepoRoot();
|
||||||
@@ -84,14 +84,16 @@ int main() {
|
|||||||
"root.shortcut-menu-open = false;");
|
"root.shortcut-menu-open = false;");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"if root.display-menu-open: Rectangle");
|
"if root.display-menu-open: Rectangle");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &=
|
||||||
"display-touch.has-hover");
|
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"root.switch-display(index)");
|
"root.switch-display(index)");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"if root.shortcut-menu-open: Rectangle");
|
"if root.shortcut-menu-open: Rectangle");
|
||||||
|
ok &=
|
||||||
|
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"shortcut-touch.has-hover");
|
"} else if !root.control-dragging && !over-control {");
|
||||||
|
|
||||||
ok &= ExpectContains("common.slint", common,
|
ok &= ExpectContains("common.slint", common,
|
||||||
"if touch.has-hover && root.tooltip != \"\": Rectangle");
|
"if touch.has-hover && root.tooltip != \"\": Rectangle");
|
||||||
@@ -100,32 +102,38 @@ int main() {
|
|||||||
"tooltip: StreamStrings.select-display");
|
"tooltip: StreamStrings.select-display");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.send-shortcut");
|
"tooltip: StreamStrings.send-shortcut");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &=
|
||||||
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse");
|
ExpectContains("stream_window.slint", stream,
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
"root.mouse-control-enabled ? StreamStrings.release-mouse "
|
||||||
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
|
": StreamStrings.control-mouse");
|
||||||
|
ok &= ExpectContains(
|
||||||
|
"stream_window.slint", stream,
|
||||||
|
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.select-file");
|
"tooltip: StreamStrings.select-file");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats");
|
"root.stats-visible ? StreamStrings.hide-stats : "
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
"StreamStrings.show-stats");
|
||||||
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen");
|
ok &=
|
||||||
|
ExpectContains("stream_window.slint", stream,
|
||||||
|
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
|
||||||
|
": StreamStrings.fullscreen");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.disconnect");
|
"tooltip: StreamStrings.disconnect");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control");
|
"root.control-expanded ? StreamStrings.collapse-control "
|
||||||
|
": StreamStrings.expand-control");
|
||||||
|
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &=
|
||||||
"set_select_display(");
|
ExpectContains("gui_application.cpp", application, "set_select_display(");
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
|
||||||
"set_show_stats(");
|
ok &=
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ExpectContains("gui_application.cpp", application, "set_expand_control(");
|
||||||
"set_expand_control(");
|
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application,
|
||||||
"set_collapse_control(");
|
"set_collapse_control(");
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application,
|
||||||
"(*ui_->stream)->global<ui::StreamStrings>()");
|
"(*ui_->stream)->global<ui::StreamStrings>()");
|
||||||
ok &= ExpectNotContains("gui_application.cpp", application,
|
ok &=
|
||||||
"#include <imgui");
|
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,20 @@ int main() {
|
|||||||
window->set_about_open(true);
|
window->set_about_open(true);
|
||||||
assert(window->get_settings_open());
|
assert(window->get_settings_open());
|
||||||
assert(window->get_about_open());
|
assert(window->get_about_open());
|
||||||
|
if (const char *snapshot_path =
|
||||||
|
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
|
||||||
|
window->set_about_open(false);
|
||||||
|
window->set_connection_dialog_open(false);
|
||||||
|
window->show();
|
||||||
|
const bool snapshot_written =
|
||||||
|
WriteWindowSnapshot(window->window(), snapshot_path);
|
||||||
|
window->hide();
|
||||||
|
if (!snapshot_written) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
window->set_connection_dialog_open(true);
|
||||||
|
window->set_about_open(true);
|
||||||
|
}
|
||||||
window->set_remote_id_input("987654321");
|
window->set_remote_id_input("987654321");
|
||||||
window->invoke_reset_remote_id();
|
window->invoke_reset_remote_id();
|
||||||
assert(std::string(window->get_remote_id_input()).empty());
|
assert(std::string(window->get_remote_id_input()).empty());
|
||||||
@@ -173,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";
|
||||||
|
|||||||
@@ -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
@@ -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})
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user