Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8752e51921 | ||
|
|
a8cb0e21ce | ||
|
|
90184f5251 | ||
|
|
fb0ab4f14e | ||
|
|
0e3f3cec00 | ||
|
|
72af49eb80 | ||
|
|
36f05834b5 | ||
|
|
1a190fbaf5 | ||
|
|
1fe236da76 | ||
|
|
3b989fb70d | ||
|
|
7595ab8bd8 | ||
|
|
126f455681 | ||
|
|
d6d0a548b2 | ||
|
|
74ac849efe | ||
|
|
e2cd18dba4 | ||
|
|
d2b1329eca | ||
|
|
0653a95250 | ||
|
|
bd73ef55f1 | ||
|
|
b854512c24 | ||
|
|
4528caf0f4 | ||
|
|
f6e89725aa | ||
|
|
62f79a8e44 | ||
|
|
096bc68d35 | ||
|
|
8ea4668305 | ||
|
|
21eec76008 | ||
|
|
3e63675787 | ||
|
|
439f137764 | ||
|
|
f537147f54 | ||
|
|
2186ebd7eb | ||
|
|
c11d48fc1f | ||
|
|
1690fc92a3 | ||
|
|
0d9d385d10 | ||
|
|
20f8137df2 | ||
|
|
29aae747c6 | ||
|
|
3f93dc05aa | ||
|
|
05885ecd48 | ||
|
|
ee92c4ce5f | ||
|
|
d94443330c | ||
|
|
df72204619 | ||
|
|
3678271577 | ||
|
|
6b5535ab3d | ||
|
|
078bd1e520 | ||
|
|
718705c352 | ||
|
|
1ca5852280 | ||
|
|
1eda6cc183 | ||
|
|
8d37f8f5fb | ||
|
|
05b8aa629e | ||
|
|
2cbceba953 | ||
|
|
89d3c8b53f | ||
|
|
51a4190e97 | ||
|
|
3737cd0dd5 | ||
|
|
00957d6740 | ||
|
|
6ec86322b2 | ||
|
|
ecc131b5ee | ||
|
|
1aa50e9ffa | ||
|
|
d49f5bc520 | ||
|
|
8818b9b8be | ||
|
|
3be283238c | ||
|
|
5d0453fff3 | ||
|
|
8a9775a571 | ||
|
|
7f4f1882ae | ||
|
|
23e24e3d47 | ||
|
|
7e1b9cb00e | ||
|
|
0c70acbdbf | ||
|
|
fcbbb0f51c | ||
|
|
f4be0700bf | ||
|
|
a45670b04d | ||
|
|
ff329cae81 | ||
|
|
092664784e | ||
|
|
1709eb0efb | ||
|
|
d27bb5d7a4 | ||
|
|
62c8f0a38d | ||
|
|
21eee5ccdc | ||
|
|
e6c26511cd | ||
|
|
c0b880e1eb | ||
|
|
c10f1499f2 | ||
|
|
d7a9c26ff9 | ||
|
|
f809ad173c | ||
|
|
34155d0266 | ||
|
|
9d544788b0 | ||
|
|
dd16e65b68 | ||
|
|
91eac61a90 | ||
|
|
941cc79af5 | ||
|
|
0d6ecfc730 |
@@ -8,13 +8,15 @@
|
|||||||
# Dependency manifests and local xmake package recipes are the only project
|
# Dependency manifests and local xmake package recipes are the only project
|
||||||
# files needed while pre-warming the build image.
|
# files needed while pre-warming the build image.
|
||||||
!xmake.lua
|
!xmake.lua
|
||||||
!xmake/
|
!apps/
|
||||||
!xmake/**
|
!apps/desktop/
|
||||||
!thirdparty/
|
!apps/desktop/xmake/
|
||||||
!thirdparty/**
|
!apps/desktop/xmake/**
|
||||||
!submodules/
|
!deps/thirdparty/
|
||||||
!submodules/xmake.lua
|
!deps/thirdparty/**
|
||||||
!submodules/minirtc/
|
!deps/submodules/
|
||||||
!submodules/minirtc/xmake.lua
|
!deps/submodules/xmake.lua
|
||||||
!submodules/minirtc/thirdparty/
|
!deps/submodules/minirtc/
|
||||||
!submodules/minirtc/thirdparty/**
|
!deps/submodules/minirtc/xmake.lua
|
||||||
|
!deps/submodules/minirtc/thirdparty/
|
||||||
|
!deps/submodules/minirtc/thirdparty/**
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ on:
|
|||||||
- docker/linux-build/**
|
- docker/linux-build/**
|
||||||
- xmake.lua
|
- xmake.lua
|
||||||
- xmake/**
|
- xmake/**
|
||||||
- thirdparty/**
|
- deps/thirdparty/**
|
||||||
- submodules/xmake.lua
|
- deps/submodules/xmake.lua
|
||||||
- submodules/minirtc
|
- deps/submodules/minirtc
|
||||||
- submodules/minirtc/xmake.lua
|
- deps/submodules/minirtc/xmake.lua
|
||||||
- submodules/minirtc/thirdparty/**
|
- deps/submodules/minirtc/thirdparty/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -13,8 +13,14 @@ on:
|
|||||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||||
required: false
|
required: false
|
||||||
default: "0"
|
default: "0"
|
||||||
|
sign_windows_test:
|
||||||
|
description: "Submit the Windows installer to SignPath using test-signing."
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: read
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -29,13 +35,13 @@ 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: ./apps/desktop/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: ./apps/desktop/scripts/linux/pkg_arm64.sh
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ${{ matrix.image }}
|
image: ${{ matrix.image }}
|
||||||
@@ -106,40 +112,61 @@ jobs:
|
|||||||
- name: Ensure Wayland build dependencies
|
- name: Ensure Wayland build dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2; then
|
if pkg-config --exists dbus-1 libdrm && \
|
||||||
|
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h && \
|
||||||
|
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libdbus-1-dev \
|
libdbus-1-dev \
|
||||||
libdrm-dev \
|
libdrm-dev
|
||||||
libpipewire-0.3-dev \
|
|
||||||
libspa-0.2-dev
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2
|
install-crossdesk-pipewire-sdk
|
||||||
|
pkg-config --exists dbus-1 libdrm
|
||||||
|
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h
|
||||||
|
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h
|
||||||
|
|
||||||
- name: Build CrossDesk
|
- 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 --USE_WAYLAND=true --root -y
|
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true \
|
||||||
|
--cc=gcc-10 --cxx=g++-10 --root -y
|
||||||
xmake b -vy --root crossdesk
|
xmake b -vy --root crossdesk
|
||||||
|
|
||||||
|
- name: Verify repository and wire boundaries
|
||||||
|
env:
|
||||||
|
XMAKE_GLOBALDIR: /data
|
||||||
|
run: |
|
||||||
|
xmake b -vy --root repository_structure_test
|
||||||
|
xmake b -vy --root crossdesk_wire_test
|
||||||
|
xmake b -vy --root keyboard_state_wire_test
|
||||||
|
xmake run --root repository_structure_test
|
||||||
|
xmake run --root crossdesk_wire_test
|
||||||
|
xmake run --root keyboard_state_wire_test
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
run: |
|
||||||
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
|
||||||
@@ -162,13 +189,13 @@ jobs:
|
|||||||
runner: macos-26-intel
|
runner: macos-26-intel
|
||||||
cache-key: intel-macos26-xcode26.6-min14
|
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: ./apps/desktop/scripts/macos/pkg_x64.sh
|
||||||
- arch: arm64
|
- arch: arm64
|
||||||
binary_arch: arm64
|
binary_arch: arm64
|
||||||
runner: macos-26
|
runner: macos-26
|
||||||
cache-key: arm-macos26-xcode26.6-min14
|
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: ./apps/desktop/scripts/macos/pkg_arm64.sh
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version number
|
- name: Extract version number
|
||||||
@@ -244,7 +271,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build CrossDesk
|
- name: Build CrossDesk
|
||||||
run: |
|
run: |
|
||||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
|
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -y
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
|
|
||||||
- name: Package CrossDesk app
|
- name: Package CrossDesk app
|
||||||
@@ -266,7 +293,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
|
./apps/desktop/scripts/macos/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
|
||||||
@@ -279,6 +306,173 @@ jobs:
|
|||||||
mkdir -p release
|
mkdir -p release
|
||||||
cp crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg release/
|
cp crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg release/
|
||||||
|
|
||||||
|
# iOS
|
||||||
|
build-ios:
|
||||||
|
name: Build on iOS (arm64)
|
||||||
|
runs-on: macos-26
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
IOS_DEPLOYMENT_TARGET: "16.0"
|
||||||
|
EXPECTED_IOS_SDK: "26.5"
|
||||||
|
XMAKE_VERSION: "3.1.1"
|
||||||
|
XMAKE_REPO_REVISION: "121109352da272cd1a3d4ed52f2dcda34f839955"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Extract version number
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
VERSION_REF="${GITHUB_REF##*/}"
|
||||||
|
VERSION_BASE="${VERSION_REF#v}"
|
||||||
|
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||||
|
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||||
|
|
||||||
|
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||||
|
PATCH_NUMBER=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||||
|
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||||
|
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||||
|
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||||
|
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||||
|
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||||
|
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||||
|
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||||
|
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||||
|
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||||
|
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||||
|
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||||
|
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||||
|
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||||
|
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||||
|
else
|
||||||
|
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}"
|
||||||
|
echo "VERSION_NUM=${VERSION_NUM}"
|
||||||
|
|
||||||
|
- 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 iphoneos --show-sdk-version)"
|
||||||
|
if [[ "${SDK_VERSION}" != "${EXPECTED_IOS_SDK}" ]]; then
|
||||||
|
echo "Unexpected iOS SDK: expected ${EXPECTED_IOS_SDK}, got ${SDK_VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Initialize submodules
|
||||||
|
run: git submodule update --init --recursive
|
||||||
|
|
||||||
|
- name: Cache xmake dependencies
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: apps/ios/.xmake/packages
|
||||||
|
key: "${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-${{ github.run_id }}"
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-xmake${{ env.XMAKE_VERSION }}-deps-ios-arm64-xcode26.6-xrepo1211093-min16-
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install xmake
|
||||||
|
uses: xmake-io/github-action-setup-xmake@v1
|
||||||
|
with:
|
||||||
|
xmake-version: "${{ env.XMAKE_VERSION }}"
|
||||||
|
|
||||||
|
- name: Verify xmake version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
XMAKE_VERSION_OUTPUT="$(xmake --version)"
|
||||||
|
echo "${XMAKE_VERSION_OUTPUT}"
|
||||||
|
if [[ "${XMAKE_VERSION_OUTPUT}" != *"xmake v${XMAKE_VERSION}"* ]]; then
|
||||||
|
echo "Unexpected xmake version: expected ${XMAKE_VERSION}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Pin xmake package repository
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
xmake repo -u
|
||||||
|
XMAKE_REPO_DIR="${HOME}/.xmake/repositories/xmake-repo"
|
||||||
|
git -C "${XMAKE_REPO_DIR}" fetch --depth=1 origin "${XMAKE_REPO_REVISION}"
|
||||||
|
git -C "${XMAKE_REPO_DIR}" checkout --detach "${XMAKE_REPO_REVISION}"
|
||||||
|
test "$(git -C "${XMAKE_REPO_DIR}" rev-parse HEAD)" = "${XMAKE_REPO_REVISION}"
|
||||||
|
|
||||||
|
- name: Build CrossDesk Mobile
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
DERIVED_DATA="${RUNNER_TEMP}/CrossDeskMobileDerivedData"
|
||||||
|
echo "DERIVED_DATA=${DERIVED_DATA}" >> "${GITHUB_ENV}"
|
||||||
|
xcodebuild \
|
||||||
|
-project apps/ios/CrossDeskMobile.xcodeproj \
|
||||||
|
-scheme CrossDeskMobile \
|
||||||
|
-configuration Release \
|
||||||
|
-destination 'generic/platform=iOS' \
|
||||||
|
-derivedDataPath "${DERIVED_DATA}" \
|
||||||
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
|
CODE_SIGNING_REQUIRED=NO \
|
||||||
|
COMPILER_INDEX_STORE_ENABLE=NO \
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET="${IOS_DEPLOYMENT_TARGET}" \
|
||||||
|
build
|
||||||
|
|
||||||
|
- name: Verify CrossDesk Mobile
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app"
|
||||||
|
EXECUTABLE_PATH="${APP_PATH}/CrossDeskMobile"
|
||||||
|
INFO_PLIST="${APP_PATH}/Info.plist"
|
||||||
|
|
||||||
|
if [[ ! -x "${EXECUTABLE_PATH}" ]]; then
|
||||||
|
echo "CrossDeskMobile executable is missing from ${APP_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
xcrun lipo "${EXECUTABLE_PATH}" -verify_arch arm64
|
||||||
|
MINIMUM_OS=$(/usr/libexec/PlistBuddy -c 'Print :MinimumOSVersion' "${INFO_PLIST}")
|
||||||
|
if [[ "${MINIMUM_OS}" != "${IOS_DEPLOYMENT_TARGET}" ]]; then
|
||||||
|
echo "Unexpected minimum iOS version: expected ${IOS_DEPLOYMENT_TARGET}, got ${MINIMUM_OS}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
xcrun lipo -info "${EXECUTABLE_PATH}"
|
||||||
|
echo "Minimum iOS version: ${MINIMUM_OS}"
|
||||||
|
|
||||||
|
- name: Package CrossDesk Mobile
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
APP_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app"
|
||||||
|
PACKAGE_DIR="${RUNNER_TEMP}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}"
|
||||||
|
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-ios-arm64-unsigned-${VERSION_NUM}.zip"
|
||||||
|
|
||||||
|
mkdir -p "${PACKAGE_DIR}"
|
||||||
|
cp -R "${APP_PATH}" "${PACKAGE_DIR}/"
|
||||||
|
|
||||||
|
DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app.dSYM"
|
||||||
|
if [[ -d "${DSYM_PATH}" ]]; then
|
||||||
|
cp -R "${DSYM_PATH}" "${PACKAGE_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent "${PACKAGE_DIR}" "${PACKAGE_FILE}"
|
||||||
|
echo "PACKAGE_FILE=${PACKAGE_FILE}" >> "${GITHUB_ENV}"
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: crossdesk-ios-arm64-unsigned-${{ env.VERSION_NUM }}
|
||||||
|
path: ${{ env.PACKAGE_FILE }}
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
build-windows-x64:
|
build-windows-x64:
|
||||||
name: Build on Windows x64
|
name: Build on Windows x64
|
||||||
@@ -388,7 +582,7 @@ jobs:
|
|||||||
- name: Copy nsProcess plugin to NSIS folder
|
- name: Copy nsProcess plugin to NSIS folder
|
||||||
run: |
|
run: |
|
||||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||||
copy "${{ github.workspace }}\scripts\windows\nsProcess.dll" $nsisPluginDir
|
copy "${{ github.workspace }}\apps\desktop\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||||
|
|
||||||
- name: Build CrossDesk
|
- name: Build CrossDesk
|
||||||
run: |
|
run: |
|
||||||
@@ -398,7 +592,53 @@ jobs:
|
|||||||
- name: Package
|
- name: Package
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
& "${{ github.workspace }}\apps\desktop\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||||
|
|
||||||
|
- name: Upload unsigned Windows installer for test signing
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||||
|
id: upload-unsigned-windows-installer
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||||
|
archive: false
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
- name: Sign Windows installer with SignPath test certificate
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||||
|
uses: signpath/github-action-submit-signing-request@v2
|
||||||
|
with:
|
||||||
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||||
|
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||||
|
project-slug: crossdesk
|
||||||
|
signing-policy-slug: test-signing
|
||||||
|
github-artifact-id: ${{ steps.upload-unsigned-windows-installer.outputs.artifact-id }}
|
||||||
|
wait-for-completion: true
|
||||||
|
output-artifact-directory: ${{ github.workspace }}\signpath\signed-installer
|
||||||
|
skip-decompress: true
|
||||||
|
parameters: |
|
||||||
|
version: ${{ toJSON(env.VERSION_NUM) }}
|
||||||
|
|
||||||
|
- name: Verify and use test-signed Windows installer
|
||||||
|
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$installerName = "crossdesk-win-x64-${{ env.VERSION_NUM }}.exe"
|
||||||
|
$signedInstaller = Join-Path "${{ github.workspace }}\signpath\signed-installer" $installerName
|
||||||
|
$destination = Join-Path "${{ github.workspace }}\apps\desktop\scripts\windows" $installerName
|
||||||
|
|
||||||
|
if (!(Test-Path $signedInstaller)) {
|
||||||
|
throw "SignPath did not return the signed installer: $signedInstaller"
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = Get-AuthenticodeSignature $signedInstaller
|
||||||
|
if (!$signature.SignerCertificate -or $signature.Status -eq "NotSigned") {
|
||||||
|
throw "SignPath returned an installer without an Authenticode signature."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Signer: $($signature.SignerCertificate.Subject)"
|
||||||
|
Write-Host "Signature status: $($signature.Status)"
|
||||||
|
Copy-Item $signedInstaller $destination -Force
|
||||||
|
|
||||||
- name: Build Portable CrossDesk
|
- name: Build Portable CrossDesk
|
||||||
run: |
|
run: |
|
||||||
@@ -442,7 +682,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
|
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
|
||||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||||
|
|
||||||
- name: Upload portable artifact
|
- name: Upload portable artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
@@ -453,7 +693,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: [build-linux, build-macos, build-windows-x64]
|
needs: [build-linux, build-macos, build-ios, build-windows-x64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -516,6 +756,7 @@ jobs:
|
|||||||
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
cp artifacts/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-macos-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg
|
||||||
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||||
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
|
||||||
|
cp artifacts/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-ios-arm64-unsigned-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||||
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
|
||||||
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||||
|
|
||||||
@@ -534,6 +775,10 @@ jobs:
|
|||||||
body: |
|
body: |
|
||||||
Binary release only. Source code is not included.
|
Binary release only. Source code is not included.
|
||||||
|
|
||||||
|
## Code signing policy
|
||||||
|
|
||||||
|
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||||
|
|
||||||
- name: Create or update 'latest' tag
|
- name: Create or update 'latest' tag
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
@@ -550,6 +795,12 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
files: release/*
|
files: release/*
|
||||||
generate_release_notes: false
|
generate_release_notes: false
|
||||||
|
body: |
|
||||||
|
Binary release only. Source code is not included.
|
||||||
|
|
||||||
|
## Code signing policy
|
||||||
|
|
||||||
|
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||||
|
|
||||||
- name: Upload artifacts to server
|
- name: Upload artifacts to server
|
||||||
uses: burnett01/rsync-deployments@5.2
|
uses: burnett01/rsync-deployments@5.2
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
# Xmake cache
|
# Xmake cache
|
||||||
.xmake/
|
.xmake/
|
||||||
build/
|
build/
|
||||||
|
apps/ios/Vendor/
|
||||||
|
apps/ios/DerivedData*/
|
||||||
|
apps/ios/.xmake/
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserstate
|
||||||
|
|
||||||
# MacOS Cache
|
# MacOS Cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
[submodule "submodules/minirtc"]
|
[submodule "deps/submodules/minirtc"]
|
||||||
path = submodules/minirtc
|
path = deps/submodules/minirtc
|
||||||
url = https://github.com/kunkundi/minirtc.git
|
url = https://github.com/kunkundi/minirtc.git
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# CrossDesk Privacy Policy
|
||||||
|
|
||||||
|
Last updated: August 8, 2026
|
||||||
|
|
||||||
|
[中文](#中文) | [English](#english)
|
||||||
|
|
||||||
|
## 中文
|
||||||
|
|
||||||
|
### 适用范围
|
||||||
|
|
||||||
|
本隐私政策适用于 CrossDesk 桌面客户端及项目提供的默认网络服务。用户自行部署或指定的服务器由相应服务器运营者负责,其数据处理方式不在本政策控制范围内。
|
||||||
|
|
||||||
|
### 网络通信
|
||||||
|
|
||||||
|
CrossDesk 是远程桌面软件,需要通过网络完成设备发现、连接协商及远程会话。客户端可能进行以下网络通信:
|
||||||
|
|
||||||
|
- **默认服务:** 未启用自托管时,客户端会连接预配置的 CrossDesk 服务(`api.crossdesk.cn`),用于设备注册、信令、网络地址转换(NAT)穿透,并在无法建立直接连接时提供中继。
|
||||||
|
- **版本检查:** 客户端启动时会从 CrossDesk 版本服务(`version.crossdesk.cn`)获取最新版本信息。当前即使启用自托管,版本检查仍会访问 CrossDesk 版本服务。
|
||||||
|
- **远程会话:** 用户主动发起或接受远程连接后,CrossDesk 会根据使用的功能在设备之间传输屏幕画面、音频、键盘和鼠标输入、剪贴板文本以及用户选择传输的文件。剪贴板文本可能在远程会话期间自动同步。这些数据可能通过直接连接传输,也可能在必要时通过配置的中继服务器转发。
|
||||||
|
- **自托管:** 用户可以在设置中指定自己的信令及中继服务器。自托管服务器的日志、数据保存和访问规则由其运营者决定。
|
||||||
|
|
||||||
|
### 处理的信息
|
||||||
|
|
||||||
|
为提供上述功能,CrossDesk 可能处理:
|
||||||
|
|
||||||
|
- 连接所需的设备标识及认证信息;
|
||||||
|
- IP 地址、端口、连接时间、连接状态等必要的网络元数据;
|
||||||
|
- 版本检查请求产生的标准网络请求信息;
|
||||||
|
- 用户授权的远程会话中处理的屏幕、音频、控制指令、剪贴板及文件内容。
|
||||||
|
|
||||||
|
### 信息的使用与保存
|
||||||
|
|
||||||
|
- 设备标识、连接设置及用户偏好会保存在本地设备上,以便后续使用。
|
||||||
|
- 通过官方信令和中继服务处理的信息仅用于建立、维持和转发 CrossDesk 连接,以及服务运行、安全防护和故障排查。
|
||||||
|
- 中继传输的远程会话内容仅用于实时转发,不用于广告或用户画像。
|
||||||
|
- CrossDesk 桌面客户端不包含广告功能,也不会主动上传诊断日志、崩溃报告或用户行为分析数据。
|
||||||
|
- 自托管服务器可能保存的日志和数据由其运营者控制,请向相应运营者了解其隐私与保存政策。
|
||||||
|
|
||||||
|
### 用户选择
|
||||||
|
|
||||||
|
用户可以:
|
||||||
|
|
||||||
|
- 配置自托管服务器,以替代默认信令和中继服务;
|
||||||
|
- 决定是否发起或接受远程会话;
|
||||||
|
- 决定是否选择并传输文件;
|
||||||
|
- 结束远程会话,以停止会话数据传输;
|
||||||
|
- 删除 CrossDesk 的本地配置、缓存及日志文件。
|
||||||
|
|
||||||
|
### 政策更新与联系方式
|
||||||
|
|
||||||
|
本政策可能随着 CrossDesk 功能或网络服务的变化而更新。更新内容将在本文件中发布。如有隐私相关问题,请联系 [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
This Privacy Policy applies to the CrossDesk desktop client and the default network services provided by the project. Servers deployed or selected by users are operated by their respective administrators, and their data-processing practices are outside the control of this policy.
|
||||||
|
|
||||||
|
### Network communications
|
||||||
|
|
||||||
|
CrossDesk is remote desktop software and requires network communications for device discovery, connection negotiation, and remote sessions. The client may perform the following communications:
|
||||||
|
|
||||||
|
- **Default services:** Unless self-hosting is enabled, the client connects to preconfigured CrossDesk services (`api.crossdesk.cn`) for device registration, signaling, network address translation (NAT) traversal, and relay when a direct connection cannot be established.
|
||||||
|
- **Update checks:** On startup, the client retrieves current release information from the CrossDesk version service (`version.crossdesk.cn`). Update checks currently continue to use the CrossDesk version service even when self-hosting is enabled.
|
||||||
|
- **Remote sessions:** After a user initiates or accepts a remote connection, CrossDesk transmits the data required by enabled features between devices. This may include screen content, audio, keyboard and mouse input, clipboard text, and files selected by the user. Clipboard text may be synchronized automatically during a remote session. Data may travel over a direct connection or through the configured relay server when necessary.
|
||||||
|
- **Self-hosting:** Users may configure their own signaling and relay servers. Logging, retention, and access policies for a self-hosted server are determined by its operator.
|
||||||
|
|
||||||
|
### Information processed
|
||||||
|
|
||||||
|
To provide these functions, CrossDesk may process:
|
||||||
|
|
||||||
|
- device identifiers and authentication information required for connections;
|
||||||
|
- necessary network metadata, such as IP addresses, ports, connection times, and connection status;
|
||||||
|
- standard network request information generated by update checks; and
|
||||||
|
- screen, audio, control, clipboard, and file content processed during a remote session authorized by the user.
|
||||||
|
|
||||||
|
### Use and storage of information
|
||||||
|
|
||||||
|
- Device identifiers, connection settings, and user preferences are stored locally for subsequent use.
|
||||||
|
- Information processed by the official signaling and relay services is used only to establish, maintain, and relay CrossDesk connections and for service operation, security, and troubleshooting.
|
||||||
|
- Remote-session content passing through a relay is used only for real-time forwarding and is not used for advertising or user profiling.
|
||||||
|
- The CrossDesk desktop client does not contain advertising functionality and does not intentionally upload diagnostic logs, crash reports, or behavioral analytics.
|
||||||
|
- Logs and data that may be retained by self-hosted servers are controlled by their operators. Contact the relevant operator for its privacy and retention practices.
|
||||||
|
|
||||||
|
### User choices
|
||||||
|
|
||||||
|
Users may:
|
||||||
|
|
||||||
|
- configure a self-hosted server instead of the default signaling and relay services;
|
||||||
|
- choose whether to initiate or accept a remote session;
|
||||||
|
- choose whether to select and transfer files;
|
||||||
|
- end a remote session to stop session-data transmission; and
|
||||||
|
- remove local CrossDesk configuration, cache, and log files.
|
||||||
|
|
||||||
|
### Changes and contact
|
||||||
|
|
||||||
|
This policy may be updated as CrossDesk features or network services change. Updates will be published in this file. For privacy-related questions, contact [junkun.di@hotmail.com](mailto:junkun.di@hotmail.com).
|
||||||
@@ -1,38 +1,51 @@
|
|||||||
# CrossDesk
|
# CrossDesk
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||||
[]()
|
|
||||||
|
[](https://www.crossdesk.cn/)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||||
[](https://github.com/kunkundi/crossdesk/actions)
|
[](https://github.com/kunkundi/crossdesk/actions)
|
||||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/issues)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/forks)
|
||||||
|
|
||||||
[ [English](README_EN.md) / 中文 ]
|
[ [English](README_EN.md) / 中文 ]
|
||||||
|
|
||||||
PC 客户端
|
---
|
||||||
|
|
||||||
|
## 界面预览
|
||||||
|
|
||||||
|
### PC 客户端
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Web 客户端
|
### Web 客户端
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 简介
|
## 简介
|
||||||
|
|
||||||
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
|
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
|
||||||
|
|
||||||
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
|
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 系统要求
|
## 系统要求
|
||||||
|
|
||||||
| 平台 | 最低版本 |
|
| 平台 | 最低版本 |
|
||||||
|----------------|---------------------------|
|
| --- | --- |
|
||||||
| **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 及以上 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
@@ -47,14 +60,17 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
|||||||
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
|
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
|
||||||

|

|
||||||
|
|
||||||
### Web 客户端
|
### 使用 Web 客户端
|
||||||
|
|
||||||
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
|
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
|
||||||
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
|
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
|
||||||
|
|
||||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||||
|
|
||||||
### Windows 服务(CrossDesk Service)
|
### Windows 服务(CrossDesk Service)
|
||||||
|
|
||||||
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||||
|
|
||||||
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||||
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||||
@@ -62,13 +78,15 @@ CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服
|
|||||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||||
|
|
||||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
# 安装
|
# 安装
|
||||||
.\CrossDesk.exe --service-install
|
.\CrossDesk.exe --service-install
|
||||||
# 启动
|
# 启动
|
||||||
.\CrossDesk.exe --service-start
|
.\CrossDesk.exe --service-start
|
||||||
# 查看状态
|
# 查看状态
|
||||||
.\CrossDesk.exe --service-status
|
.\CrossDesk.exe --service-status
|
||||||
|
# 停止
|
||||||
.\CrossDesk.exe --service-stop
|
.\CrossDesk.exe --service-stop
|
||||||
# 卸载
|
# 卸载
|
||||||
.\CrossDesk.exe --service-uninstall
|
.\CrossDesk.exe --service-uninstall
|
||||||
@@ -76,31 +94,46 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
|
|||||||
|
|
||||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 如何编译
|
## 如何编译
|
||||||
|
|
||||||
依赖:
|
### 构建依赖
|
||||||
|
|
||||||
- [xmake](https://xmake.io/#/guide/installation)
|
- [xmake](https://xmake.io/#/guide/installation)
|
||||||
- [cmake](https://cmake.org/download/)
|
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
|
||||||
|
|
||||||
### 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 \
|
||||||
|
libgl1-mesa-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
|
||||||
|
CMake 3.21 或更高版本,无需手动替换系统 CMake。
|
||||||
|
|
||||||
|
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
|
||||||
|
因此上述命令同时安装并在后续配置中指定 GCC 10。
|
||||||
|
|
||||||
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
|
需要启用 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,16 +145,25 @@ 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 -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
|
||||||
|
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
|
||||||
|
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
|
||||||
|
|
||||||
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
||||||
|
|
||||||
启用 Wayland 和 DRM 的配置示例:
|
启用 Wayland 和 DRM 的配置示例:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||||
|
--toolchain=gcc-10 -y
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,13 +171,15 @@ xmake b -vy crossdesk
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# amd64
|
# amd64
|
||||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||||
|
|
||||||
# arm64
|
# arm64
|
||||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||||
|
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
|
||||||
|
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
|
||||||
|
|
||||||
### 通用编译选项
|
### 通用编译选项
|
||||||
|
|
||||||
@@ -158,10 +202,11 @@ 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 安装包。
|
||||||
|
|
||||||
进入容器,下载工程后执行:
|
进入容器,下载工程后执行:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
export CUDA_PATH=/usr/local/cuda
|
export CUDA_PATH=/usr/local/cuda
|
||||||
export XMAKE_GLOBALDIR=/data
|
export XMAKE_GLOBALDIR=/data
|
||||||
|
|
||||||
@@ -170,50 +215,65 @@ xmake b --root -vy crossdesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
|
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake require -vy "cuda 12.6.3"
|
xmake require -vy "cuda 12.6.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
安装完成后执行:
|
安装完成后执行:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake require --info "cuda 12.6.3"
|
xmake require --info "cuda 12.6.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
输出如下:
|
输出如下:
|
||||||
|
|
||||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||||
|
|
||||||
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
|
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
set CUDA_PATH=path_to_cuda_installdir
|
set CUDA_PATH=path_to_cuda_installdir
|
||||||
```
|
```
|
||||||
|
|
||||||
重新执行:
|
重新执行:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake f --USE_CUDA=true
|
xmake f --USE_CUDA=true
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 注意
|
#### 注意
|
||||||
|
|
||||||
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
|
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
|
||||||
|
|
||||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||||
|
|
||||||
## 关于 Xmake
|
### 关于 Xmake
|
||||||
|
|
||||||
#### 安装 Xmake
|
#### 安装 Xmake
|
||||||
|
|
||||||
使用 curl:
|
使用 curl:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
curl -fsSL https://xmake.io/shget.text | bash
|
curl -fsSL https://xmake.io/shget.text | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 wget:
|
使用 wget:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
wget https://xmake.io/shget.text -O - | bash
|
wget https://xmake.io/shget.text -O - | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
使用 powershell:
|
使用 powershell:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
irm https://xmake.io/psget.text | iex
|
irm https://xmake.io/psget.text | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 编译选项
|
#### 编译选项
|
||||||
```
|
|
||||||
|
```text
|
||||||
# 切换编译模式
|
# 切换编译模式
|
||||||
xmake f -m debug/release
|
xmake f -m debug/release
|
||||||
|
|
||||||
@@ -227,14 +287,22 @@ xmake b -vy crossdesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### 运行选项
|
#### 运行选项
|
||||||
```
|
|
||||||
|
```bash
|
||||||
# 使用调试模式运行
|
# 使用调试模式运行
|
||||||
xmake r -d crossdesk
|
xmake r -d crossdesk
|
||||||
```
|
```
|
||||||
更多使用方法可参考 [Xmake官方文档](https://xmake.io/guide/quick-start.html) 。
|
|
||||||
|
更多使用方法可参考 [Xmake 官方文档](https://xmake.io/guide/quick-start.html)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 自托管服务器
|
## 自托管服务器
|
||||||
推荐使用Docker部署CrossDesk Server。
|
|
||||||
|
### 服务端
|
||||||
|
|
||||||
|
推荐使用 Docker 部署 CrossDesk Server。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
--name crossdesk_server \
|
--name crossdesk_server \
|
||||||
@@ -252,7 +320,8 @@ sudo docker run -d \
|
|||||||
|
|
||||||
上述命令中,用户需注意的参数如下:
|
上述命令中,用户需注意的参数如下:
|
||||||
|
|
||||||
**参数**
|
#### 配置参数
|
||||||
|
|
||||||
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
|
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
|
||||||
- INTERNAL_IP:服务器内网 IP
|
- INTERNAL_IP:服务器内网 IP
|
||||||
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
|
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
|
||||||
@@ -261,7 +330,8 @@ sudo docker run -d \
|
|||||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
|
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
|
||||||
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
|
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
|
||||||
|
|
||||||
**示例**:
|
#### 部署示例
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
--name crossdesk_server \
|
--name crossdesk_server \
|
||||||
@@ -277,26 +347,31 @@ sudo docker run -d \
|
|||||||
crossdesk/crossdesk-server:v1.1.6
|
crossdesk/crossdesk-server:v1.1.6
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**:
|
#### 注意事项
|
||||||
|
|
||||||
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
|
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
|
||||||
- 如果不挂载 volume,容器删除后数据会丢失
|
- 如果不挂载 volume,容器删除后数据会丢失
|
||||||
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
|
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
|
||||||
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
|
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
|
||||||
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
|
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
|
||||||
|
|
||||||
**权限注意**:如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
#### 目录权限
|
||||||
- 证书生成失败,容器启动脚本会报错退出
|
|
||||||
- 数据库目录创建失败,程序会抛出异常并崩溃
|
如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
||||||
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
|
||||||
|
- 证书生成失败,容器启动脚本会报错退出
|
||||||
|
- 数据库目录创建失败,程序会抛出异常并崩溃
|
||||||
|
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
||||||
|
|
||||||
|
在启动容器前手动设置权限:
|
||||||
|
|
||||||
**解决方案**:在启动容器前手动设置权限:
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### 客户端
|
### 客户端
|
||||||
|
|
||||||
1. 点击右上角设置进入设置页面。<br><br>
|
1. 点击右上角设置进入设置页面。<br><br>
|
||||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||||
|
|
||||||
@@ -309,27 +384,51 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
|||||||
|
|
||||||
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
|
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
|
||||||
|
|
||||||
Windows 平台使用**管理员权限**打开 PowerShell 执行
|
**Windows:** 使用管理员权限打开 PowerShell 执行:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||||
```
|
```
|
||||||
Linux
|
|
||||||
```
|
**Linux:**
|
||||||
|
|
||||||
|
```bash
|
||||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||||
sudo update-ca-certificates
|
sudo update-ca-certificates
|
||||||
```
|
```
|
||||||
macOS
|
|
||||||
```
|
**macOS:**
|
||||||
|
|
||||||
|
```bash
|
||||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web 客户端
|
### 自托管 Web 客户端
|
||||||
|
|
||||||
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||||
|
|
||||||
# 常见问题
|
---
|
||||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 致谢
|
||||||
|
|
||||||
# 致谢
|
|
||||||
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
||||||
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
||||||
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code signing policy
|
||||||
|
|
||||||
|
CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供代码签名。
|
||||||
|
|
||||||
|
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
|
||||||
|
|
||||||
|
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
|
||||||
|
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
|
||||||
|
- **隐私政策:** [查看 CrossDesk 隐私政策](PRIVACY.md#中文)
|
||||||
|
|||||||
@@ -1,39 +1,51 @@
|
|||||||
# CrossDesk
|
# CrossDesk
|
||||||
|
|
||||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||||
[]()
|
|
||||||
|
[](https://www.crossdesk.cn/)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||||
[](https://github.com/kunkundi/crossdesk/actions)
|
[](https://github.com/kunkundi/crossdesk/actions)
|
||||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/issues)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||||
[]()
|
[](https://github.com/kunkundi/crossdesk/forks)
|
||||||
|
|
||||||
[ [中文](README.md) / English ]
|
[ [中文](README.md) / English ]
|
||||||
|
|
||||||
PC Client
|
---
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
### PC Client
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Web Client
|
### Web Client
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Intro
|
---
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
CrossDesk is a lightweight cross-platform remote desktop software.
|
CrossDesk is a lightweight cross-platform remote desktop software.
|
||||||
|
|
||||||
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
|
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## System Requirements
|
## System Requirements
|
||||||
|
|
||||||
| Platform | Minimum Version |
|
| Platform | Minimum Version |
|
||||||
|-----------|-----------------|
|
| --- | --- |
|
||||||
| **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
|
||||||
|
|
||||||
@@ -49,15 +61,17 @@ Before connecting, you can customize configuration options in the settings, such
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Web Client
|
### Using the Web Client
|
||||||
|
|
||||||
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
|
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
|
||||||
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
|
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
|
||||||
|
|
||||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||||
|
|
||||||
### Windows Service (CrossDesk Service)
|
### Windows Service (CrossDesk Service)
|
||||||
|
|
||||||
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
||||||
|
|
||||||
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||||
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
||||||
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
||||||
@@ -65,7 +79,8 @@ CrossDesk provides a local helper service on Windows named **CrossDesk Service**
|
|||||||
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
||||||
|
|
||||||
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
# install
|
# install
|
||||||
.\CrossDesk.exe --service-install
|
.\CrossDesk.exe --service-install
|
||||||
# start
|
# start
|
||||||
@@ -81,31 +96,49 @@ For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_
|
|||||||
|
|
||||||
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## How to build
|
## How to build
|
||||||
|
|
||||||
Requirements:
|
### Build 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 (installed automatically by xmake when the system version is too old)
|
||||||
|
|
||||||
### 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 \
|
||||||
|
libgl1-mesa-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
|
||||||
|
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
|
||||||
|
not required.
|
||||||
|
|
||||||
|
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
|
||||||
|
by Slint's C++ API, so the commands above install GCC 10 and the configuration
|
||||||
|
below selects it explicitly.
|
||||||
|
|
||||||
Install the optional dependencies when Wayland capture or DRM capture is enabled:
|
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,16 +150,28 @@ 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 -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
|
||||||
|
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
|
||||||
|
`--cc`/`--cxx` may still let dependency builds fall back to the system
|
||||||
|
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
|
||||||
|
configuration.
|
||||||
|
|
||||||
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
||||||
|
|
||||||
Example configuration with Wayland and DRM capture enabled:
|
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 -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||||
|
--toolchain=gcc-10 -y
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -134,13 +179,16 @@ Build a Debian package after compiling a release binary on the matching architec
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# amd64
|
# amd64
|
||||||
./scripts/linux/pkg_amd64.sh 1.0.0
|
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||||
|
|
||||||
# arm64
|
# arm64
|
||||||
./scripts/linux/pkg_arm64.sh 1.0.0
|
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -161,13 +209,14 @@ Run:
|
|||||||
xmake r crossdesk
|
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:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
export CUDA_PATH=/usr/local/cuda
|
export CUDA_PATH=/usr/local/cuda
|
||||||
export XMAKE_GLOBALDIR=/data
|
export XMAKE_GLOBALDIR=/data
|
||||||
|
|
||||||
@@ -176,53 +225,68 @@ xmake b --root -vy crossdesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
|
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake require -vy "cuda 12.6.3"
|
xmake require -vy "cuda 12.6.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
After the installation is complete, execute:
|
After the installation is complete, execute:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake require --info "cuda 12.6.3"
|
xmake require --info "cuda 12.6.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
The output will look like this:
|
The output will look like this:
|
||||||
|
|
||||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||||
|
|
||||||
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
|
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
|
||||||
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
|
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
set CUDA_PATH=path_to_cuda_installdir
|
||||||
```
|
```
|
||||||
set CUDA_PATH=path_to_cuda_installdir:
|
|
||||||
```
|
|
||||||
Then re-run:
|
Then re-run:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
xmake f --USE_CUDA=true
|
xmake f --USE_CUDA=true
|
||||||
xmake b -vy crossdesk
|
xmake b -vy crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Notice
|
#### Notice
|
||||||
|
|
||||||
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
|
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
|
||||||
|
|
||||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||||
|
|
||||||
## About Xmake
|
### About Xmake
|
||||||
|
|
||||||
#### Installing Xmake
|
#### Installing Xmake
|
||||||
|
|
||||||
You can install Xmake using one of the following methods:
|
You can install Xmake using one of the following methods:
|
||||||
|
|
||||||
Using curl:
|
Using curl:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
curl -fsSL https://xmake.io/shget.text | bash
|
curl -fsSL https://xmake.io/shget.text | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Using wget:
|
Using wget:
|
||||||
```
|
|
||||||
|
```bash
|
||||||
wget https://xmake.io/shget.text -O - | bash
|
wget https://xmake.io/shget.text -O - | bash
|
||||||
```
|
```
|
||||||
|
|
||||||
Using powershell:
|
Using powershell:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
irm https://xmake.io/psget.text | iex
|
irm https://xmake.io/psget.text | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build Options
|
#### Build Options
|
||||||
```
|
|
||||||
|
```text
|
||||||
# Switch build mode
|
# Switch build mode
|
||||||
xmake f -m debug/release
|
xmake f -m debug/release
|
||||||
|
|
||||||
@@ -236,16 +300,23 @@ xmake b -vy crossdesk
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Run Options
|
#### Run Options
|
||||||
```
|
|
||||||
|
```bash
|
||||||
# Run in debug mode
|
# Run in debug mode
|
||||||
xmake r -d crossdesk
|
xmake r -d crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html) .
|
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Self-Hosted Server
|
## Self-Hosted Server
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
It is recommended to deploy CrossDesk Server using Docker.
|
It is recommended to deploy CrossDesk Server using Docker.
|
||||||
```
|
|
||||||
|
```bash
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
--name crossdesk_server \
|
--name crossdesk_server \
|
||||||
--network host \
|
--network host \
|
||||||
@@ -262,7 +333,8 @@ sudo docker run -d \
|
|||||||
|
|
||||||
The parameters you need to pay attention to are as follows:
|
The parameters you need to pay attention to are as follows:
|
||||||
|
|
||||||
**Parameters**
|
#### Parameters
|
||||||
|
|
||||||
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||||
- **INTERNAL_IP**: The server’s internal IP.
|
- **INTERNAL_IP**: The server’s internal IP.
|
||||||
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||||
@@ -271,7 +343,8 @@ The parameters you need to pay attention to are as follows:
|
|||||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
|
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
|
||||||
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
|
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
|
||||||
|
|
||||||
**Example**:
|
#### Example
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo docker run -d \
|
sudo docker run -d \
|
||||||
--name crossdesk_server \
|
--name crossdesk_server \
|
||||||
@@ -287,29 +360,35 @@ sudo docker run -d \
|
|||||||
crossdesk/crossdesk-server:v1.1.6
|
crossdesk/crossdesk-server:v1.1.6
|
||||||
```
|
```
|
||||||
|
|
||||||
**Notes**
|
#### Notes
|
||||||
|
|
||||||
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
|
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
|
||||||
- If you don’t mount volumes, all data will be lost when the container is removed.
|
- If you don’t mount volumes, all data will be lost when the container is removed.
|
||||||
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
|
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`. As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
|
||||||
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
|
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
|
||||||
- Log files will be created and stored at `/var/log/crossdesk/`.
|
- Log files will be created and stored at `/var/log/crossdesk/`.
|
||||||
|
|
||||||
**Permission Notice**
|
#### Directory permissions
|
||||||
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
|
|
||||||
- Certificate generation failure, leading to startup script errors and container exit.
|
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
|
||||||
- Database directory creation failure, causing the program to throw exceptions and crash.
|
|
||||||
- Log directory creation failure, preventing logs from being written (though the program may continue running).
|
- Certificate generation failure, leading to startup script errors and container exit.
|
||||||
|
- Database directory creation failure, causing the program to throw exceptions and crash.
|
||||||
|
- Log directory creation failure, preventing logs from being written (though the program may continue running).
|
||||||
|
|
||||||
|
Manually set permissions before starting the container:
|
||||||
|
|
||||||
**Solution:** Manually set permissions before starting the container:
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server Side
|
#### TLS certificate
|
||||||
|
|
||||||
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
||||||
|
|
||||||
### Client Side
|
### Client
|
||||||
|
|
||||||
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
|
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
|
||||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||||
|
|
||||||
@@ -322,28 +401,51 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
|
|||||||
|
|
||||||
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
|
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
|
||||||
|
|
||||||
On Windows, open PowerShell with **administrator privileges** and execute:
|
**Windows:** Open PowerShell with administrator privileges and execute:
|
||||||
```
|
|
||||||
|
```powershell
|
||||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||||
```
|
```
|
||||||
Linux
|
|
||||||
```
|
**Linux:**
|
||||||
|
|
||||||
|
```bash
|
||||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||||
sudo update-ca-certificates
|
sudo update-ca-certificates
|
||||||
```
|
```
|
||||||
macOS
|
|
||||||
```
|
**macOS:**
|
||||||
|
|
||||||
|
```bash
|
||||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Self-Hosted Web Client
|
||||||
|
|
||||||
### Web Client
|
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
|
||||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
|
||||||
|
|
||||||
# FAQ
|
---
|
||||||
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
See [FAQ](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
# Acknowledgements
|
|
||||||
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
|
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
|
||||||
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
|
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
|
||||||
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
|
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code signing policy
|
||||||
|
|
||||||
|
CrossDesk uses SignPath.io to code-sign official Windows releases built from this repository.
|
||||||
|
|
||||||
|
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
|
||||||
|
|
||||||
|
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
|
||||||
|
- **Approvers:** [kunkundi](https://github.com/kunkundi)
|
||||||
|
- **Privacy policy:** [Read the CrossDesk Privacy Policy](PRIVACY.md#english)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 746 B After Width: | Height: | Size: 746 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
@@ -1,5 +1,5 @@
|
|||||||
// Application icon resource; load by the resource name IDI_ICON1.
|
// Application icon resource; load by the resource name IDI_ICON1.
|
||||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
IDI_ICON1 ICON "crossdesk.ico"
|
||||||
|
|
||||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||||
#define RT_MANIFEST 24
|
#define RT_MANIFEST 24
|
||||||
@@ -9,3 +9,8 @@ CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
|||||||
#else
|
#else
|
||||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||||
|
|
||||||
|
#include "version_info.rcinc"
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
// Portable build resource. The app itself runs as the current user; service
|
// Portable build resource. The app itself runs as the current user; service
|
||||||
// installation raises a separate UAC prompt only when the user chooses it.
|
// installation raises a separate UAC prompt only when the user chooses it.
|
||||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
IDI_ICON1 ICON "crossdesk.ico"
|
||||||
|
|
||||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||||
#define RT_MANIFEST 24
|
#define RT_MANIFEST 24
|
||||||
|
|
||||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||||
|
|
||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Portable"
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||||
|
|
||||||
|
#include "version_info.rcinc"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Service"
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_service.exe"
|
||||||
|
|
||||||
|
#include "version_info.rcinc"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Session helper icon and manifest resources.
|
||||||
|
IDI_ICON1 ICON "crossdesk.ico"
|
||||||
|
|
||||||
|
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||||
|
#define RT_MANIFEST 24
|
||||||
|
|
||||||
|
#ifdef CROSSDESK_PORTABLE
|
||||||
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||||
|
#elif defined(CROSSDESK_DEBUG)
|
||||||
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||||
|
#else
|
||||||
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
||||||
|
|
||||||
|
#include "version_info.rcinc"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#include <winver.h>
|
||||||
|
|
||||||
|
#ifndef CROSSDESK_VERSION_STRING
|
||||||
|
#define CROSSDESK_VERSION_STRING "0.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CROSSDESK_VERSION_NUMERIC
|
||||||
|
#define CROSSDESK_VERSION_NUMERIC 0,0,0,0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CROSSDESK_FILE_DESCRIPTION
|
||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CROSSDESK_ORIGINAL_FILENAME
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CROSSDESK_FILE_TYPE
|
||||||
|
#define CROSSDESK_FILE_TYPE VFT_APP
|
||||||
|
#endif
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEVERSION CROSSDESK_VERSION_NUMERIC
|
||||||
|
PRODUCTVERSION CROSSDESK_VERSION_NUMERIC
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef CROSSDESK_DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE CROSSDESK_FILE_TYPE
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904B0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "CrossDesk\0"
|
||||||
|
VALUE "FileDescription", CROSSDESK_FILE_DESCRIPTION "\0"
|
||||||
|
VALUE "FileVersion", CROSSDESK_VERSION_STRING "\0"
|
||||||
|
VALUE "InternalName", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||||
|
VALUE "LegalCopyright", "Copyright (C) CrossDesk contributors\0"
|
||||||
|
VALUE "OriginalFilename", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||||
|
VALUE "ProductName", "CrossDesk\0"
|
||||||
|
VALUE "ProductVersion", CROSSDESK_VERSION_STRING "\0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x0409, 1200
|
||||||
|
END
|
||||||
|
END
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Windows Graphics Capture Plugin"
|
||||||
|
#define CROSSDESK_ORIGINAL_FILENAME "wgc_plugin.dll"
|
||||||
|
#define CROSSDESK_FILE_TYPE VFT_DLL
|
||||||
|
|
||||||
|
#include "version_info.rcinc"
|
||||||
@@ -17,9 +17,10 @@ MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
|||||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||||
|
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
|
||||||
normalize_app_version() {
|
normalize_app_version() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
@@ -104,6 +105,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 \
|
||||||
@@ -132,11 +142,11 @@ ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
|
|||||||
|
|
||||||
for size in 16 24 32 48 64 96 128 256; do
|
for size in 16 24 32 48 64 96 128 256; do
|
||||||
install -d "$ICON_BASE_DIR/${size}x${size}/apps"
|
install -d "$ICON_BASE_DIR/${size}x${size}/apps"
|
||||||
install -m 0644 "$PROJECT_ROOT/icons/linux/crossdesk_${size}x${size}.png" \
|
install -m 0644 "$PROJECT_ROOT/apps/desktop/resources/linux/crossdesk_${size}x${size}.png" \
|
||||||
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
||||||
done
|
done
|
||||||
|
|
||||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS"
|
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
|
||||||
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
||||||
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
||||||
fi
|
fi
|
||||||
@@ -147,11 +157,11 @@ 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, libxcursor1, 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,
|
||||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
||||||
Recommends: $RECOMMENDS
|
Recommends: $RECOMMENDS
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Section: utils
|
Section: utils
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
APP_NAME="crossdesk"
|
APP_NAME="crossdesk"
|
||||||
APP_NAME_UPPER="CrossDesk"
|
APP_NAME_UPPER="CrossDesk"
|
||||||
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
|
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/arm64/release/crossdesk"
|
||||||
PLATFORM="macos"
|
PLATFORM="macos"
|
||||||
ARCH="arm64"
|
ARCH="arm64"
|
||||||
BINARY_ARCH="arm64"
|
BINARY_ARCH="arm64"
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
IDENTIFIER="cn.crossdesk.app"
|
||||||
ICON_PATH="icons/macos/crossdesk.icns"
|
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||||
MACOS_MIN_VERSION="14.0"
|
MACOS_MIN_VERSION="14.0"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
normalize_app_version() {
|
normalize_app_version() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
@@ -103,7 +106,7 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
|||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
mkdir -p build_pkg_scripts
|
||||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||||
chmod +x build_pkg_scripts/postinstall
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
mkdir -p build_pkg_resources
|
mkdir -p build_pkg_resources
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
APP_NAME="crossdesk"
|
APP_NAME="crossdesk"
|
||||||
APP_NAME_UPPER="CrossDesk"
|
APP_NAME_UPPER="CrossDesk"
|
||||||
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
|
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/x86_64/release/crossdesk"
|
||||||
PLATFORM="macos"
|
PLATFORM="macos"
|
||||||
ARCH="x64"
|
ARCH="x64"
|
||||||
BINARY_ARCH="x86_64"
|
BINARY_ARCH="x86_64"
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
IDENTIFIER="cn.crossdesk.app"
|
||||||
ICON_PATH="icons/macos/crossdesk.icns"
|
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||||
MACOS_MIN_VERSION="14.0"
|
MACOS_MIN_VERSION="14.0"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
normalize_app_version() {
|
normalize_app_version() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
@@ -103,7 +106,7 @@ find "${APP_BUNDLE}" -name '._*' -delete
|
|||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
mkdir -p build_pkg_scripts
|
||||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
cp "${SCRIPT_DIR}/tcc_postinstall.sh" build_pkg_scripts/postinstall
|
||||||
chmod +x build_pkg_scripts/postinstall
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
mkdir -p build_pkg_resources
|
mkdir -p build_pkg_resources
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
; Installer initial constants
|
; Installer initial constants
|
||||||
!define PRODUCT_NAME "CrossDesk"
|
!define PRODUCT_NAME "CrossDesk"
|
||||||
!define PRODUCT_VERSION "${VERSION}"
|
!define PRODUCT_VERSION "${VERSION}"
|
||||||
|
!define PRODUCT_VERSION_NUMERIC "${VERSION_NUMERIC}"
|
||||||
!define PRODUCT_PUBLISHER "CrossDesk"
|
!define PRODUCT_PUBLISHER "CrossDesk"
|
||||||
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
||||||
!define APP_NAME "CrossDesk"
|
!define APP_NAME "CrossDesk"
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
||||||
|
|
||||||
; Installer icon path
|
; Installer icon path
|
||||||
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
|
!define MUI_ICON "${__FILEDIR__}\..\..\resources\windows\crossdesk.ico"
|
||||||
|
|
||||||
; Compression settings
|
; Compression settings
|
||||||
SetCompressor /FINAL lzma
|
SetCompressor /FINAL lzma
|
||||||
@@ -41,6 +42,14 @@ RequestExecutionLevel admin
|
|||||||
|
|
||||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||||
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||||
|
VIProductVersion "${PRODUCT_VERSION_NUMERIC}"
|
||||||
|
VIAddVersionKey /LANG=2052 "CompanyName" "CrossDesk"
|
||||||
|
VIAddVersionKey /LANG=2052 "FileDescription" "CrossDesk Installer"
|
||||||
|
VIAddVersionKey /LANG=2052 "FileVersion" "${PRODUCT_VERSION}"
|
||||||
|
VIAddVersionKey /LANG=2052 "LegalCopyright" "Copyright (C) CrossDesk contributors"
|
||||||
|
VIAddVersionKey /LANG=2052 "OriginalFilename" "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||||
|
VIAddVersionKey /LANG=2052 "ProductName" "${PRODUCT_NAME}"
|
||||||
|
VIAddVersionKey /LANG=2052 "ProductVersion" "${PRODUCT_VERSION}"
|
||||||
InstallDir "$PROGRAMFILES\CrossDesk"
|
InstallDir "$PROGRAMFILES\CrossDesk"
|
||||||
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
||||||
ShowInstDetails show
|
ShowInstDetails show
|
||||||
@@ -75,12 +84,12 @@ installApp:
|
|||||||
SetOverwrite ifnewer
|
SetOverwrite ifnewer
|
||||||
|
|
||||||
; Main application executable path
|
; Main application executable path
|
||||||
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
|
File /oname=CrossDesk.exe "..\..\..\..\build\windows\x64\release\crossdesk.exe"
|
||||||
; Bundle service-side binaries required by the Windows service flow
|
; Bundle service-side binaries required by the Windows service flow
|
||||||
File "..\..\build\windows\x64\release\crossdesk_service.exe"
|
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||||
File "..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||||
; Bundle runtime DLLs from the release output directory
|
; Bundle runtime DLLs from the release output directory
|
||||||
File "..\..\build\windows\x64\release\*.dll"
|
File "..\..\..\..\build\windows\x64\release\*.dll"
|
||||||
|
|
||||||
Call RegisterInstalledService
|
Call RegisterInstalledService
|
||||||
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Normalize-AppVersion {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$InputVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
$prefix = ""
|
||||||
|
$body = $InputVersion
|
||||||
|
|
||||||
|
if ($body.StartsWith("v")) {
|
||||||
|
$prefix = "v"
|
||||||
|
$body = $body.Substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||||
|
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $InputVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-To-WindowsVersion {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$InputVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
$body = $InputVersion -replace '^v', ''
|
||||||
|
$baseMatch = [regex]::Match($body, '^[0-9]+(?:\.[0-9]+){0,3}')
|
||||||
|
if (!$baseMatch.Success) {
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = [System.Collections.Generic.List[int]]::new()
|
||||||
|
foreach ($part in $baseMatch.Value.Split('.')) {
|
||||||
|
$value = [int]$part
|
||||||
|
if ($value -gt 65535) {
|
||||||
|
$value = 0
|
||||||
|
}
|
||||||
|
$parts.Add($value)
|
||||||
|
}
|
||||||
|
while ($parts.Count -lt 4) {
|
||||||
|
$parts.Add(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($parts -join '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||||
|
$windowsVersion = Convert-To-WindowsVersion -InputVersion $normalizedVersion
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
|
||||||
|
Push-Location $scriptDir
|
||||||
|
try {
|
||||||
|
& makensis "/DVERSION=$normalizedVersion" "/DVERSION_NUMERIC=$windowsVersion" "nsis_script.nsi"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||||
|
<parameters>
|
||||||
|
<parameter name="version" required="true" />
|
||||||
|
</parameters>
|
||||||
|
<pe-file path="crossdesk-win-x64-${version}.exe"
|
||||||
|
product-name="CrossDesk"
|
||||||
|
product-version="${version}"
|
||||||
|
file-version="${version}"
|
||||||
|
company-name="CrossDesk"
|
||||||
|
copyright="Copyright (C) CrossDesk contributors"
|
||||||
|
original-filename="crossdesk-win-x64-${version}.exe">
|
||||||
|
<authenticode-sign />
|
||||||
|
</pe-file>
|
||||||
|
</artifact-configuration>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#include "daemon.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "platform/daemon_backend.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kRestartDelayMs = 1000;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
|
||||||
|
|
||||||
|
void Daemon::stop() {
|
||||||
|
running_.store(false);
|
||||||
|
crossdesk::platform::RequestDaemonStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Daemon::isRunning() const {
|
||||||
|
return running_.load() && !crossdesk::platform::DaemonStopRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Daemon::start(MainLoopFunc loop) {
|
||||||
|
crossdesk::platform::ResetDaemonStop();
|
||||||
|
if (!crossdesk::platform::PrepareDaemon()) return false;
|
||||||
|
running_.store(true);
|
||||||
|
return runWithRestart(std::move(loop));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||||
|
int restart_count = 0;
|
||||||
|
const std::string executable_path =
|
||||||
|
crossdesk::platform::GetDaemonExecutablePath();
|
||||||
|
if (executable_path.empty()) {
|
||||||
|
std::cerr
|
||||||
|
<< "Failed to get executable path, falling back to direct execution"
|
||||||
|
<< std::endl;
|
||||||
|
while (isRunning()) {
|
||||||
|
try {
|
||||||
|
loop();
|
||||||
|
break;
|
||||||
|
} catch (...) {
|
||||||
|
++restart_count;
|
||||||
|
std::cerr << "Exception caught, restarting... (attempt "
|
||||||
|
<< restart_count << ")" << std::endl;
|
||||||
|
std::this_thread::sleep_for(
|
||||||
|
std::chrono::milliseconds(kRestartDelayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (isRunning()) {
|
||||||
|
const crossdesk::platform::DaemonChildResult result =
|
||||||
|
crossdesk::platform::RunDaemonChild(
|
||||||
|
executable_path, [this] { return isRunning(); });
|
||||||
|
if (!isRunning() || result.normal_exit) break;
|
||||||
|
|
||||||
|
++restart_count;
|
||||||
|
if (!result.started) {
|
||||||
|
std::cerr << "Failed to create child process";
|
||||||
|
} else if (result.terminated_by_signal) {
|
||||||
|
std::cerr << "Child process crashed with signal " << result.exit_code;
|
||||||
|
} else {
|
||||||
|
std::cerr << "Child process exited with code " << result.exit_code;
|
||||||
|
}
|
||||||
|
std::cerr << ", restarting... (attempt " << restart_count << ")"
|
||||||
|
<< std::endl;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
#define _DAEMON_H_
|
#define _DAEMON_H_
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <csignal>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -28,9 +27,6 @@ class Daemon {
|
|||||||
std::string name_;
|
std::string name_;
|
||||||
bool runWithRestart(MainLoopFunc loop);
|
bool runWithRestart(MainLoopFunc loop);
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
static volatile std::sig_atomic_t stop_requested_;
|
|
||||||
#endif
|
|
||||||
std::atomic<bool> running_;
|
std::atomic<bool> running_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include "autostart.h"
|
||||||
|
|
||||||
|
#include "platform/autostart_backend.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
bool EnableAutostart(const std::string& app_name) {
|
||||||
|
const std::string executable_path = platform::GetAutostartExecutablePath();
|
||||||
|
return !executable_path.empty() &&
|
||||||
|
platform::EnableAutostart(app_name, executable_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DisableAutostart(const std::string& app_name) {
|
||||||
|
return platform::DisableAutostart(app_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAutostartEnabled(const std::string& app_name) {
|
||||||
|
return platform::IsAutostartEnabled(app_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "nv12_scaler.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "libyuv/planar_functions.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||||
|
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||||
|
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||||
|
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||||
|
int dst_height, libyuv::FilterMode filtering,
|
||||||
|
std::vector<uint8_t>* scratch_buffer) {
|
||||||
|
if (!src_y || !src_uv || !dst_y || !dst_uv || !scratch_buffer ||
|
||||||
|
src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 ||
|
||||||
|
(src_width & 1) != 0 || (src_height & 1) != 0 || (dst_width & 1) != 0 ||
|
||||||
|
(dst_height & 1) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int src_chroma_width = src_width / 2;
|
||||||
|
const int src_chroma_height = src_height / 2;
|
||||||
|
const int dst_chroma_width = dst_width / 2;
|
||||||
|
const int dst_chroma_height = dst_height / 2;
|
||||||
|
const size_t src_chroma_size =
|
||||||
|
static_cast<size_t>(src_chroma_width) * src_chroma_height;
|
||||||
|
const size_t dst_chroma_size =
|
||||||
|
static_cast<size_t>(dst_chroma_width) * dst_chroma_height;
|
||||||
|
const size_t max_size = std::numeric_limits<size_t>::max();
|
||||||
|
if (src_chroma_size > max_size / 2 || dst_chroma_size > max_size / 2) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t src_planes_size = 2 * src_chroma_size;
|
||||||
|
const size_t dst_planes_size = 2 * dst_chroma_size;
|
||||||
|
if (src_planes_size > max_size - dst_planes_size) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
scratch_buffer->resize(src_planes_size + dst_planes_size);
|
||||||
|
uint8_t* src_u = scratch_buffer->data();
|
||||||
|
uint8_t* src_v = src_u + src_chroma_size;
|
||||||
|
uint8_t* dst_u = src_v + src_chroma_size;
|
||||||
|
uint8_t* dst_v = dst_u + dst_chroma_size;
|
||||||
|
|
||||||
|
libyuv::SplitUVPlane(src_uv, src_stride_uv, src_u, src_chroma_width, src_v,
|
||||||
|
src_chroma_width, src_chroma_width, src_chroma_height);
|
||||||
|
const int result = libyuv::I420Scale(
|
||||||
|
src_y, src_stride_y, src_u, src_chroma_width, src_v, src_chroma_width,
|
||||||
|
src_width, src_height, dst_y, dst_stride_y, dst_u, dst_chroma_width,
|
||||||
|
dst_v, dst_chroma_width, dst_width, dst_height, filtering);
|
||||||
|
if (result != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
libyuv::MergeUVPlane(dst_u, dst_chroma_width, dst_v, dst_chroma_width, dst_uv,
|
||||||
|
dst_stride_uv, dst_chroma_width, dst_chroma_height);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#ifndef CROSSDESK_COMMON_NV12_SCALER_H_
|
||||||
|
#define CROSSDESK_COMMON_NV12_SCALER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "libyuv/scale.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Scales NV12 by separating the interleaved chroma plane before filtering.
|
||||||
|
// This avoids the downward chroma rounding bias in libyuv::NV12Scale while
|
||||||
|
// preserving the requested filtering quality.
|
||||||
|
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||||
|
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||||
|
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||||
|
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||||
|
int dst_height, libyuv::FilterMode filtering,
|
||||||
|
std::vector<uint8_t>* scratch_buffer);
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_COMMON_NV12_SCALER_H_
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
#ifndef _PLATFORM_H_
|
#ifndef _PLATFORM_H_
|
||||||
#define _PLATFORM_H_
|
#define _PLATFORM_H_
|
||||||
|
|
||||||
#include <iostream>
|
#include <string>
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#include "shared_cursor_state.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::mutex g_cursor_state_mutex;
|
||||||
|
SharedCursorState g_cursor_state;
|
||||||
|
bool g_cursor_state_available = false;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||||
|
++g_cursor_state.generation;
|
||||||
|
g_cursor_state.visible = visible;
|
||||||
|
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
|
||||||
|
g_cursor_state_available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetSharedCursorState(SharedCursorState* state) {
|
||||||
|
if (!state) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||||
|
if (!g_cursor_state_available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*state = g_cursor_state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearSharedCursorState() {
|
||||||
|
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||||
|
++g_cursor_state.generation;
|
||||||
|
g_cursor_state.visible = false;
|
||||||
|
g_cursor_state.shape = RemoteCursorShape::none;
|
||||||
|
g_cursor_state_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||||
|
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <remote_cursor_shape.h>
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
struct SharedCursorState {
|
||||||
|
uint64_t generation = 0;
|
||||||
|
bool visible = false;
|
||||||
|
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
|
||||||
|
bool GetSharedCursorState(SharedCursorState* state);
|
||||||
|
void ClearSharedCursorState();
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||||
@@ -29,7 +29,13 @@ int ConfigCenter::Load() {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool persist_turn_mode_migration = false;
|
bool persist_config_migration = false;
|
||||||
|
persist_config_migration |= ini_.Delete(section_, "video_content_type");
|
||||||
|
persist_config_migration |= ini_.Delete(section_, "screen_content");
|
||||||
|
persist_config_migration |=
|
||||||
|
ini_.Delete(section_, "enable_desktop_quality_optimization");
|
||||||
|
persist_config_migration |=
|
||||||
|
ini_.Delete(section_, "enable_minimize_to_tray");
|
||||||
|
|
||||||
const long language_value =
|
const long language_value =
|
||||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||||
@@ -52,7 +58,6 @@ int ConfigCenter::Load() {
|
|||||||
|
|
||||||
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
||||||
hardware_video_codec_);
|
hardware_video_codec_);
|
||||||
|
|
||||||
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
|
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
|
||||||
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
|
||||||
const long parsed_turn_mode = ini_.GetLongValue(
|
const long parsed_turn_mode = ini_.GetLongValue(
|
||||||
@@ -70,7 +75,7 @@ int ConfigCenter::Load() {
|
|||||||
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
|
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
|
||||||
: TURN_MODE::DISABLED;
|
: TURN_MODE::DISABLED;
|
||||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||||
persist_turn_mode_migration = true;
|
persist_config_migration = true;
|
||||||
}
|
}
|
||||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||||
enable_self_hosted_ =
|
enable_self_hosted_ =
|
||||||
@@ -106,8 +111,6 @@ int ConfigCenter::Load() {
|
|||||||
enable_autostart_ =
|
enable_autostart_ =
|
||||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
|
||||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
|
||||||
portable_service_prompt_suppressed_ =
|
portable_service_prompt_suppressed_ =
|
||||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||||
portable_service_prompt_suppressed_);
|
portable_service_prompt_suppressed_);
|
||||||
@@ -121,8 +124,7 @@ int ConfigCenter::Load() {
|
|||||||
file_transfer_save_path_ = "";
|
file_transfer_save_path_ = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persist_turn_mode_migration &&
|
if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||||
ini_.SaveFile(config_path_.c_str()) < 0) {
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +157,6 @@ int ConfigCenter::Save() {
|
|||||||
|
|
||||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
|
||||||
enable_minimize_to_tray_);
|
|
||||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||||
portable_service_prompt_suppressed_);
|
portable_service_prompt_suppressed_);
|
||||||
|
|
||||||
@@ -336,17 +336,6 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ConfigCenter::SetMinimizeToTray(bool enable_minimize_to_tray) {
|
|
||||||
enable_minimize_to_tray_ = enable_minimize_to_tray;
|
|
||||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
|
||||||
enable_minimize_to_tray_);
|
|
||||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
|
||||||
if (rc < 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ConfigCenter::SetAutostart(bool enable_autostart) {
|
int ConfigCenter::SetAutostart(bool enable_autostart) {
|
||||||
enable_autostart_ = enable_autostart;
|
enable_autostart_ = enable_autostart;
|
||||||
bool success = false;
|
bool success = false;
|
||||||
@@ -446,8 +435,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
|
|||||||
|
|
||||||
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
||||||
|
|
||||||
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
|
|
||||||
|
|
||||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||||
|
|
||||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||||
@@ -43,7 +43,6 @@ class ConfigCenter {
|
|||||||
int SetServerPort(int signal_server_port);
|
int SetServerPort(int signal_server_port);
|
||||||
int SetCoturnServerPort(int coturn_server_port);
|
int SetCoturnServerPort(int coturn_server_port);
|
||||||
int SetSelfHosted(bool enable_self_hosted);
|
int SetSelfHosted(bool enable_self_hosted);
|
||||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
|
||||||
int SetAutostart(bool enable_autostart);
|
int SetAutostart(bool enable_autostart);
|
||||||
int SetDaemon(bool enable_daemon);
|
int SetDaemon(bool enable_daemon);
|
||||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||||
@@ -66,7 +65,6 @@ class ConfigCenter {
|
|||||||
int GetDefaultSignalServerPort() const;
|
int GetDefaultSignalServerPort() const;
|
||||||
int GetDefaultCoturnServerPort() const;
|
int GetDefaultCoturnServerPort() const;
|
||||||
bool IsSelfHosted() const;
|
bool IsSelfHosted() const;
|
||||||
bool IsMinimizeToTray() const;
|
|
||||||
bool IsEnableAutostart() const;
|
bool IsEnableAutostart() const;
|
||||||
bool IsEnableDaemon() const;
|
bool IsEnableDaemon() const;
|
||||||
bool IsPortableServicePromptSuppressed() const;
|
bool IsPortableServicePromptSuppressed() const;
|
||||||
@@ -94,7 +92,6 @@ class ConfigCenter {
|
|||||||
int coturn_server_port_ = 0;
|
int coturn_server_port_ = 0;
|
||||||
int coturn_server_port_default_ = 3478;
|
int coturn_server_port_default_ = 3478;
|
||||||
bool enable_self_hosted_ = false;
|
bool enable_self_hosted_ = false;
|
||||||
bool enable_minimize_to_tray_ = false;
|
|
||||||
bool enable_autostart_ = false;
|
bool enable_autostart_ = false;
|
||||||
bool enable_daemon_ = false;
|
bool enable_daemon_ = false;
|
||||||
bool portable_service_prompt_suppressed_ = false;
|
bool portable_service_prompt_suppressed_ = false;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* @Author: DI JUNKUN
|
||||||
|
* @Date: 2023-12-14
|
||||||
|
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _DEVICE_CONTROLLER_H_
|
||||||
|
#define _DEVICE_CONTROLLER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include "display_info.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// int key_code, bool is_down, uint32_t scan_code, bool extended
|
||||||
|
using OnKeyAction = void (*)(int, bool, uint32_t, bool, void*);
|
||||||
|
|
||||||
|
class DeviceController {
|
||||||
|
public:
|
||||||
|
virtual ~DeviceController() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MouseController : public DeviceController {
|
||||||
|
public:
|
||||||
|
~MouseController() override = default;
|
||||||
|
|
||||||
|
virtual int Init(std::vector<DisplayInfo> display_info_list) = 0;
|
||||||
|
virtual int Destroy() = 0;
|
||||||
|
virtual int SendMouseCommand(RemoteAction remote_action,
|
||||||
|
int display_index) = 0;
|
||||||
|
virtual void UpdateDisplayInfoList(
|
||||||
|
const std::vector<DisplayInfo>&) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class KeyboardCapturer : public DeviceController {
|
||||||
|
public:
|
||||||
|
~KeyboardCapturer() override = default;
|
||||||
|
|
||||||
|
virtual int Hook(OnKeyAction on_key_action, void* user_ptr) = 0;
|
||||||
|
virtual int Unhook() = 0;
|
||||||
|
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||||
|
uint32_t scan_code = 0,
|
||||||
|
bool extended = false) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -8,8 +8,6 @@
|
|||||||
#define _DEVICE_CONTROLLER_FACTORY_H_
|
#define _DEVICE_CONTROLLER_FACTORY_H_
|
||||||
|
|
||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
#include "keyboard_capturer.h"
|
|
||||||
#include "mouse_controller.h"
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -21,16 +19,7 @@ class DeviceControllerFactory {
|
|||||||
virtual ~DeviceControllerFactory() {}
|
virtual ~DeviceControllerFactory() {}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
DeviceController* Create(Device device) {
|
DeviceController* Create(Device device);
|
||||||
switch (device) {
|
|
||||||
case Mouse:
|
|
||||||
return new MouseController();
|
|
||||||
case Keyboard:
|
|
||||||
return new KeyboardCapturer();
|
|
||||||
default:
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
#endif
|
#endif
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
#ifndef _GUI_APPLICATION_H_
|
||||||
#define CROSSDESK_GUI_APPLICATION_H_
|
#define _GUI_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -19,6 +24,12 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
struct SlintUi;
|
struct SlintUi;
|
||||||
|
struct CursorDeliveryState {
|
||||||
|
CursorState last_sent{};
|
||||||
|
bool has_sent = false;
|
||||||
|
bool feedback_pending = false;
|
||||||
|
std::chrono::steady_clock::time_point last_sent_time{};
|
||||||
|
};
|
||||||
|
|
||||||
void InitializeLogger();
|
void InitializeLogger();
|
||||||
void InitializeSettings();
|
void InitializeSettings();
|
||||||
@@ -31,10 +42,16 @@ private:
|
|||||||
void BindStreamCallbacks();
|
void BindStreamCallbacks();
|
||||||
void BindServerCallbacks();
|
void BindServerCallbacks();
|
||||||
void Tick();
|
void Tick();
|
||||||
|
void ShareLocalCursorState();
|
||||||
|
void HandlePasswordChangeResult();
|
||||||
|
void HandleCredentialRecovery();
|
||||||
void SyncMainWindow();
|
void SyncMainWindow();
|
||||||
void SyncConnectionDialog();
|
void SyncConnectionDialog();
|
||||||
void SyncPlatformDialogs();
|
void SyncPlatformDialogs();
|
||||||
void SyncStreamWindow();
|
void SyncStreamWindow();
|
||||||
|
void SyncStreamVideoFrame();
|
||||||
|
void ScheduleNextVideoFrame();
|
||||||
|
void ConfigureStreamVideoRenderer();
|
||||||
void SyncStreamKeyboardFocus();
|
void SyncStreamKeyboardFocus();
|
||||||
void SetStreamKeyboardFocus(bool focused);
|
void SetStreamKeyboardFocus(bool focused);
|
||||||
void SyncServerWindow();
|
void SyncServerWindow();
|
||||||
@@ -64,11 +81,17 @@ private:
|
|||||||
bool OpenUrl(const std::string &url);
|
bool OpenUrl(const std::string &url);
|
||||||
|
|
||||||
std::unique_ptr<SlintUi> ui_;
|
std::unique_ptr<SlintUi> ui_;
|
||||||
|
CursorStateProvider cursor_state_provider_;
|
||||||
|
std::unordered_map<std::string, CursorDeliveryState>
|
||||||
|
cursor_delivery_states_;
|
||||||
|
uint32_t cursor_state_sequence_ = 0;
|
||||||
|
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
bool use_xwayland_gui_ = false;
|
bool use_xwayland_gui_ = false;
|
||||||
|
bool use_x11_custom_titlebar_ = false;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif // CROSSDESK_GUI_APPLICATION_H_
|
#endif
|
||||||
@@ -134,8 +134,10 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (main_window_ &&
|
if (main_window_ &&
|
||||||
event.window.windowID == SDL_GetWindowID(main_window_) &&
|
event.window.windowID == SDL_GetWindowID(main_window_)) {
|
||||||
MinimizeMainWindowToTray()) {
|
if (!MinimizeMainWindowToTray()) {
|
||||||
|
SDL_HideWindow(main_window_);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "application/gui_application.h"
|
#include "application/gui_application.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -233,6 +235,15 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
|||||||
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
||||||
controlled_remote_id_ = "";
|
controlled_remote_id_ = "";
|
||||||
RemoteAction remote_action{};
|
RemoteAction remote_action{};
|
||||||
|
|
||||||
|
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||||
|
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
|
||||||
|
event.button.button != SDL_BUTTON_LEFT &&
|
||||||
|
event.button.button != SDL_BUTTON_RIGHT &&
|
||||||
|
event.button.button != SDL_BUTTON_MIDDLE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
float cursor_x = last_mouse_event.motion.x;
|
float cursor_x = last_mouse_event.motion.x;
|
||||||
float cursor_y = last_mouse_event.motion.y;
|
float cursor_y = last_mouse_event.motion.y;
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iterator>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace crossdesk::server_window_state {
|
||||||
|
|
||||||
|
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||||
|
std::string* selected_id) {
|
||||||
|
if (!selected_id) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ids.empty()) {
|
||||||
|
selected_id->clear();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto selected = std::find(ids.begin(), ids.end(), *selected_id);
|
||||||
|
if (selected == ids.end()) {
|
||||||
|
*selected_id = ids.front();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk::server_window_state
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace crossdesk::window_geometry {
|
||||||
|
|
||||||
|
struct PhysicalRect {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PhysicalSize {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PhysicalPosition {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr PhysicalPosition CenteredPosition(const PhysicalRect& bounds,
|
||||||
|
const PhysicalSize& size) {
|
||||||
|
return {
|
||||||
|
bounds.x + (bounds.width - std::min(bounds.width, size.width)) / 2,
|
||||||
|
bounds.y + (bounds.height - std::min(bounds.height, size.height)) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||||
|
const PhysicalSize& size) {
|
||||||
|
return {
|
||||||
|
bounds.x + std::max(0, bounds.width - size.width),
|
||||||
|
bounds.y + std::max(0, bounds.height - size.height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk::window_geometry
|
||||||
@@ -150,7 +150,7 @@ int GuiApplication::DrawServerWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||||
if (!enable_minimize_to_tray_ || !main_window_) {
|
if (!main_window_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
#ifndef _LOCALIZATION_H_
|
#ifndef _LOCALIZATION_H_
|
||||||
#define _LOCALIZATION_H_
|
#define _LOCALIZATION_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -115,14 +116,29 @@ inline const std::string& LocalizedString::operator[](
|
|||||||
return detail::GetTranslatedText(key_, language_index);
|
return detail::GetTranslatedText(key_, language_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string FormatDisplayLabel(size_t display_index,
|
||||||
|
const std::string& display_name,
|
||||||
|
int language_index) {
|
||||||
|
const std::string number = std::to_string(display_index + 1);
|
||||||
|
std::string label =
|
||||||
|
detail::GetTranslatedText("display_screen", language_index) + " " +
|
||||||
|
number;
|
||||||
|
const std::string fallback_name = "Display" + number;
|
||||||
|
if (!display_name.empty() && display_name != fallback_name) {
|
||||||
|
label += " (" + display_name + ")";
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||||
inline const LocalizedString name(#name);
|
inline const LocalizedString name(#name);
|
||||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
inline const wchar_t* GetWindowsLocalizedLabel(
|
||||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
int language_index, const char* key, const wchar_t* fallback,
|
||||||
|
std::vector<std::wstring>& cache) {
|
||||||
const int normalized_index = detail::ClampLanguageIndex(language_index);
|
const int normalized_index = detail::ClampLanguageIndex(language_index);
|
||||||
std::wstring& cached_text = cache[normalized_index];
|
std::wstring& cached_text = cache[normalized_index];
|
||||||
if (!cached_text.empty()) {
|
if (!cached_text.empty()) {
|
||||||
@@ -130,24 +146,46 @@ inline const wchar_t* GetExitProgramLabel(int language_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string& utf8_text =
|
const std::string& utf8_text =
|
||||||
detail::GetTranslatedText("exit_program", normalized_index);
|
detail::GetTranslatedText(key, normalized_index);
|
||||||
if (utf8_text.empty()) {
|
if (utf8_text.empty()) {
|
||||||
cached_text = L"Exit";
|
cached_text = fallback;
|
||||||
return cached_text.c_str();
|
return cached_text.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
int wide_length =
|
int wide_length =
|
||||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
||||||
if (wide_length <= 0) {
|
if (wide_length <= 0) {
|
||||||
cached_text = L"Exit";
|
cached_text = fallback;
|
||||||
return cached_text.c_str();
|
return cached_text.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_text.resize(static_cast<size_t>(wide_length - 1));
|
cached_text.resize(static_cast<size_t>(wide_length));
|
||||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
|
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
|
||||||
wide_length);
|
cached_text.data(), wide_length) <= 0) {
|
||||||
|
cached_text = fallback;
|
||||||
|
return cached_text.c_str();
|
||||||
|
}
|
||||||
|
cached_text.pop_back();
|
||||||
return cached_text.c_str();
|
return cached_text.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
|
||||||
|
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||||
|
return GetWindowsLocalizedLabel(language_index, "show_main_window",
|
||||||
|
L"Show Main Window", cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const wchar_t* GetSettingsLabel(int language_index) {
|
||||||
|
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||||
|
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
|
||||||
|
cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||||
|
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||||
|
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
|
||||||
|
cache);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace localization
|
} // namespace localization
|
||||||
@@ -35,6 +35,7 @@ struct TranslationRow {
|
|||||||
u8"Недавние подключения") \
|
u8"Недавние подключения") \
|
||||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||||
|
X(display_screen, u8"显示屏", "Display", u8"Экран") \
|
||||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||||
u8"Развернуть панель управления") \
|
u8"Развернуть панель управления") \
|
||||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||||
@@ -214,8 +215,6 @@ struct TranslationRow {
|
|||||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
|
||||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
|
||||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||||
@@ -232,6 +231,8 @@ struct TranslationRow {
|
|||||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||||
"The application requires the following permissions:", \
|
"The application requires the following permissions:", \
|
||||||
u8"Для работы приложения требуются следующие разрешения:") \
|
u8"Для работы приложения требуются следующие разрешения:") \
|
||||||
|
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||||
|
u8"Показать главное окно") \
|
||||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||||
|
|
||||||
inline constexpr TranslationRow kTranslationRows[] = {
|
inline constexpr TranslationRow kTranslationRows[] = {
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
#include "features/devices/session_device_manager.h"
|
#include "features/devices/session_device_manager.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <display_stream_id.h>
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
@@ -11,11 +14,30 @@ namespace crossdesk {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||||
|
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||||
|
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
||||||
|
|
||||||
|
bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||||
|
std::chrono::steady_clock::time_point now, int fps) {
|
||||||
|
const auto interval =
|
||||||
|
std::chrono::nanoseconds(std::chrono::seconds(1)) / (fps > 0 ? fps : 1);
|
||||||
|
if (next_frame_deadline_ == std::chrono::steady_clock::time_point{}) {
|
||||||
|
next_frame_deadline_ = now;
|
||||||
|
}
|
||||||
|
if (now + kFrameDeadlineTolerance < next_frame_deadline_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
next_frame_deadline_ += interval;
|
||||||
|
if (next_frame_deadline_ <= now) {
|
||||||
|
next_frame_deadline_ = now + interval;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void SessionDeviceManager::Initialize() {
|
void SessionDeviceManager::Initialize() {
|
||||||
InitializeAudioOutput();
|
InitializeAudioOutput();
|
||||||
screen_capturer_factory_ = new ScreenCapturerFactory();
|
screen_capturer_factory_ = new ScreenCapturerFactory();
|
||||||
@@ -38,37 +60,79 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
|||||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||||
}
|
}
|
||||||
|
|
||||||
last_frame_time_ = std::chrono::duration_cast<std::chrono::milliseconds>(
|
last_frame_time_ = {};
|
||||||
std::chrono::steady_clock::now().time_since_epoch())
|
next_frame_deadline_ = {};
|
||||||
.count();
|
|
||||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||||
? 30
|
? 30
|
||||||
: 60;
|
: 60;
|
||||||
LOG_INFO("Init screen capturer with {} fps", fps);
|
LOG_INFO("Init screen capturer with {} fps", fps);
|
||||||
|
display_info_list_.clear();
|
||||||
|
registered_display_stream_count_ = 0;
|
||||||
|
last_video_frame_stream_id_.clear();
|
||||||
|
invalid_video_stream_id_logged_ = false;
|
||||||
|
|
||||||
const int init_ret = screen_capturer_->Init(
|
const int init_ret = screen_capturer_->Init(
|
||||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||||
const char *display_name) {
|
const char *display_name) {
|
||||||
const auto now_time = static_cast<uint64_t>(
|
const auto now_time = std::chrono::steady_clock::now();
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||||
std::chrono::steady_clock::now().time_since_epoch())
|
|
||||||
.count());
|
|
||||||
const auto duration = now_time - last_frame_time_;
|
|
||||||
if (duration * fps < 1000) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string stream_id = display_name ? display_name : "";
|
const bool has_previous_frame =
|
||||||
|
last_frame_time_.time_since_epoch().count() != 0;
|
||||||
|
const auto duration_ms =
|
||||||
|
has_previous_frame
|
||||||
|
? std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now_time - last_frame_time_)
|
||||||
|
.count()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
std::vector<std::string> connected_remote_ids;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||||
|
connected_remote_ids.reserve(owner_.connection_status_.size());
|
||||||
|
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
||||||
|
if (status == ConnectionStatus::Connected) {
|
||||||
|
connected_remote_ids.push_back(remote_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture can still deliver frames while ICE is gathering or after
|
||||||
|
// the final controller disconnects. Do not broadcast those frames to
|
||||||
|
// MiniRTC: a broadcast also reaches newly joining peers whose ICE
|
||||||
|
// transport is not ready yet.
|
||||||
|
if (connected_remote_ids.empty()) {
|
||||||
|
last_frame_time_ = now_time;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string stream_id = ResolveDisplayStreamId(
|
||||||
|
display_name, registered_display_stream_count_, -1,
|
||||||
|
last_video_frame_stream_id_);
|
||||||
|
if (stream_id.empty()) {
|
||||||
|
if (!invalid_video_stream_id_logged_) {
|
||||||
|
LOG_ERROR(
|
||||||
|
"Drop captured frames with an empty or unregistered video "
|
||||||
|
"stream id, reported='{}', registered_streams={}",
|
||||||
|
display_name ? display_name : "",
|
||||||
|
registered_display_stream_count_);
|
||||||
|
invalid_video_stream_id_logged_ = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invalid_video_stream_id_logged_ = false;
|
||||||
const bool resumed_after_gap =
|
const bool resumed_after_gap =
|
||||||
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
|
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
||||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||||
last_video_frame_stream_id_ != stream_id;
|
last_video_frame_stream_id_ != stream_id;
|
||||||
if (resumed_after_gap || stream_changed) {
|
if (resumed_after_gap || stream_changed) {
|
||||||
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
||||||
LOG_INFO("Request video key frame before sending captured frame, "
|
LOG_INFO("Request video key frame before sending captured frame, "
|
||||||
"stream='{}', gap_ms={}, stream_changed={}",
|
"stream='{}', gap_ms={}, stream_changed={}",
|
||||||
stream_id, duration, stream_changed);
|
stream_id, duration_ms, stream_changed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +142,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
|||||||
frame.width = width;
|
frame.width = width;
|
||||||
frame.height = height;
|
frame.height = height;
|
||||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||||
SendVideoFrame(owner_.peer_, &frame, stream_id.c_str());
|
for (const std::string &remote_id : connected_remote_ids) {
|
||||||
|
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||||
|
remote_id.data(), remote_id.size());
|
||||||
|
}
|
||||||
last_video_frame_stream_id_ = stream_id;
|
last_video_frame_stream_id_ = stream_id;
|
||||||
last_frame_time_ = now_time;
|
last_frame_time_ = now_time;
|
||||||
});
|
});
|
||||||
@@ -89,6 +156,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
|||||||
if (!latest_display_info.empty()) {
|
if (!latest_display_info.empty()) {
|
||||||
display_info_list_ = latest_display_info;
|
display_info_list_ = latest_display_info;
|
||||||
}
|
}
|
||||||
|
registered_display_stream_count_ = display_info_list_.size();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,18 +324,18 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
|||||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||||
void *user_ptr) {
|
void *user_ptr) {
|
||||||
if (user_ptr) {
|
if (user_ptr) {
|
||||||
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
|
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
||||||
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
|
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
||||||
extended);
|
extended);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&owner_);
|
this);
|
||||||
if (hook_ret != 0) {
|
if (hook_ret != 0) {
|
||||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Start keyboard capturer failed, falling back to Slint 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 input");
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -281,6 +349,7 @@ int SessionDeviceManager::StopKeyboardCapturer() {
|
|||||||
|
|
||||||
if (keyboard_capturer_) {
|
if (keyboard_capturer_) {
|
||||||
keyboard_capturer_->Unhook();
|
keyboard_capturer_->Unhook();
|
||||||
|
ClearCapturedKeyboardInput();
|
||||||
LOG_INFO("Stop keyboard capturer");
|
LOG_INFO("Stop keyboard capturer");
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -394,7 +463,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
|||||||
owner_.mouse_controller_is_started_ = false;
|
owner_.mouse_controller_is_started_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) || defined(__APPLE__)
|
||||||
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
|
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
|
||||||
mouse_controller_) {
|
mouse_controller_) {
|
||||||
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
|
||||||
@@ -410,6 +479,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
|||||||
owner_.keyboard_capturer_is_started_ = true;
|
owner_.keyboard_capturer_is_started_ = true;
|
||||||
}
|
}
|
||||||
if (owner_.keyboard_capturer_is_started_) {
|
if (owner_.keyboard_capturer_is_started_) {
|
||||||
|
DrainCapturedKeyboardInput();
|
||||||
owner_.keyboard_.SendHeartbeat(false);
|
owner_.keyboard_.SendHeartbeat(false);
|
||||||
}
|
}
|
||||||
} else if (owner_.keyboard_capturer_is_started_) {
|
} else if (owner_.keyboard_capturer_is_started_) {
|
||||||
@@ -421,6 +491,36 @@ void SessionDeviceManager::UpdateInteractions() {
|
|||||||
owner_.keyboard_.CheckRemoteTimeouts();
|
owner_.keyboard_.CheckRemoteTimeouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
|
||||||
|
bool is_down,
|
||||||
|
uint32_t scan_code,
|
||||||
|
bool extended) {
|
||||||
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||||
|
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
|
||||||
|
captured_keyboard_inputs_.pop_front();
|
||||||
|
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
|
||||||
|
}
|
||||||
|
captured_keyboard_inputs_.push_back(
|
||||||
|
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||||
|
std::deque<CapturedKeyboardInput> inputs;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||||
|
inputs.swap(captured_keyboard_inputs_);
|
||||||
|
}
|
||||||
|
for (const CapturedKeyboardInput &input : inputs) {
|
||||||
|
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||||
|
input.scan_code, input.extended);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SessionDeviceManager::ClearCapturedKeyboardInput() {
|
||||||
|
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||||
|
captured_keyboard_inputs_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||||
uint32_t scan_code,
|
uint32_t scan_code,
|
||||||
bool extended) {
|
bool extended) {
|
||||||
@@ -3,11 +3,16 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
#include "device_controller_factory.h"
|
#include "device_controller_factory.h"
|
||||||
#include "display_info.h"
|
#include "display_info.h"
|
||||||
@@ -53,6 +58,20 @@ public:
|
|||||||
const std::vector<DisplayInfo> &display_info_list() const;
|
const std::vector<DisplayInfo> &display_info_list() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct CapturedKeyboardInput {
|
||||||
|
int key_code = 0;
|
||||||
|
bool is_down = false;
|
||||||
|
uint32_t scan_code = 0;
|
||||||
|
bool extended = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void QueueCapturedKeyboardInput(int key_code, bool is_down,
|
||||||
|
uint32_t scan_code, bool extended);
|
||||||
|
void DrainCapturedKeyboardInput();
|
||||||
|
void ClearCapturedKeyboardInput();
|
||||||
|
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||||
|
int fps);
|
||||||
|
|
||||||
GuiRuntime &owner_;
|
GuiRuntime &owner_;
|
||||||
SDL_AudioStream *output_stream_ = nullptr;
|
SDL_AudioStream *output_stream_ = nullptr;
|
||||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||||
@@ -63,8 +82,13 @@ private:
|
|||||||
MouseController *mouse_controller_ = nullptr;
|
MouseController *mouse_controller_ = nullptr;
|
||||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||||
std::vector<DisplayInfo> display_info_list_;
|
std::vector<DisplayInfo> display_info_list_;
|
||||||
uint64_t last_frame_time_ = 0;
|
size_t registered_display_stream_count_ = 0;
|
||||||
|
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||||
|
std::mutex captured_keyboard_inputs_mutex_;
|
||||||
|
std::chrono::steady_clock::time_point last_frame_time_{};
|
||||||
|
std::chrono::steady_clock::time_point next_frame_deadline_{};
|
||||||
std::string last_video_frame_stream_id_;
|
std::string last_video_frame_stream_id_;
|
||||||
|
bool invalid_video_stream_id_logged_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -236,15 +235,9 @@ void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FileTransferManager::HandleAck(const char *data, size_t size) {
|
void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||||
if (size < sizeof(FileTransferAck)) {
|
|
||||||
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FileTransferAck ack{};
|
FileTransferAck ack{};
|
||||||
std::memcpy(&ack, data, sizeof(ack));
|
if (!DecodeFileTransferAck(data, size, &ack)) {
|
||||||
if (ack.magic != kFileAckMagic) {
|
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
|
||||||
LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
#include "features/input/keyboard_controller.h"
|
#include "features/input/keyboard_controller.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
@@ -45,8 +47,8 @@ int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||||
bool *extended_out) {
|
bool* extended_out) {
|
||||||
if (!scan_code_out || !extended_out) {
|
if (!scan_code_out || !extended_out) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -65,7 +67,7 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
|||||||
#if _WIN32
|
#if _WIN32
|
||||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||||
|
|
||||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
|
||||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||||
return;
|
return;
|
||||||
@@ -77,8 +79,8 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
|||||||
stage ? stage : "");
|
stage ? stage : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||||
const RemoteAction &action) {
|
const RemoteAction& action) {
|
||||||
return response.is_object() &&
|
return response.is_object() &&
|
||||||
response.value("error", std::string()) == "send_input_failed" &&
|
response.value("error", std::string()) == "send_input_failed" &&
|
||||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||||
@@ -87,9 +89,9 @@ bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
|
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
|
||||||
|
|
||||||
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
||||||
uint32_t scan_code, bool extended) {
|
uint32_t scan_code, bool extended) {
|
||||||
@@ -106,13 +108,13 @@ void KeyboardController::ForceReleasePressedKeys() {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||||
pressed_keys.reserve(pressed_keys_.size());
|
pressed_keys.reserve(pressed_keys_.size());
|
||||||
for (const auto &[_, key] : pressed_keys_) {
|
for (const auto& [_, key] : pressed_keys_) {
|
||||||
pressed_keys.push_back(key);
|
pressed_keys.push_back(key);
|
||||||
}
|
}
|
||||||
pressed_keys_.clear();
|
pressed_keys_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const PressedKey &key : pressed_keys) {
|
for (const PressedKey& key : pressed_keys) {
|
||||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||||
}
|
}
|
||||||
SendHeartbeat(true);
|
SendHeartbeat(true);
|
||||||
@@ -130,7 +132,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||||
size_t index = 0;
|
size_t index = 0;
|
||||||
for (const auto &[_, key] : pressed_keys_) {
|
for (const auto& [_, key] : pressed_keys_) {
|
||||||
if (index >= kMaxKeyboardStateKeys) {
|
if (index >= kMaxKeyboardStateKeys) {
|
||||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||||
pressed_keys_.size());
|
pressed_keys_.size());
|
||||||
@@ -223,9 +225,10 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
|||||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||||
if (!json.is_discarded() &&
|
if (!json.is_discarded() &&
|
||||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
IsTransientSecureDesktopInputFailure(json, action)) {
|
||||||
LOG_INFO("Secure desktop keyboard injection transient failure, "
|
LOG_INFO(
|
||||||
"key_code={}, is_down={}, response={}",
|
"Secure desktop keyboard injection transient failure, "
|
||||||
key_code, is_down, response);
|
"key_code={}, is_down={}, response={}",
|
||||||
|
key_code, is_down, response);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,15 +248,15 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
|||||||
extended);
|
extended);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||||
const RemoteAction &action) {
|
const RemoteAction& action) {
|
||||||
const int key_code = static_cast<int>(action.k.key_value);
|
const int key_code = static_cast<int>(action.k.key_value);
|
||||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||||
const bool injected =
|
const bool injected =
|
||||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
auto &state = remote_states_[remote_id];
|
auto& state = remote_states_[remote_id];
|
||||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||||
if (is_down && injected) {
|
if (is_down && injected) {
|
||||||
state.pressed_keys[key_code] =
|
state.pressed_keys[key_code] =
|
||||||
@@ -263,13 +266,13 @@ void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||||
const RemoteAction &action) {
|
const RemoteAction& action) {
|
||||||
std::vector<PressedKey> keys_to_release;
|
std::vector<PressedKey> keys_to_release;
|
||||||
std::vector<PressedKey> keys_to_press;
|
std::vector<PressedKey> keys_to_press;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
auto &state = remote_states_[remote_id];
|
auto& state = remote_states_[remote_id];
|
||||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -283,24 +286,24 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
|||||||
const size_t count =
|
const size_t count =
|
||||||
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
|
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
|
||||||
for (size_t index = 0; index < count; ++index) {
|
for (size_t index = 0; index < count; ++index) {
|
||||||
const auto &key = action.ks.pressed_keys[index];
|
const auto& key = action.ks.pressed_keys[index];
|
||||||
const int key_code = static_cast<int>(key.key_value);
|
const int key_code = static_cast<int>(key.key_value);
|
||||||
desired_keys[key_code] =
|
desired_keys[key_code] =
|
||||||
PressedKey{key_code, key.scan_code, key.extended};
|
PressedKey{key_code, key.scan_code, key.extended};
|
||||||
}
|
}
|
||||||
for (const auto &[key_code, key] : state.pressed_keys) {
|
for (const auto& [key_code, key] : state.pressed_keys) {
|
||||||
if (desired_keys.find(key_code) == desired_keys.end()) {
|
if (desired_keys.find(key_code) == desired_keys.end()) {
|
||||||
keys_to_release.push_back(key);
|
keys_to_release.push_back(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto &[key_code, key] : desired_keys) {
|
for (const auto& [key_code, key] : desired_keys) {
|
||||||
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
|
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
|
||||||
keys_to_press.push_back(key);
|
keys_to_press.push_back(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const PressedKey &key : keys_to_release) {
|
for (const PressedKey& key : keys_to_release) {
|
||||||
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
const auto state_it = remote_states_.find(remote_id);
|
const auto state_it = remote_states_.find(remote_id);
|
||||||
@@ -309,7 +312,7 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const PressedKey &key : keys_to_press) {
|
for (const PressedKey& key : keys_to_press) {
|
||||||
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
||||||
@@ -317,8 +320,8 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||||
const char *reason) {
|
const char* reason) {
|
||||||
std::vector<PressedKey> keys_to_release;
|
std::vector<PressedKey> keys_to_release;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
@@ -326,7 +329,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
|||||||
if (state_it == remote_states_.end()) {
|
if (state_it == remote_states_.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const auto &[_, key] : state_it->second.pressed_keys) {
|
for (const auto& [_, key] : state_it->second.pressed_keys) {
|
||||||
keys_to_release.push_back(key);
|
keys_to_release.push_back(key);
|
||||||
}
|
}
|
||||||
remote_states_.erase(state_it);
|
remote_states_.erase(state_it);
|
||||||
@@ -336,7 +339,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
|||||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||||
}
|
}
|
||||||
for (const PressedKey &key : keys_to_release) {
|
for (const PressedKey& key : keys_to_release) {
|
||||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +349,7 @@ void KeyboardController::CheckRemoteTimeouts() {
|
|||||||
std::vector<std::string> timed_out_remotes;
|
std::vector<std::string> timed_out_remotes;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||||
for (const auto &[remote_id, state] : remote_states_) {
|
for (const auto& [remote_id, state] : remote_states_) {
|
||||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||||
state.last_seen_tick != 0 &&
|
state.last_seen_tick != 0 &&
|
||||||
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
||||||
@@ -354,9 +357,9 @@ void KeyboardController::CheckRemoteTimeouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const std::string &remote_id : timed_out_remotes) {
|
for (const std::string& remote_id : timed_out_remotes) {
|
||||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
#include "device_controller.h"
|
#include "device_controller.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -0,0 +1,622 @@
|
|||||||
|
#include "features/settings/settings_manager.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <exception>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <io.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "localization.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
|
||||||
|
constexpr uint32_t kCacheV3Version = 3;
|
||||||
|
|
||||||
|
template <size_t Size>
|
||||||
|
void CopyString(char (&destination)[Size], const char *source) {
|
||||||
|
static_assert(Size > 0);
|
||||||
|
std::memset(destination, 0, Size);
|
||||||
|
if (source) {
|
||||||
|
std::strncpy(destination, source, Size - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t CacheChecksum(const void *data, size_t size) {
|
||||||
|
const auto *bytes = static_cast<const unsigned char *>(data);
|
||||||
|
uint32_t hash = 2166136261u;
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
hash ^= bytes[i];
|
||||||
|
hash *= 16777619u;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path TemporaryPathFor(
|
||||||
|
const std::filesystem::path &target) {
|
||||||
|
const auto timestamp = std::chrono::steady_clock::now()
|
||||||
|
.time_since_epoch()
|
||||||
|
.count();
|
||||||
|
const auto thread_id =
|
||||||
|
std::hash<std::thread::id>{}(std::this_thread::get_id());
|
||||||
|
std::filesystem::path temporary = target;
|
||||||
|
temporary += ".tmp-" + std::to_string(timestamp) + "-" +
|
||||||
|
std::to_string(thread_id);
|
||||||
|
return temporary;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FlushFileToDisk(FILE *file) {
|
||||||
|
if (!file || std::fflush(file) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return _commit(_fileno(file)) == 0;
|
||||||
|
#else
|
||||||
|
return fsync(fileno(file)) == 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReplaceFileAtomically(const std::filesystem::path &temporary,
|
||||||
|
const std::filesystem::path &target) {
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return MoveFileExW(temporary.c_str(), target.c_str(),
|
||||||
|
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
|
||||||
|
#else
|
||||||
|
if (::rename(temporary.c_str(), target.c_str()) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path parent = target.parent_path().empty()
|
||||||
|
? std::filesystem::path(".")
|
||||||
|
: target.parent_path();
|
||||||
|
int directory_flags = O_RDONLY;
|
||||||
|
#if defined(O_DIRECTORY)
|
||||||
|
directory_flags |= O_DIRECTORY;
|
||||||
|
#endif
|
||||||
|
const int directory = open(parent.c_str(), directory_flags);
|
||||||
|
if (directory >= 0) {
|
||||||
|
const bool synced = fsync(directory) == 0;
|
||||||
|
close(directory);
|
||||||
|
return synced;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteFileAtomically(const std::filesystem::path &target,
|
||||||
|
const void *data, size_t size) {
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path parent = target.parent_path();
|
||||||
|
if (!parent.empty()) {
|
||||||
|
std::filesystem::create_directories(parent, ec);
|
||||||
|
if (ec) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path temporary = TemporaryPathFor(target);
|
||||||
|
#if defined(_WIN32)
|
||||||
|
FILE *file = _wfopen(temporary.c_str(), L"wb");
|
||||||
|
#else
|
||||||
|
FILE *file = std::fopen(temporary.c_str(), "wb");
|
||||||
|
#endif
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool written = std::fwrite(data, 1, size, file) == size;
|
||||||
|
const bool flushed = written && FlushFileToDisk(file);
|
||||||
|
const bool closed = std::fclose(file) == 0;
|
||||||
|
if (!written || !flushed || !closed ||
|
||||||
|
!ReplaceFileAtomically(temporary, target)) {
|
||||||
|
std::filesystem::remove(temporary, ec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
||||||
|
|
||||||
|
int SettingsManager::Save() {
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
return SaveLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsManager::SaveLocked() {
|
||||||
|
CopyString(cache_v2_.client_id_with_password,
|
||||||
|
owner_.client_id_with_password_);
|
||||||
|
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||||
|
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||||
|
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
|
||||||
|
|
||||||
|
cache_v3_.magic = kCacheV3Magic;
|
||||||
|
cache_v3_.version = kCacheV3Version;
|
||||||
|
cache_v3_.base = cache_v2_;
|
||||||
|
cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0';
|
||||||
|
cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] =
|
||||||
|
'\0';
|
||||||
|
cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] =
|
||||||
|
'\0';
|
||||||
|
cache_v3_.checksum =
|
||||||
|
CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum));
|
||||||
|
|
||||||
|
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||||
|
&cache_v3_, sizeof(cache_v3_))) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||||
|
&cache_v2_, sizeof(cache_v2_))) {
|
||||||
|
LOG_WARN("Failed to update legacy v2 credential cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep writing the legacy cache while older installations may still read it.
|
||||||
|
CopyString(cache_v1_.client_id_with_password,
|
||||||
|
owner_.client_id_with_password_);
|
||||||
|
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
|
||||||
|
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
|
||||||
|
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
|
||||||
|
&cache_v1_, sizeof(cache_v1_))) {
|
||||||
|
LOG_WARN("Failed to update legacy v1 credential cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::ReadV3Locked() {
|
||||||
|
std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||||
|
std::ios::binary);
|
||||||
|
if (!cache_file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheV3 loaded{};
|
||||||
|
cache_file.read(reinterpret_cast<char *>(&loaded), sizeof(loaded));
|
||||||
|
if (cache_file.gcount() != static_cast<std::streamsize>(sizeof(loaded)) ||
|
||||||
|
loaded.magic != kCacheV3Magic ||
|
||||||
|
loaded.version != kCacheV3Version ||
|
||||||
|
loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) {
|
||||||
|
LOG_WARN("Ignore invalid v3 credential cache");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded.base.client_id_with_password
|
||||||
|
[sizeof(loaded.base.client_id_with_password) - 1] = '\0';
|
||||||
|
loaded.base.self_hosted_id[sizeof(loaded.base.self_hosted_id) - 1] = '\0';
|
||||||
|
loaded.pending_identity[sizeof(loaded.pending_identity) - 1] = '\0';
|
||||||
|
loaded.pending_server_host[sizeof(loaded.pending_server_host) - 1] = '\0';
|
||||||
|
loaded.pending_request_id[sizeof(loaded.pending_request_id) - 1] = '\0';
|
||||||
|
cache_v3_ = loaded;
|
||||||
|
cache_v2_ = loaded.base;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::ReadV2Locked() {
|
||||||
|
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||||
|
std::ios::binary);
|
||||||
|
if (!cache_v2_file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cache_v2_file.read(reinterpret_cast<char *>(&cache_v2_), sizeof(cache_v2_));
|
||||||
|
if (cache_v2_file.gcount() !=
|
||||||
|
static_cast<std::streamsize>(sizeof(cache_v2_))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cache_v2_
|
||||||
|
.client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] =
|
||||||
|
'\0';
|
||||||
|
cache_v2_.self_hosted_id[sizeof(cache_v2_.self_hosted_id) - 1] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsManager::Load() {
|
||||||
|
std::unique_lock<std::mutex> lock(cache_mutex_);
|
||||||
|
|
||||||
|
const bool loaded_v3 = ReadV3Locked();
|
||||||
|
if (loaded_v3 || ReadV2Locked()) {
|
||||||
|
CopyString(owner_.client_id_with_password_,
|
||||||
|
cache_v2_.client_id_with_password);
|
||||||
|
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
||||||
|
std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key));
|
||||||
|
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
|
||||||
|
if (loaded_v3) {
|
||||||
|
LOG_INFO("Load settings from v3 cache file");
|
||||||
|
} else {
|
||||||
|
cache_v3_ = {};
|
||||||
|
SaveLocked();
|
||||||
|
LOG_INFO("Migrated settings from v2 to v3 cache file");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
|
||||||
|
std::ios::binary);
|
||||||
|
if (!cache_v1_file) {
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
std::memset(owner_.aes128_key_, 0, sizeof(owner_.aes128_key_));
|
||||||
|
std::memset(owner_.aes128_iv_, 0, sizeof(owner_.aes128_iv_));
|
||||||
|
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||||
|
|
||||||
|
owner_.thumbnail_ =
|
||||||
|
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/");
|
||||||
|
owner_.thumbnail_->GetKeyAndIv(owner_.aes128_key_, owner_.aes128_iv_);
|
||||||
|
owner_.thumbnail_->DeleteAllFilesInDirectory();
|
||||||
|
|
||||||
|
Save();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_v1_file.read(reinterpret_cast<char *>(&cache_v1_), sizeof(cache_v1_));
|
||||||
|
cache_v1_
|
||||||
|
.client_id_with_password[sizeof(cache_v1_.client_id_with_password) -
|
||||||
|
1] = '\0';
|
||||||
|
|
||||||
|
CopyString(cache_v2_.client_id_with_password,
|
||||||
|
cache_v1_.client_id_with_password);
|
||||||
|
std::memcpy(cache_v2_.key, cache_v1_.key, sizeof(cache_v1_.key));
|
||||||
|
std::memcpy(cache_v2_.iv, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||||
|
std::memset(cache_v2_.self_hosted_id, 0, sizeof(cache_v2_.self_hosted_id));
|
||||||
|
|
||||||
|
CopyString(owner_.client_id_with_password_,
|
||||||
|
cache_v1_.client_id_with_password);
|
||||||
|
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||||
|
std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key));
|
||||||
|
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
|
||||||
|
|
||||||
|
cache_v3_ = {};
|
||||||
|
SaveLocked();
|
||||||
|
LOG_INFO("Migrated settings from v1 to v3 cache file");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
ActivateCachedPublicIdentity();
|
||||||
|
|
||||||
|
owner_.thumbnail_ =
|
||||||
|
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
|
||||||
|
owner_.aes128_key_, owner_.aes128_iv_);
|
||||||
|
|
||||||
|
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
|
||||||
|
static_cast<int>(owner_.config_center_->GetLanguage()));
|
||||||
|
owner_.video_quality_button_value_ =
|
||||||
|
static_cast<int>(owner_.config_center_->GetVideoQuality());
|
||||||
|
owner_.video_frame_rate_button_value_ =
|
||||||
|
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
|
||||||
|
owner_.video_encode_format_button_value_ =
|
||||||
|
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
|
||||||
|
owner_.enable_hardware_video_codec_ =
|
||||||
|
owner_.config_center_->IsHardwareVideoCodec();
|
||||||
|
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
|
||||||
|
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
|
||||||
|
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
|
||||||
|
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
|
||||||
|
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
owner_.portable_service_prompt_suppressed_ =
|
||||||
|
owner_.config_center_->IsPortableServicePromptSuppressed();
|
||||||
|
owner_.portable_service_do_not_remind_ =
|
||||||
|
owner_.portable_service_prompt_suppressed_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const std::string saved_path =
|
||||||
|
owner_.config_center_->GetFileTransferSavePath();
|
||||||
|
CopyString(owner_.file_transfer_save_path_buf_, saved_path.c_str());
|
||||||
|
owner_.file_transfer_save_path_last_ = saved_path;
|
||||||
|
|
||||||
|
owner_.language_button_value_last_ = owner_.language_button_value_;
|
||||||
|
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
|
||||||
|
owner_.video_encode_format_button_value_last_ =
|
||||||
|
owner_.video_encode_format_button_value_;
|
||||||
|
owner_.enable_hardware_video_codec_last_ =
|
||||||
|
owner_.enable_hardware_video_codec_;
|
||||||
|
owner_.enable_turn_last_ = owner_.enable_turn_;
|
||||||
|
owner_.enable_srtp_last_ = owner_.enable_srtp_;
|
||||||
|
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
|
||||||
|
owner_.enable_autostart_last_ = owner_.enable_autostart_;
|
||||||
|
|
||||||
|
LOG_INFO("Load settings from cache file");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
if ((!ReadV3Locked() && !ReadV2Locked()) ||
|
||||||
|
cache_v2_.self_hosted_id[0] == '\0') {
|
||||||
|
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||||
|
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
|
||||||
|
const char *at_pos = std::strchr(owner_.self_hosted_id_, '@');
|
||||||
|
if (at_pos == nullptr) {
|
||||||
|
CopyString(owner_.client_id_, owner_.self_hosted_id_);
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
} else {
|
||||||
|
const std::string id(owner_.self_hosted_id_,
|
||||||
|
at_pos - owner_.self_hosted_id_);
|
||||||
|
CopyString(owner_.client_id_, id.c_str());
|
||||||
|
CopyString(owner_.password_saved_, at_pos + 1);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::ActivateCachedPublicIdentity() {
|
||||||
|
if (owner_.client_id_with_password_[0] == '\0') {
|
||||||
|
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
|
||||||
|
if (at_pos == nullptr) {
|
||||||
|
CopyString(owner_.client_id_, owner_.client_id_with_password_);
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
} else {
|
||||||
|
const std::string id(owner_.client_id_with_password_,
|
||||||
|
at_pos - owner_.client_id_with_password_);
|
||||||
|
CopyString(owner_.client_id_, id.c_str());
|
||||||
|
CopyString(owner_.password_saved_, at_pos + 1);
|
||||||
|
}
|
||||||
|
return owner_.client_id_[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsManager::ActivateIdentity(const char *identity,
|
||||||
|
bool self_hosted) {
|
||||||
|
if (!identity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self_hosted) {
|
||||||
|
CopyString(owner_.self_hosted_id_, identity);
|
||||||
|
} else {
|
||||||
|
CopyString(owner_.client_id_with_password_, identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *at_pos = std::strchr(identity, '@');
|
||||||
|
if (at_pos == nullptr) {
|
||||||
|
CopyString(owner_.client_id_, identity);
|
||||||
|
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string id(identity, at_pos - identity);
|
||||||
|
CopyString(owner_.client_id_, id.c_str());
|
||||||
|
CopyString(owner_.password_saved_, at_pos + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::StagePendingPasswordChange(
|
||||||
|
const std::string &identity, bool self_hosted,
|
||||||
|
const std::string &server_host, int server_port,
|
||||||
|
const std::string &request_id) {
|
||||||
|
if (identity.empty() || identity.size() >= sizeof(cache_v3_.pending_identity) ||
|
||||||
|
server_host.empty() ||
|
||||||
|
server_host.size() >= sizeof(cache_v3_.pending_server_host) ||
|
||||||
|
server_port <= 0 || request_id.empty() ||
|
||||||
|
request_id.size() >= sizeof(cache_v3_.pending_request_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
if (cache_v3_.pending_identity[0] != '\0' &&
|
||||||
|
(cache_v3_.pending_self_hosted != self_hosted ||
|
||||||
|
cache_v3_.pending_server_port != server_port ||
|
||||||
|
server_host != cache_v3_.pending_server_host ||
|
||||||
|
request_id != cache_v3_.pending_request_id)) {
|
||||||
|
LOG_WARN("Refuse to overwrite an unresolved password change");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const CacheV3 previous = cache_v3_;
|
||||||
|
CopyString(cache_v3_.pending_identity, identity.c_str());
|
||||||
|
CopyString(cache_v3_.pending_server_host, server_host.c_str());
|
||||||
|
cache_v3_.pending_server_port = server_port;
|
||||||
|
cache_v3_.pending_self_hosted = self_hosted;
|
||||||
|
CopyString(cache_v3_.pending_request_id, request_id.c_str());
|
||||||
|
if (SaveLocked() != 0) {
|
||||||
|
cache_v3_ = previous;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SettingsManager::PendingPasswordChangeIdentity(
|
||||||
|
bool self_hosted, const std::string &server_host, int server_port) const {
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
if (cache_v3_.pending_identity[0] == '\0' ||
|
||||||
|
cache_v3_.pending_self_hosted != self_hosted ||
|
||||||
|
cache_v3_.pending_server_port != server_port ||
|
||||||
|
server_host != cache_v3_.pending_server_host) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return cache_v3_.pending_identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::PromotePendingPasswordChange() {
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
if (cache_v3_.pending_identity[0] == '\0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CacheV3 previous = cache_v3_;
|
||||||
|
const std::string identity = cache_v3_.pending_identity;
|
||||||
|
const bool self_hosted = cache_v3_.pending_self_hosted;
|
||||||
|
ActivateIdentity(identity.c_str(), self_hosted);
|
||||||
|
std::memset(cache_v3_.pending_identity, 0,
|
||||||
|
sizeof(cache_v3_.pending_identity));
|
||||||
|
std::memset(cache_v3_.pending_server_host, 0,
|
||||||
|
sizeof(cache_v3_.pending_server_host));
|
||||||
|
cache_v3_.pending_server_port = 0;
|
||||||
|
cache_v3_.pending_self_hosted = false;
|
||||||
|
std::memset(cache_v3_.pending_request_id, 0,
|
||||||
|
sizeof(cache_v3_.pending_request_id));
|
||||||
|
if (SaveLocked() != 0) {
|
||||||
|
// The already-durable pending credential remains the recovery source on
|
||||||
|
// disk. Keep it in memory as well so a reconnect in this process retries
|
||||||
|
// the credential that the server has accepted.
|
||||||
|
cache_v3_ = previous;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsManager::ClearPendingPasswordChange() {
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
if (cache_v3_.pending_identity[0] == '\0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CacheV3 previous = cache_v3_;
|
||||||
|
std::memset(cache_v3_.pending_identity, 0,
|
||||||
|
sizeof(cache_v3_.pending_identity));
|
||||||
|
std::memset(cache_v3_.pending_server_host, 0,
|
||||||
|
sizeof(cache_v3_.pending_server_host));
|
||||||
|
cache_v3_.pending_server_port = 0;
|
||||||
|
cache_v3_.pending_self_hosted = false;
|
||||||
|
std::memset(cache_v3_.pending_request_id, 0,
|
||||||
|
sizeof(cache_v3_.pending_request_id));
|
||||||
|
if (SaveLocked() != 0) {
|
||||||
|
cache_v3_ = previous;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
|
||||||
|
if (!client_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||||
|
CopyString(owner_.self_hosted_id_, client_id);
|
||||||
|
if (SaveLocked() != 0) {
|
||||||
|
LOG_ERROR("Failed to persist self-hosted identity atomically");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsManager::LoadRecentConnectionAliases() {
|
||||||
|
recent_connection_aliases_.clear();
|
||||||
|
|
||||||
|
std::ifstream alias_file(owner_.cache_path_ +
|
||||||
|
"/recent_connection_aliases.json");
|
||||||
|
if (!alias_file.good()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
nlohmann::json alias_json;
|
||||||
|
alias_file >> alias_json;
|
||||||
|
|
||||||
|
const nlohmann::json *aliases = &alias_json;
|
||||||
|
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
|
||||||
|
aliases = &alias_json["aliases"];
|
||||||
|
}
|
||||||
|
if (!aliases->is_object()) {
|
||||||
|
LOG_WARN("Invalid recent connection alias file");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
|
||||||
|
if (it.value().is_string()) {
|
||||||
|
const std::string remote_id = it.key();
|
||||||
|
const std::string alias = it.value().get<std::string>();
|
||||||
|
if (!remote_id.empty() && !alias.empty()) {
|
||||||
|
recent_connection_aliases_[remote_id] = alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
LOG_WARN("Load recent connection aliases failed: {}", e.what());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsManager::SaveRecentConnectionAliases() const {
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(owner_.cache_path_, ec);
|
||||||
|
if (ec) {
|
||||||
|
LOG_WARN("Create cache directory failed while saving aliases: {}",
|
||||||
|
ec.message());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json alias_json;
|
||||||
|
alias_json["aliases"] = nlohmann::json::object();
|
||||||
|
for (const auto &[remote_id, alias] : recent_connection_aliases_) {
|
||||||
|
if (!remote_id.empty() && !alias.empty()) {
|
||||||
|
alias_json["aliases"][remote_id] = alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream alias_file(
|
||||||
|
owner_.cache_path_ + "/recent_connection_aliases.json", std::ios::trunc);
|
||||||
|
if (!alias_file.good()) {
|
||||||
|
LOG_WARN("Open recent connection alias file failed");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
alias_file << alias_json.dump(2);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string SettingsManager::RecentConnectionDisplayName(
|
||||||
|
const Thumbnail::RecentConnection &connection) const {
|
||||||
|
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
|
||||||
|
if (alias_it != recent_connection_aliases_.end() &&
|
||||||
|
!alias_it->second.empty()) {
|
||||||
|
return alias_it->second;
|
||||||
|
}
|
||||||
|
if (!connection.remote_host_name.empty() &&
|
||||||
|
connection.remote_host_name != "unknown") {
|
||||||
|
return connection.remote_host_name;
|
||||||
|
}
|
||||||
|
return connection.remote_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsManager::BeginEditRecentConnectionAlias(
|
||||||
|
const Thumbnail::RecentConnection &connection) {
|
||||||
|
owner_.edit_connection_alias_remote_id_ = connection.remote_id;
|
||||||
|
std::memset(owner_.edit_connection_alias_, 0,
|
||||||
|
sizeof(owner_.edit_connection_alias_));
|
||||||
|
|
||||||
|
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
|
||||||
|
const std::string alias = alias_it != recent_connection_aliases_.end()
|
||||||
|
? alias_it->second
|
||||||
|
: RecentConnectionDisplayName(connection);
|
||||||
|
CopyString(owner_.edit_connection_alias_, alias.c_str());
|
||||||
|
|
||||||
|
owner_.focus_on_input_widget_ = true;
|
||||||
|
owner_.show_edit_connection_alias_window_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id,
|
||||||
|
const std::string &alias) {
|
||||||
|
if (!remote_id.empty()) {
|
||||||
|
recent_connection_aliases_[remote_id] = alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsManager::EraseRecentConnectionAlias(const std::string &remote_id) {
|
||||||
|
recent_connection_aliases_.erase(remote_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||||
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -33,8 +34,28 @@ public:
|
|||||||
// Loads the cached self-hosted identity into the owner's active connection
|
// Loads the cached self-hosted identity into the owner's active connection
|
||||||
// fields. Returns true only when a non-empty identity was restored.
|
// fields. Returns true only when a non-empty identity was restored.
|
||||||
bool LoadCachedSelfHostedIdentity();
|
bool LoadCachedSelfHostedIdentity();
|
||||||
|
// Restores the public-server identity kept in the in-memory cache fields.
|
||||||
|
// This must run when switching away from a self-hosted server so signal
|
||||||
|
// callbacks are matched against the identity used by the replacement peer.
|
||||||
|
bool ActivateCachedPublicIdentity();
|
||||||
void PersistSelfHostedIdentity(const char *client_id);
|
void PersistSelfHostedIdentity(const char *client_id);
|
||||||
|
|
||||||
|
// Password rotation is deliberately persisted in two phases. The active
|
||||||
|
// credential remains usable while the pending credential records the value
|
||||||
|
// that may already have been committed by the server. On the next launch the
|
||||||
|
// caller can try the pending credential first and safely fall back to the
|
||||||
|
// active credential when the request never reached the server.
|
||||||
|
bool StagePendingPasswordChange(const std::string &identity,
|
||||||
|
bool self_hosted,
|
||||||
|
const std::string &server_host,
|
||||||
|
int server_port,
|
||||||
|
const std::string &request_id);
|
||||||
|
std::string PendingPasswordChangeIdentity(
|
||||||
|
bool self_hosted, const std::string &server_host,
|
||||||
|
int server_port) const;
|
||||||
|
bool PromotePendingPasswordChange();
|
||||||
|
bool ClearPendingPasswordChange();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct CacheV1 {
|
struct CacheV1 {
|
||||||
char client_id_with_password[17];
|
char client_id_with_password[17];
|
||||||
@@ -63,12 +84,27 @@ private:
|
|||||||
char self_hosted_id[17];
|
char self_hosted_id[17];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CacheV3 {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t version;
|
||||||
|
CacheV2 base;
|
||||||
|
char pending_identity[17];
|
||||||
|
char pending_server_host[256];
|
||||||
|
int pending_server_port;
|
||||||
|
bool pending_self_hosted;
|
||||||
|
char pending_request_id[64];
|
||||||
|
uint32_t checksum;
|
||||||
|
};
|
||||||
|
|
||||||
int SaveLocked();
|
int SaveLocked();
|
||||||
|
bool ReadV3Locked();
|
||||||
bool ReadV2Locked();
|
bool ReadV2Locked();
|
||||||
|
void ActivateIdentity(const char *identity, bool self_hosted);
|
||||||
|
|
||||||
GuiRuntime &owner_;
|
GuiRuntime &owner_;
|
||||||
CacheV1 cache_v1_{};
|
CacheV1 cache_v1_{};
|
||||||
CacheV2 cache_v2_{};
|
CacheV2 cache_v2_{};
|
||||||
|
CacheV3 cache_v3_{};
|
||||||
mutable std::mutex cache_mutex_;
|
mutable std::mutex cache_mutex_;
|
||||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,502 @@
|
|||||||
|
#include "rendering/slint_video_presenter.h"
|
||||||
|
|
||||||
|
#include <libyuv.h>
|
||||||
|
#include <slint.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "nv12_scaler.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
#include "platform/macos/gui/metal_video_renderer.h"
|
||||||
|
#else
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
#include <GL/gl.h>
|
||||||
|
|
||||||
|
#include "platform/common/gui/opengl_video_renderer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
constexpr GLint kGlClampToEdge = 0x812F;
|
||||||
|
constexpr float kStreamWindowCornerRadius = 12.0f;
|
||||||
|
#else
|
||||||
|
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct VideoRenderSize {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool operator==(const VideoRenderSize&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
||||||
|
int area_width, int area_height) {
|
||||||
|
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
||||||
|
area_height <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const double scale =
|
||||||
|
std::min({1.0, static_cast<double>(area_width) / source_width,
|
||||||
|
static_cast<double>(area_height) / source_height});
|
||||||
|
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
||||||
|
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
||||||
|
|
||||||
|
width &= ~1;
|
||||||
|
height &= ~1;
|
||||||
|
return {std::min(width, source_width & ~1),
|
||||||
|
std::min(height, source_height & ~1)};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct SlintVideoPresenter::Impl {
|
||||||
|
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
|
||||||
|
|
||||||
|
VideoRenderer& renderer;
|
||||||
|
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
|
||||||
|
StateProvider state_provider;
|
||||||
|
DirtyCallback dirty_callback;
|
||||||
|
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||||
|
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
|
||||||
|
std::vector<uint8_t> scaled_video_frame;
|
||||||
|
std::vector<uint8_t> scaled_video_frame_scratch;
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
int attachment_attempts = 0;
|
||||||
|
bool skip_attachment_once = false;
|
||||||
|
#else
|
||||||
|
std::mutex gl_mutex;
|
||||||
|
std::vector<uint8_t> gl_conversion_frame;
|
||||||
|
std::vector<uint8_t> gl_pending_frame;
|
||||||
|
uint32_t gl_texture = 0;
|
||||||
|
VideoRenderSize gl_texture_size;
|
||||||
|
VideoRenderSize gl_image_size;
|
||||||
|
VideoRenderSize gl_pending_size;
|
||||||
|
bool gl_pending_dirty = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SurfaceState CurrentState() const {
|
||||||
|
return state_provider ? state_provider() : SurfaceState{};
|
||||||
|
}
|
||||||
|
|
||||||
|
void MarkDirty() const {
|
||||||
|
if (dirty_callback) {
|
||||||
|
dirty_callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
|
||||||
|
if (remote_id.empty() || sequence == 0 ||
|
||||||
|
displayed_frame_sequence[remote_id] == sequence) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
displayed_frame_sequence[remote_id] = sequence;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
void ConfigureOpenGlNotifier() {
|
||||||
|
if (!stream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto error = (*stream)->window().set_rendering_notifier(
|
||||||
|
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
||||||
|
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(gl_mutex);
|
||||||
|
auto* opengl_renderer =
|
||||||
|
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||||
|
if (state == slint::RenderingState::RenderingSetup) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
if (texture == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GLint previous_texture = 0;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||||
|
gl_texture = texture;
|
||||||
|
if (opengl_renderer && opengl_renderer->Setup()) {
|
||||||
|
const std::string selected_stream =
|
||||||
|
CurrentState().selected_stream;
|
||||||
|
opengl_renderer->SetSelectedStream(selected_stream);
|
||||||
|
seeded_frame_sequence.erase(selected_stream);
|
||||||
|
MarkDirty();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::BeforeRendering && stream &&
|
||||||
|
opengl_renderer && opengl_renderer->IsReady() &&
|
||||||
|
(*stream)->get_native_video_enabled()) {
|
||||||
|
const SurfaceState surface = CurrentState();
|
||||||
|
const auto window_size = (*stream)->window().size();
|
||||||
|
const float scale_factor =
|
||||||
|
std::max(1.0f, (*stream)->window().scale_factor());
|
||||||
|
const int top_inset =
|
||||||
|
!surface.fullscreen && surface.tab_count > 1
|
||||||
|
? static_cast<int>(std::lround(30.0f * scale_factor))
|
||||||
|
: 0;
|
||||||
|
const bool rounded_window = (*stream)->get_custom_titlebar() &&
|
||||||
|
!surface.fullscreen &&
|
||||||
|
!(*stream)->window().is_maximized();
|
||||||
|
const int corner_radius =
|
||||||
|
rounded_window ? static_cast<int>(std::lround(
|
||||||
|
kStreamWindowCornerRadius * scale_factor))
|
||||||
|
: 0;
|
||||||
|
opengl_renderer->RenderLatest(
|
||||||
|
surface.selected_stream, static_cast<int>(window_size.width),
|
||||||
|
static_cast<int>(window_size.height), top_inset, corner_radius);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::BeforeRendering &&
|
||||||
|
gl_texture != 0 && gl_pending_dirty &&
|
||||||
|
!gl_pending_frame.empty()) {
|
||||||
|
GLint previous_texture = 0;
|
||||||
|
GLint previous_unpack_alignment = 0;
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||||
|
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, gl_texture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||||
|
if (gl_texture_size != gl_pending_size) {
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
|
||||||
|
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
gl_pending_frame.data());
|
||||||
|
gl_texture_size = gl_pending_size;
|
||||||
|
} else {
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
|
||||||
|
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||||
|
gl_pending_frame.data());
|
||||||
|
}
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||||
|
gl_pending_dirty = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == slint::RenderingState::RenderingTeardown) {
|
||||||
|
if (opengl_renderer) {
|
||||||
|
opengl_renderer->Teardown();
|
||||||
|
}
|
||||||
|
if (gl_texture != 0) {
|
||||||
|
const GLuint texture = gl_texture;
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
}
|
||||||
|
gl_texture = 0;
|
||||||
|
gl_texture_size = {};
|
||||||
|
gl_image_size = {};
|
||||||
|
gl_pending_size = {};
|
||||||
|
gl_pending_dirty = false;
|
||||||
|
gl_pending_frame.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error.has_value()) {
|
||||||
|
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||||
|
} else {
|
||||||
|
renderer.SetSelectedStream(CurrentState().selected_stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
|
||||||
|
: impl_(std::make_unique<Impl>(renderer)) {}
|
||||||
|
|
||||||
|
SlintVideoPresenter::~SlintVideoPresenter() = default;
|
||||||
|
|
||||||
|
void SlintVideoPresenter::PrepareWindow(
|
||||||
|
slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||||
|
StateProvider state_provider, DirtyCallback dirty_callback) {
|
||||||
|
impl_->stream = &stream;
|
||||||
|
impl_->state_provider = std::move(state_provider);
|
||||||
|
impl_->dirty_callback = std::move(dirty_callback);
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
stream->set_native_video_enabled(false);
|
||||||
|
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
|
||||||
|
impl_->skip_attachment_once = true;
|
||||||
|
#else
|
||||||
|
impl_->ConfigureOpenGlNotifier();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::EnsureAttached() {
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (!impl_->stream || impl_->attachment_attempts <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (impl_->skip_attachment_once) {
|
||||||
|
impl_->skip_attachment_once = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (!metal_renderer) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!metal_renderer->IsReady()) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (metal_renderer->IsAttached()) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (void* view = (*impl_->stream)->window().appkit_view()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(true);
|
||||||
|
if (metal_renderer->Attach(view)) {
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
const SurfaceState surface = impl_->CurrentState();
|
||||||
|
impl_->displayed_frame_sequence.erase(surface.selected_stream);
|
||||||
|
impl_->seeded_frame_sequence.erase(surface.selected_stream);
|
||||||
|
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
|
||||||
|
impl_->MarkDirty();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--impl_->attachment_attempts == 0) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::NeedsRedraw() const {
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
const auto* metal_renderer =
|
||||||
|
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
return metal_renderer && metal_renderer->IsActive() &&
|
||||||
|
metal_renderer->NeedsSurfaceRedraw();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
|
||||||
|
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
|
||||||
|
if (changed && !remote_id.empty()) {
|
||||||
|
impl_->seeded_frame_sequence.erase(remote_id);
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
|
||||||
|
const Frame& frame) {
|
||||||
|
PresentResult result;
|
||||||
|
if (!impl_->stream || frame.remote_id.empty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
|
||||||
|
frame.height > 0 && frame.sequence > 0 &&
|
||||||
|
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
|
||||||
|
const auto submit_result = impl_->renderer.SubmitCachedNv12(
|
||||||
|
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
|
||||||
|
frame.height);
|
||||||
|
if (submit_result == VideoRenderer::SubmitResult::submitted ||
|
||||||
|
submit_result == VideoRenderer::SubmitResult::dropped) {
|
||||||
|
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (metal_renderer && metal_renderer->IsActive()) {
|
||||||
|
const SurfaceState surface = impl_->CurrentState();
|
||||||
|
const double top_inset =
|
||||||
|
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
|
||||||
|
const auto outcome = metal_renderer->RenderLatest(
|
||||||
|
frame.remote_id, top_inset, !surface.fullscreen);
|
||||||
|
if (outcome.result == VideoRenderer::RenderResult::rendered) {
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = outcome.width;
|
||||||
|
result.height = outcome.height;
|
||||||
|
result.new_frame =
|
||||||
|
impl_->MarkPresented(frame.remote_id, outcome.sequence);
|
||||||
|
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
|
||||||
|
(*impl_->stream)->set_has_frame(false);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
auto* opengl_renderer =
|
||||||
|
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
|
||||||
|
if (opengl_renderer && opengl_renderer->IsReady()) {
|
||||||
|
opengl_renderer->SetSelectedStream(frame.remote_id);
|
||||||
|
if (!(*impl_->stream)->get_native_video_enabled()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool has_frame =
|
||||||
|
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
|
||||||
|
(*impl_->stream)->set_has_frame(has_frame);
|
||||||
|
if (has_frame) {
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
}
|
||||||
|
(*impl_->stream)->window().request_redraw();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if ((*impl_->stream)->get_native_video_enabled()) {
|
||||||
|
(*impl_->stream)->set_native_video_enabled(false);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const size_t nv12_size =
|
||||||
|
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
|
||||||
|
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
|
||||||
|
frame.nv12->size() < nv12_size) {
|
||||||
|
(*impl_->stream)->set_has_frame(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto window_size = (*impl_->stream)->window().size();
|
||||||
|
const VideoRenderSize render_size = FitVideoToRenderArea(
|
||||||
|
frame.width, frame.height, static_cast<int>(window_size.width),
|
||||||
|
static_cast<int>(window_size.height));
|
||||||
|
const uint8_t* y_plane = frame.nv12->data();
|
||||||
|
const uint8_t* uv_plane =
|
||||||
|
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
|
||||||
|
int output_width = frame.width;
|
||||||
|
int output_height = frame.height;
|
||||||
|
|
||||||
|
if (render_size.width > 0 && render_size.height > 0 &&
|
||||||
|
(render_size.width != frame.width ||
|
||||||
|
render_size.height != frame.height)) {
|
||||||
|
const size_t scaled_nv12_size =
|
||||||
|
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
||||||
|
impl_->scaled_video_frame.resize(scaled_nv12_size);
|
||||||
|
uint8_t* scaled_y = impl_->scaled_video_frame.data();
|
||||||
|
uint8_t* scaled_uv =
|
||||||
|
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
|
||||||
|
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
|
||||||
|
frame.width, frame.height, scaled_y, render_size.width,
|
||||||
|
scaled_uv, render_size.width, render_size.width,
|
||||||
|
render_size.height, libyuv::kFilterBox,
|
||||||
|
&impl_->scaled_video_frame_scratch) == 0) {
|
||||||
|
y_plane = scaled_y;
|
||||||
|
uv_plane = scaled_uv;
|
||||||
|
output_width = render_size.width;
|
||||||
|
output_height = render_size.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(__APPLE__)
|
||||||
|
uint32_t texture_id = 0;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(impl_->gl_mutex);
|
||||||
|
texture_id = impl_->gl_texture;
|
||||||
|
}
|
||||||
|
if (texture_id != 0) {
|
||||||
|
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
|
||||||
|
output_height * 4);
|
||||||
|
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
|
||||||
|
impl_->gl_conversion_frame.data(), output_width * 4,
|
||||||
|
output_width, output_height) != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock(impl_->gl_mutex);
|
||||||
|
if (impl_->gl_texture == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
texture_id = impl_->gl_texture;
|
||||||
|
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
|
||||||
|
impl_->gl_pending_size = {output_width, output_height};
|
||||||
|
impl_->gl_pending_dirty = true;
|
||||||
|
}
|
||||||
|
const VideoRenderSize output_size{output_width, output_height};
|
||||||
|
if (impl_->gl_image_size != output_size) {
|
||||||
|
(*impl_->stream)
|
||||||
|
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
||||||
|
texture_id,
|
||||||
|
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
||||||
|
static_cast<uint32_t>(output_height)}));
|
||||||
|
impl_->gl_image_size = output_size;
|
||||||
|
}
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
(*impl_->stream)->window().request_redraw();
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||||
|
output_height);
|
||||||
|
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
|
||||||
|
reinterpret_cast<uint8_t*>(pixels.begin()),
|
||||||
|
output_width * 3, output_width, output_height) != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
|
||||||
|
(*impl_->stream)->set_has_frame(true);
|
||||||
|
(*impl_->stream)->set_receiving_text("");
|
||||||
|
result.width = frame.width;
|
||||||
|
result.height = frame.height;
|
||||||
|
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
|
||||||
|
impl_->displayed_frame_sequence.erase(remote_id);
|
||||||
|
impl_->seeded_frame_sequence.erase(remote_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SlintVideoPresenter::Detach() {
|
||||||
|
impl_->renderer.SetSelectedStream({});
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (auto* metal_renderer =
|
||||||
|
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
|
||||||
|
metal_renderer->Detach();
|
||||||
|
}
|
||||||
|
impl_->attachment_attempts = 0;
|
||||||
|
impl_->skip_attachment_once = false;
|
||||||
|
#endif
|
||||||
|
impl_->stream = nullptr;
|
||||||
|
impl_->state_provider = {};
|
||||||
|
impl_->dirty_callback = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
|
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "crossdesk_ui.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class VideoRenderer;
|
||||||
|
|
||||||
|
// Owns the bridge between decoded NV12 frames, the selected native graphics
|
||||||
|
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
|
||||||
|
// details are intentionally contained in the implementation file.
|
||||||
|
class SlintVideoPresenter {
|
||||||
|
public:
|
||||||
|
struct SurfaceState {
|
||||||
|
std::string selected_stream;
|
||||||
|
bool fullscreen = false;
|
||||||
|
size_t tab_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Frame {
|
||||||
|
std::string remote_id;
|
||||||
|
std::shared_ptr<std::vector<unsigned char>> nv12;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
uint64_t sequence = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PresentResult {
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
bool new_frame = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
using StateProvider = std::function<SurfaceState()>;
|
||||||
|
using DirtyCallback = std::function<void()>;
|
||||||
|
|
||||||
|
explicit SlintVideoPresenter(VideoRenderer& renderer);
|
||||||
|
~SlintVideoPresenter();
|
||||||
|
|
||||||
|
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
|
||||||
|
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
|
||||||
|
|
||||||
|
// Called before the window is shown. OpenGL installs Slint's rendering
|
||||||
|
// notifier here; macOS deliberately defers native view attachment until a
|
||||||
|
// later UI pass, after AppKit has committed the ordinary window style.
|
||||||
|
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||||
|
StateProvider state_provider,
|
||||||
|
DirtyCallback dirty_callback);
|
||||||
|
|
||||||
|
// Returns true only when a deferred native surface became attached during
|
||||||
|
// this call, allowing the caller to seed it from a cached frame once.
|
||||||
|
bool EnsureAttached();
|
||||||
|
bool NeedsRedraw() const;
|
||||||
|
|
||||||
|
bool SelectStream(std::string remote_id);
|
||||||
|
PresentResult Present(const Frame& frame);
|
||||||
|
void ForgetStream(const std::string& remote_id);
|
||||||
|
void Detach();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||||
@@ -8,8 +8,10 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <display_stream_id.h>
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
@@ -121,6 +123,10 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
|
|||||||
void GuiRuntime::HandleServerControllerDisconnected(
|
void GuiRuntime::HandleServerControllerDisconnected(
|
||||||
const std::string& remote_id, const char* reason) {
|
const std::string& remote_id, const char* reason) {
|
||||||
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
||||||
|
{
|
||||||
|
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||||
|
last_remote_pointer_input_time_.erase(remote_id);
|
||||||
|
}
|
||||||
|
|
||||||
bool has_connected_controller = false;
|
bool has_connected_controller = false;
|
||||||
bool has_web_controller = false;
|
bool has_web_controller = false;
|
||||||
@@ -222,6 +228,21 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
frame_snapshot = props->front_frame_;
|
frame_snapshot = props->front_frame_;
|
||||||
video_width = props->video_width_;
|
video_width = props->video_width_;
|
||||||
video_height = props->video_height_;
|
video_height = props->video_height_;
|
||||||
|
if ((!frame_snapshot || frame_snapshot->empty()) &&
|
||||||
|
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
||||||
|
frame_snapshot = props->thumbnail_frame_;
|
||||||
|
video_width = props->thumbnail_width_;
|
||||||
|
video_height = props->thumbnail_height_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto* native_renderer = video_renderer_.get();
|
||||||
|
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
||||||
|
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||||
|
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
||||||
|
native_snapshot.get(), &video_width,
|
||||||
|
&video_height)) {
|
||||||
|
frame_snapshot = std::move(native_snapshot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||||
@@ -246,6 +267,11 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (native_renderer) {
|
||||||
|
native_renderer->DiscardStream(props->remote_id_);
|
||||||
|
video_frame_dirty_.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
if (props->peer_) {
|
if (props->peer_) {
|
||||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||||
@@ -306,12 +332,21 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
props->front_frame_.reset();
|
props->front_frame_.reset();
|
||||||
props->back_frame_.reset();
|
props->back_frame_.reset();
|
||||||
|
props->thumbnail_frame_.reset();
|
||||||
|
props->thumbnail_width_ = 0;
|
||||||
|
props->thumbnail_height_ = 0;
|
||||||
|
props->background_snapshot_time_ = {};
|
||||||
props->video_width_ = 0;
|
props->video_width_ = 0;
|
||||||
props->video_height_ = 0;
|
props->video_height_ = 0;
|
||||||
props->video_size_ = 0;
|
props->video_size_ = 0;
|
||||||
props->render_rect_dirty_ = true;
|
props->render_rect_dirty_ = true;
|
||||||
props->stream_cleanup_pending_ = false;
|
props->stream_cleanup_pending_ = false;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
|
||||||
|
props->remote_cursor_state_ = {};
|
||||||
|
props->remote_cursor_state_received_ = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||||
@@ -381,8 +416,10 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& display_info : devices_.display_info_list()) {
|
const auto& displays = devices_.display_info_list();
|
||||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
for (size_t index = 0; index < displays.size(); ++index) {
|
||||||
|
const std::string stream_id = MakeDisplayStreamId(index);
|
||||||
|
AddVideoStream(props->peer_, stream_id.c_str());
|
||||||
}
|
}
|
||||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef _CURSOR_POSITION_H_
|
||||||
|
#define _CURSOR_POSITION_H_
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include "display_info.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
inline void ResetCursorPosition(CursorState* state) {
|
||||||
|
if (!state) return;
|
||||||
|
state->position_update = true;
|
||||||
|
state->position_valid = false;
|
||||||
|
state->x = 0.5f;
|
||||||
|
state->y = 0.5f;
|
||||||
|
state->visual_offset_x = 0.0f;
|
||||||
|
state->visual_offset_y = 0.0f;
|
||||||
|
state->display_id = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool NormalizeCursorPosition(
|
||||||
|
double screen_x, double screen_y,
|
||||||
|
const std::vector<DisplayInfo>& displays, int preferred_display,
|
||||||
|
CursorState* state) {
|
||||||
|
if (!state) return false;
|
||||||
|
ResetCursorPosition(state);
|
||||||
|
|
||||||
|
auto contains = [&](int index) {
|
||||||
|
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
|
||||||
|
const auto& display = displays[index];
|
||||||
|
return display.width > 0 && display.height > 0 &&
|
||||||
|
screen_x >= display.left && screen_x < display.right &&
|
||||||
|
screen_y >= display.top && screen_y < display.bottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
int display_id = contains(preferred_display) ? preferred_display : -1;
|
||||||
|
if (display_id < 0) {
|
||||||
|
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
|
||||||
|
if (contains(index)) {
|
||||||
|
display_id = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (display_id < 0) return false;
|
||||||
|
|
||||||
|
const auto& display = displays[display_id];
|
||||||
|
// Screen coordinates describe a continuous rectangle with an exclusive
|
||||||
|
// right/bottom edge. Use its full extent so feedback is the exact inverse of
|
||||||
|
// normalized input instead of accumulating a one-pixel edge convention.
|
||||||
|
const double horizontal_extent = std::max(display.width, 1);
|
||||||
|
const double vertical_extent = std::max(display.height, 1);
|
||||||
|
state->position_valid = true;
|
||||||
|
state->x = static_cast<float>(std::clamp(
|
||||||
|
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
|
||||||
|
state->y = static_cast<float>(std::clamp(
|
||||||
|
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
|
||||||
|
state->display_id = display_id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#include "runtime/cursor_state_provider.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "runtime/cursor_position.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
|
||||||
|
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
|
||||||
|
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
|
||||||
|
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
|
||||||
|
if (IsSystemCursor(cursor, IDC_APPSTARTING))
|
||||||
|
return RemoteCursorShape::progress;
|
||||||
|
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
|
||||||
|
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
|
||||||
|
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
|
||||||
|
if (IsSystemCursor(cursor, IDC_NO))
|
||||||
|
return RemoteCursorShape::not_allowed;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENESW))
|
||||||
|
return RemoteCursorShape::nesw_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_SIZENWSE))
|
||||||
|
return RemoteCursorShape::nwse_resize;
|
||||||
|
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
|
||||||
|
return RemoteCursorShape::default_cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct CursorStateProvider::Impl {};
|
||||||
|
|
||||||
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
|
|
||||||
|
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||||
|
int preferred_display, CursorState* state) {
|
||||||
|
if (!state) return false;
|
||||||
|
|
||||||
|
CURSORINFO info{};
|
||||||
|
info.cbSize = sizeof(info);
|
||||||
|
if (!GetCursorInfo(&info)) return false;
|
||||||
|
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = (info.flags & CURSOR_SHOWING) != 0;
|
||||||
|
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||||
|
: RemoteCursorShape::none;
|
||||||
|
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
|
||||||
|
preferred_display, state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#elif defined(__linux__) && !defined(__APPLE__)
|
||||||
|
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/extensions/Xfixes.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "linux_cursor_shape.h"
|
||||||
|
#include "platform.h"
|
||||||
|
#include "shared_cursor_state.h"
|
||||||
|
#include "runtime/cursor_position.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
|
||||||
|
const size_t pixel_count =
|
||||||
|
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
|
||||||
|
for (size_t index = 0; index < pixel_count; ++index) {
|
||||||
|
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
struct CursorStateProvider::Impl {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
|
||||||
|
~Impl() {
|
||||||
|
if (display) XCloseDisplay(display);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||||
|
CursorStateProvider::~CursorStateProvider() = default;
|
||||||
|
|
||||||
|
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||||
|
int preferred_display, CursorState* state) {
|
||||||
|
if (!state || !impl_) return false;
|
||||||
|
ResetCursorPosition(state);
|
||||||
|
|
||||||
|
if (IsWaylandSession()) {
|
||||||
|
SharedCursorState shared{};
|
||||||
|
if (GetSharedCursorState(&shared)) {
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = shared.visible;
|
||||||
|
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!impl_->display) return false;
|
||||||
|
|
||||||
|
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
|
||||||
|
if (!image) return false;
|
||||||
|
|
||||||
|
const std::string name = image->name ? image->name : "";
|
||||||
|
|
||||||
|
state->seq = 0;
|
||||||
|
state->visible = CursorHasVisiblePixel(*image);
|
||||||
|
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
|
||||||
|
: RemoteCursorShape::none;
|
||||||
|
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
|
||||||
|
state);
|
||||||
|
XFree(image);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef _CURSOR_STATE_PROVIDER_H_
|
||||||
|
#define _CURSOR_STATE_PROVIDER_H_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
|
#include "display_info.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
|
||||||
|
// Samples the cursor that is currently displayed by the controlled desktop
|
||||||
|
// and converts platform-specific cursor handles to protocol cursor shapes.
|
||||||
|
class CursorStateProvider {
|
||||||
|
public:
|
||||||
|
CursorStateProvider();
|
||||||
|
~CursorStateProvider();
|
||||||
|
|
||||||
|
CursorStateProvider(const CursorStateProvider&) = delete;
|
||||||
|
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
|
||||||
|
|
||||||
|
bool Sample(const std::vector<DisplayInfo>& displays,
|
||||||
|
int preferred_display, CursorState* state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -10,7 +10,9 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <display_stream_id.h>
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
|
#include "platform/video_renderer.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -57,9 +59,44 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
signal_server_ip = config_center_->GetDefaultServerHost();
|
signal_server_ip = config_center_->GetDefaultServerHost();
|
||||||
signal_server_port = config_center_->GetDefaultSignalServerPort();
|
signal_server_port = config_center_->GetDefaultSignalServerPort();
|
||||||
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
|
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
|
||||||
|
settings_.ActivateCachedPublicIdentity();
|
||||||
params_.user_id = client_id_with_password_;
|
params_.user_id = client_id_with_password_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool self_hosted = config_center_->IsSelfHosted();
|
||||||
|
const std::string pending_identity =
|
||||||
|
settings_.PendingPasswordChangeIdentity(
|
||||||
|
self_hosted, signal_server_ip, signal_server_port);
|
||||||
|
bool try_pending_identity = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||||
|
if (pending_identity.empty()) {
|
||||||
|
credential_recovery_in_progress_ = false;
|
||||||
|
credential_recovery_attempt_pending_ = false;
|
||||||
|
credential_recovery_retry_active_ = false;
|
||||||
|
credential_recovery_promote_pending_ = false;
|
||||||
|
credential_recovery_clear_pending_ = false;
|
||||||
|
} else if (!credential_recovery_in_progress_) {
|
||||||
|
credential_recovery_in_progress_ = true;
|
||||||
|
credential_recovery_attempt_pending_ = true;
|
||||||
|
LOG_INFO("Recovering an interrupted password change for [{}]",
|
||||||
|
client_id_);
|
||||||
|
}
|
||||||
|
try_pending_identity = credential_recovery_in_progress_ &&
|
||||||
|
credential_recovery_attempt_pending_ &&
|
||||||
|
!pending_identity.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *active_identity =
|
||||||
|
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
|
||||||
|
const std::string login_identity =
|
||||||
|
try_pending_identity ? pending_identity : std::string(active_identity);
|
||||||
|
std::memset(connection_login_identity_, 0,
|
||||||
|
sizeof(connection_login_identity_));
|
||||||
|
std::strncpy(connection_login_identity_, login_identity.c_str(),
|
||||||
|
sizeof(connection_login_identity_) - 1);
|
||||||
|
params_.user_id = connection_login_identity_;
|
||||||
|
|
||||||
// self hosted server config
|
// self hosted server config
|
||||||
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
||||||
sizeof(signal_server_ip_self_) - 1);
|
sizeof(signal_server_ip_self_) - 1);
|
||||||
@@ -94,25 +131,35 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
sizeof(params_.turn_server_ip) - 1);
|
sizeof(params_.turn_server_ip) - 1);
|
||||||
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
|
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
|
||||||
params_.turn_server_port = coturn_server_port;
|
params_.turn_server_port = coturn_server_port;
|
||||||
strncpy((char *)params_.turn_server_username, "crossdesk",
|
// TURN credentials are issued by the signaling server after login. Keep the
|
||||||
sizeof(params_.turn_server_username) - 1);
|
// initial values empty so a reusable static password is never embedded in
|
||||||
params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0';
|
// the client binary.
|
||||||
strncpy((char *)params_.turn_server_password, "crossdeskpw",
|
params_.turn_server_username[0] = '\0';
|
||||||
sizeof(params_.turn_server_password) - 1);
|
params_.turn_server_password[0] = '\0';
|
||||||
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
|
|
||||||
|
|
||||||
strncpy(params_.log_path, dll_log_path_.c_str(),
|
strncpy(params_.log_path, dll_log_path_.c_str(),
|
||||||
sizeof(params_.log_path) - 1);
|
sizeof(params_.log_path) - 1);
|
||||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||||
|
// The Slint desktop renderer currently consumes packed CPU frames. Native
|
||||||
|
// renderers can opt into platform-native frame output independently.
|
||||||
|
params_.native_video_output = false;
|
||||||
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||||
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
|
||||||
? true
|
? true
|
||||||
: false;
|
: false;
|
||||||
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
|
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
|
||||||
params_.enable_srtp = config_center_->IsEnableSrtp();
|
params_.enable_srtp = config_center_->IsEnableSrtp();
|
||||||
|
params_.video_content_type = VideoContentType::ScreenContent;
|
||||||
params_.video_quality =
|
params_.video_quality =
|
||||||
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
||||||
|
params_.video_frame_rate =
|
||||||
|
config_center_->GetVideoFrameRate() ==
|
||||||
|
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||||
|
? 30
|
||||||
|
: 60;
|
||||||
|
params_.video_degradation_preference =
|
||||||
|
VideoDegradationPreference::MaintainFrameRate;
|
||||||
params_.on_receive_video_buffer = nullptr;
|
params_.on_receive_video_buffer = nullptr;
|
||||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||||
@@ -126,6 +173,11 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
|
|
||||||
params_.user_data = &peer_events_;
|
params_.user_data = &peer_events_;
|
||||||
|
|
||||||
|
// The previous peer may have left a terminal status behind. Reset it before
|
||||||
|
// Init() starts emitting callbacks for the newly selected server.
|
||||||
|
signal_connected_ = false;
|
||||||
|
signal_status_ = SignalStatus::SignalConnecting;
|
||||||
|
|
||||||
peer_ = CreatePeer(¶ms_);
|
peer_ = CreatePeer(¶ms_);
|
||||||
if (peer_) {
|
if (peer_) {
|
||||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||||
@@ -136,8 +188,10 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (0 == devices_.InitializeScreenCapturer()) {
|
if (0 == devices_.InitializeScreenCapturer()) {
|
||||||
for (const auto &display_info : devices_.display_info_list()) {
|
const auto &displays = devices_.display_info_list();
|
||||||
AddVideoStream(peer_, display_info.name.c_str());
|
for (size_t index = 0; index < displays.size(); ++index) {
|
||||||
|
const std::string stream_id = MakeDisplayStreamId(index);
|
||||||
|
AddVideoStream(peer_, stream_id.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
AddAudioStream(peer_, audio_label_.c_str());
|
AddAudioStream(peer_, audio_label_.c_str());
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||||
#define CROSSDESK_GUI_RUNTIME_H_
|
#define CROSSDESK_GUI_RUNTIME_H_
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
class VideoRenderer;
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -81,6 +84,8 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
SettingsManager settings_;
|
SettingsManager settings_;
|
||||||
KeyboardController keyboard_;
|
KeyboardController keyboard_;
|
||||||
PeerEventHandler peer_events_;
|
PeerEventHandler peer_events_;
|
||||||
|
std::atomic<bool> video_frame_dirty_{false};
|
||||||
|
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class ClipboardController;
|
friend class ClipboardController;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "runtime/peer_event_handler.h"
|
#include "runtime/peer_event_handler.h"
|
||||||
|
|
||||||
|
#include <remote_action.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -97,6 +99,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
}
|
}
|
||||||
receiver.SetOnSendAck([runtime,
|
receiver.SetOnSendAck([runtime,
|
||||||
remote_user_id](const FileTransferAck &ack) -> int {
|
remote_user_id](const FileTransferAck &ack) -> int {
|
||||||
|
const auto encoded_ack = EncodeFileTransferAck(ack);
|
||||||
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
||||||
if (is_server_sending) {
|
if (is_server_sending) {
|
||||||
auto props =
|
auto props =
|
||||||
@@ -104,14 +107,14 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
if (props) {
|
if (props) {
|
||||||
PeerPtr *peer = props->peer_;
|
PeerPtr *peer = props->peer_;
|
||||||
return SendReliableDataFrame(
|
return SendReliableDataFrame(
|
||||||
peer, reinterpret_cast<const char *>(&ack),
|
peer, encoded_ack.data(), encoded_ack.size(),
|
||||||
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
|
runtime->file_feedback_label_.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendReliableDataFrame(
|
return SendReliableDataFrame(
|
||||||
runtime->peer_, reinterpret_cast<const char *>(&ack),
|
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
|
||||||
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
|
runtime->file_feedback_label_.c_str());
|
||||||
});
|
});
|
||||||
|
|
||||||
receiver.OnData(data, size);
|
receiver.OnData(data, size);
|
||||||
@@ -164,6 +167,48 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remote_action.type == ControlType::cursor_state) {
|
||||||
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||||
|
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||||
|
if (props_it != runtime->remote_sessions_.end()) {
|
||||||
|
props = props_it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!props) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||||
|
const uint32_t previous_seq = props->remote_cursor_state_.seq;
|
||||||
|
const bool is_newer =
|
||||||
|
!props->remote_cursor_state_received_ ||
|
||||||
|
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
|
||||||
|
if (is_newer) {
|
||||||
|
const bool changed =
|
||||||
|
!props->remote_cursor_state_received_ ||
|
||||||
|
props->remote_cursor_state_.visible != remote_action.cs.visible ||
|
||||||
|
props->remote_cursor_state_.shape != remote_action.cs.shape;
|
||||||
|
CursorState merged = remote_action.cs;
|
||||||
|
if (!remote_action.cs.position_update &&
|
||||||
|
props->remote_cursor_state_received_) {
|
||||||
|
merged.position_valid = props->remote_cursor_state_.position_valid;
|
||||||
|
merged.x = props->remote_cursor_state_.x;
|
||||||
|
merged.y = props->remote_cursor_state_.y;
|
||||||
|
merged.display_id = props->remote_cursor_state_.display_id;
|
||||||
|
}
|
||||||
|
props->remote_cursor_state_ = merged;
|
||||||
|
props->remote_cursor_state_received_ = true;
|
||||||
|
if (changed) {
|
||||||
|
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
|
||||||
|
remote_action.cs.seq, remote_action.cs.visible,
|
||||||
|
static_cast<int>(remote_action.cs.shape));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (remote_action.type == ControlType::host_infomation) {
|
if (remote_action.type == ControlType::host_infomation) {
|
||||||
bool is_client_mode = false;
|
bool is_client_mode = false;
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
@@ -204,6 +249,12 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// remote
|
// remote
|
||||||
|
if (runtime->is_server_mode_ &&
|
||||||
|
remote_action.type == ControlType::mouse) {
|
||||||
|
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
|
||||||
|
runtime->last_remote_pointer_input_time_[remote_id] =
|
||||||
|
std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
if (runtime->local_service_status_received_ &&
|
if (runtime->local_service_status_received_ &&
|
||||||
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
||||||