mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16348444f0 |
@@ -1,62 +0,0 @@
|
||||
name: Sign Windows artifact
|
||||
description: Submit one raw EXE or ZIP to SignPath and download the signed result.
|
||||
inputs:
|
||||
api-token:
|
||||
description: SignPath submitter token.
|
||||
required: true
|
||||
organization-id:
|
||||
description: SignPath organization ID.
|
||||
required: true
|
||||
signing-policy:
|
||||
description: test-signing or release-signing.
|
||||
required: true
|
||||
artifact-configuration:
|
||||
description: Artifact configuration slug in the crossdesk project.
|
||||
required: true
|
||||
artifact:
|
||||
description: Absolute path to one EXE or ZIP file.
|
||||
required: true
|
||||
output-directory:
|
||||
description: Absolute directory for the downloaded signed file.
|
||||
required: true
|
||||
version:
|
||||
description: CrossDesk ProductVersion and FileVersion.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Upload unsigned artifact
|
||||
id: unsigned
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ inputs.artifact }}
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Submit signing request and wait for approval
|
||||
id: sign
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ inputs.api-token }}
|
||||
organization-id: ${{ inputs.organization-id }}
|
||||
project-slug: crossdesk
|
||||
signing-policy-slug: ${{ inputs.signing-policy }}
|
||||
artifact-configuration-slug: ${{ inputs.artifact-configuration }}
|
||||
github-artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: ${{ inputs.output-directory }}
|
||||
skip-decompress: true
|
||||
parameters: |
|
||||
version: ${{ toJSON(inputs.version) }}
|
||||
|
||||
- name: Record signing request
|
||||
if: always() && steps.sign.outputs.signing-request-web-url != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_REQUEST_URL: ${{ steps.sign.outputs.signing-request-web-url }}
|
||||
SIGNING_CONFIGURATION: ${{ inputs.artifact-configuration }}
|
||||
SIGNING_POLICY: ${{ inputs.signing-policy }}
|
||||
run: |
|
||||
"- $env:SIGNING_CONFIGURATION ($env:SIGNING_POLICY): $env:SIGNING_REQUEST_URL" >> $env:GITHUB_STEP_SUMMARY
|
||||
+54
-169
@@ -14,17 +14,7 @@ on:
|
||||
required: false
|
||||
default: "0"
|
||||
sign_windows_test:
|
||||
description: "Test-sign Windows binaries and installer (Actions artifacts only)."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
sign_windows_release:
|
||||
description: "Test release-signing with the production certificate (Actions artifacts only)."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
windows_only:
|
||||
description: "Build only Windows, for signing tests."
|
||||
description: "Submit the Windows installer to SignPath using test-signing."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -38,7 +28,6 @@ env:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
@@ -187,7 +176,6 @@ jobs:
|
||||
|
||||
# macOS
|
||||
build-macos:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build on macOS
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
@@ -320,12 +308,9 @@ jobs:
|
||||
|
||||
# iOS
|
||||
build-ios:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build on iOS (arm64)
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 120
|
||||
outputs:
|
||||
version: ${{ steps.ios_version.outputs.version }}
|
||||
env:
|
||||
IOS_DEPLOYMENT_TARGET: "16.0"
|
||||
EXPECTED_IOS_SDK: "26.5"
|
||||
@@ -384,26 +369,6 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Read independent iOS version
|
||||
id: ios_version
|
||||
shell: bash
|
||||
run: |
|
||||
IOS_VERSION_BASE="$(xcodebuild \
|
||||
-project apps/ios/CrossDeskMobile.xcodeproj \
|
||||
-scheme CrossDeskMobile \
|
||||
-configuration Release \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-showBuildSettings CODE_SIGNING_ALLOWED=NO | \
|
||||
awk '$1 == "MARKETING_VERSION" && $2 == "=" { print $3 }')"
|
||||
if [[ ! "${IOS_VERSION_BASE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Invalid iOS marketing version: ${IOS_VERSION_BASE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION_NUM="v${IOS_VERSION_BASE}-${VERSION_NUM##*-}"
|
||||
echo "IOS_VERSION_BASE=${IOS_VERSION_BASE}" >> "${GITHUB_ENV}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> "${GITHUB_ENV}"
|
||||
echo "version=${VERSION_NUM}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Initialize submodules
|
||||
run: git submodule update --init --recursive
|
||||
|
||||
@@ -474,11 +439,6 @@ jobs:
|
||||
fi
|
||||
|
||||
xcrun lipo "${EXECUTABLE_PATH}" -verify_arch arm64
|
||||
APP_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "${INFO_PLIST}")
|
||||
if [[ "${APP_VERSION}" != "${IOS_VERSION_BASE}" ]]; then
|
||||
echo "Unexpected iOS app version: expected ${IOS_VERSION_BASE}, got ${APP_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
@@ -517,37 +477,9 @@ jobs:
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 240
|
||||
outputs:
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
env:
|
||||
XMAKE_GLOBALDIR: D:\xmake_global
|
||||
steps:
|
||||
- name: Select Windows signing policy
|
||||
id: signing
|
||||
shell: pwsh
|
||||
env:
|
||||
TEST_SIGNING: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows_test }}
|
||||
RELEASE_SIGNING: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows_release }}
|
||||
PUBLISH_RELEASE: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
run: |
|
||||
if ($env:TEST_SIGNING -eq 'true' -and $env:RELEASE_SIGNING -eq 'true') {
|
||||
throw 'Select only one of sign_windows_test and sign_windows_release.'
|
||||
}
|
||||
$policy = ''
|
||||
if ($env:PUBLISH_RELEASE -eq 'true' -or $env:RELEASE_SIGNING -eq 'true') {
|
||||
$policy = 'release-signing'
|
||||
} elseif ($env:TEST_SIGNING -eq 'true') {
|
||||
$policy = 'test-signing'
|
||||
}
|
||||
if ($policy -and (!$env:SIGNPATH_API_TOKEN -or !$env:SIGNPATH_ORGANIZATION_ID)) {
|
||||
throw 'Signing requires SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID.'
|
||||
}
|
||||
"policy=$policy" >> $env:GITHUB_OUTPUT
|
||||
"Windows signing policy: $(if ($policy) { $policy } else { 'unsigned development build' })" >> $env:GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Extract version number
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -652,99 +584,67 @@ jobs:
|
||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||
copy "${{ github.workspace }}\apps\desktop\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||
|
||||
- name: Test Windows signing helpers
|
||||
shell: pwsh
|
||||
run: ./apps/desktop/tests/windows_signing_test.ps1
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Prepare installer binaries for signing
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation PrepareBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/unsigned/installer-binaries.zip
|
||||
|
||||
- name: Sign installer binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: windows-binaries
|
||||
artifact: ${{ github.workspace }}/signpath/unsigned/installer-binaries.zip
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/installer-binaries
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
|
||||
- name: Verify and restore signed installer binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation RestoreBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/signed/installer-binaries/installer-binaries.zip -SigningPolicy $env:SIGNING_POLICY
|
||||
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "${{ github.workspace }}\apps\desktop\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Sign Windows installer
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
- 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 }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: initial
|
||||
artifact: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/installer
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
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 signed Windows installer
|
||||
if: steps.signing.outputs.policy != ''
|
||||
- name: Verify and use test-signed Windows installer
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
$installerName = "crossdesk-win-x64-$env:VERSION_NUM.exe"
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation VerifyInstaller -Artifact "./signpath/signed/installer/$installerName" -SigningPolicy $env:SIGNING_POLICY
|
||||
Copy-Item "./signpath/signed/installer/$installerName" ./apps/desktop/scripts/windows/ -Force
|
||||
$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
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Prepare portable binaries for signing
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation PrepareBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/unsigned/portable-binaries.zip
|
||||
|
||||
- name: Sign portable binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: windows-binaries
|
||||
artifact: ${{ github.workspace }}/signpath/unsigned/portable-binaries.zip
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/portable-binaries
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
|
||||
- name: Verify and restore signed portable binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation RestoreBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/signed/portable-binaries/portable-binaries.zip -SigningPolicy $env:SIGNING_POLICY
|
||||
|
||||
- name: Package Portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -792,7 +692,7 @@ jobs:
|
||||
|
||||
release:
|
||||
name: Publish Release
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && needs.build-windows-x64.outputs.signing-policy == 'release-signing'
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-linux, build-macos, build-ios, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -849,25 +749,6 @@ jobs:
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare release notes
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
VERSION_BASE: ${{ steps.version.outputs.VERSION_BASE }}
|
||||
run: |
|
||||
if [[ -f "docs/releases/${RELEASE_TAG}.md" ]]; then
|
||||
cp "docs/releases/${RELEASE_TAG}.md" release-notes.md
|
||||
elif [[ -f "docs/releases/v${VERSION_BASE}.md" ]]; then
|
||||
cp "docs/releases/v${VERSION_BASE}.md" release-notes.md
|
||||
else
|
||||
cat > release-notes.md << 'EOF'
|
||||
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/).
|
||||
EOF
|
||||
fi
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
mkdir -p release
|
||||
@@ -875,7 +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-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-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
|
||||
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-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
|
||||
|
||||
@@ -886,13 +767,17 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
name: Release ${{ steps.version.outputs.VERSION_WITH_V }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: release/*
|
||||
generate_release_notes: false
|
||||
body_path: release-notes.md
|
||||
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: Create or update 'latest' tag
|
||||
run: |
|
||||
@@ -910,7 +795,12 @@ jobs:
|
||||
prerelease: false
|
||||
files: release/*
|
||||
generate_release_notes: false
|
||||
body_path: release-notes.md
|
||||
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
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
@@ -961,11 +851,6 @@ jobs:
|
||||
}
|
||||
}
|
||||
EOF
|
||||
jq --arg release_name "Release ${{ steps.version.outputs.VERSION_WITH_V }}" \
|
||||
--rawfile release_notes release-notes.md \
|
||||
'.releaseName = $release_name | .releaseNotes = $release_notes' \
|
||||
version.json > version-with-notes.json
|
||||
mv version-with-notes.json version.json
|
||||
cat version.json
|
||||
|
||||
- name: Upload version.json to server
|
||||
|
||||
@@ -92,9 +92,8 @@ jobs:
|
||||
|
||||
- name: Generate or update version.json
|
||||
run: |
|
||||
# Preserve download URLs only for this release, never an older version.
|
||||
if [ -f version.json ] && jq -e --arg tag "${{ steps.version.outputs.TAG_NAME }}" \
|
||||
'.tagName == $tag and (.downloads | type == "object")' version.json > /dev/null 2>&1; then
|
||||
# If version.json exists, try to preserve downloads section
|
||||
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
|
||||
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
|
||||
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
||||
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
||||
|
||||
@@ -1,211 +1,429 @@
|
||||
<div align="center">
|
||||
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk 标志" />
|
||||
<h1>CrossDesk</h1>
|
||||
<p>轻量、跨平台的远程桌面,让电脑、浏览器和 iPhone / iPad 连接到同一张桌面。</p>
|
||||
<p><a href="https://www.crossdesk.cn/">官方网站</a> · <a href="https://github.com/kunkundi/crossdesk/releases">下载客户端</a> · <a href="https://web.crossdesk.cn/">打开 Web 客户端</a> · <a href="README_EN.md">English</a></p>
|
||||
</div>
|
||||
# CrossDesk
|
||||
|
||||
<div align="center">
|
||||
<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>
|
||||
|
||||
[](#download)
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](https://www.crossdesk.cn/)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](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)
|
||||
|
||||
</div>
|
||||
[ [English](README_EN.md) / 中文 ]
|
||||
|
||||
CrossDesk 支持 Windows、macOS、Linux 之间的远程访问,也可从浏览器或原生 iOS 客户端控制电脑。项目基于 [MiniRTC](https://github.com/kunkundi/minirtc),提供实时音视频传输、键鼠控制、文件传输与自托管能力,仍在持续开发中。
|
||||
|
||||
> 本文按当前仓库代码整理。截图展示当前 Slint 桌面界面,使用演示 ID、密码、在线状态和连接记录;缩略图来自公开页面;下载包的功能与系统要求请同时核对对应 Release 说明。原生 iOS 客户端的构建、签名方式见下文。
|
||||
|
||||
[界面预览](#preview) · [下载与安装](#download) · [快速连接](#quick-start) · [会话操作](#session) · [设置](#settings) · [iOS](#ios) · [自托管](#self-hosting) · [源码构建](docs/BUILD.md) · [常见问题](docs/FAQ.md)
|
||||
|
||||
<a id="preview"></a>
|
||||
---
|
||||
|
||||
## 界面预览
|
||||
|
||||
### 桌面客户端
|
||||
### PC 客户端
|
||||
|
||||
主窗口集中展示本机 ID、连接密码、远程连接入口和近期连接记录。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-main-zh.png" width="800" alt="当前桌面客户端示例:已连接服务器,三台在线设备均显示页面缩略图" />
|
||||
</p>
|
||||

|
||||
|
||||
### Web 客户端
|
||||
|
||||
在浏览器中输入远程设备 ID 和密码,无需在控制端安装桌面客户端。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/web-client.png" width="800" alt="CrossDesk Web 客户端当前连接首页" />
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
图片来源与更新方式见 [截图说明](docs/images/README.md)。
|
||||
---
|
||||
|
||||
## 主要功能
|
||||
## 简介
|
||||
|
||||
| 能力 | 当前支持 |
|
||||
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))等基础能力。
|
||||
|
||||
---
|
||||
|
||||
## 系统要求
|
||||
|
||||
| 平台 | 最低版本 |
|
||||
| --- | --- |
|
||||
| 跨平台控制 | Windows / macOS / Linux 桌面互控;浏览器与原生 iOS 作为控制端 |
|
||||
| 实时画面与声音 | H.264 / AV1、30 / 60 fps 设置、硬件编解码选项、远端声音播放;实际能力取决于设备与构建配置 |
|
||||
| 多设备与多显示器 | 近期连接、设备别名、会话标签与远端显示器切换 |
|
||||
| 输入与协作 | 键鼠输入、远端光标同步、组合键、文本剪贴板同步、文件传输 |
|
||||
| 网络与部署 | P2P 直连、TURN 中继、SRTP 加密选项、自托管信令与中继服务 |
|
||||
| Windows 锁屏控制 | 通过 CrossDesk Service 支持锁屏、登录界面及安全桌面的输入转发 |
|
||||
| **Windows** | Windows 10 及以上 (64 位) |
|
||||
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
|
||||
| **Linux** | Ubuntu 20.04 及以上 |
|
||||
|
||||
各客户端的入口和能力有所不同;桌面会话操作见下文,iOS 的具体功能见 [开发说明](apps/ios/README.md)。
|
||||
---
|
||||
|
||||
<a id="download"></a>
|
||||
## 使用
|
||||
|
||||
## 下载与安装
|
||||
在菜单栏“对端ID”处输入远端桌面的ID,点击“→”即可发起远程连接。
|
||||
|
||||
前往 [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) 或 [官方网站](https://www.crossdesk.cn/),按系统与 CPU 架构选择安装包。
|
||||

|
||||
|
||||
| 平台 | 当前源码 / CI 基线 | 安装方式 |
|
||||
| --- | --- | --- |
|
||||
| Windows | Windows 10+,x64 | `.exe` 安装包;便携构建也受支持 |
|
||||
| macOS | macOS 14.0+,Intel / Apple Silicon | 选择 x64 或 arm64 的 `.pkg` |
|
||||
| Linux | Ubuntu 20.04+,amd64 / arm64,glibc 2.31 基线 | `.deb` 安装包 |
|
||||
| iOS / iPadOS | iOS 16.0+,arm64 真机 | 原生客户端;使用 Xcode 构建并签名,CI 产物为未签名应用 |
|
||||
| Web | 支持 WebRTC 的浏览器 | 访问 [web.crossdesk.cn](https://web.crossdesk.cn/) |
|
||||
如果远端桌面设置了连接密码,则本端需填写正确的连接密码才能成功发起远程连接。
|
||||
|
||||
Linux 下载后,在下载目录中将文件名替换为实际安装包名称:
|
||||

|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
|
||||
```
|
||||
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
|
||||

|
||||
|
||||
**macOS 首次运行:** 按应用提示,在“系统设置 → 隐私与安全性”中授予 CrossDesk **屏幕录制**(新系统可能显示为“屏幕与系统音频录制”)及 **辅助功能**权限,再按系统提示重新打开应用。前者用于捕获桌面,后者用于远程键鼠输入。
|
||||
### 使用 Web 客户端
|
||||
|
||||
<a id="quick-start"></a>
|
||||
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
|
||||
输入 **远程设备 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" />
|
||||
|
||||
### 从另一台电脑连接
|
||||
### Windows 服务(CrossDesk Service)
|
||||
|
||||
1. **准备被控电脑。** 安装并运行 CrossDesk,等待底部显示“已连接服务器”。在左侧“本桌面”查看 **本机 ID** 和 **密码**,提供给控制端。
|
||||
2. **输入对端 ID。** 在控制端右侧“远程桌面 → 对端 ID”输入被控电脑的 ID,点击 **→**。
|
||||
3. **完成密码验证。** 按弹窗输入被控电脑当前的连接密码,确认后等待远程画面出现。需要保存密码时勾选“记住密码”。
|
||||
4. **再次连接。** 成功连接过的设备会出现在“近期连接”中,可通过卡片重连,也可编辑别名或删除记录。
|
||||
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||
|
||||
本机密码旁的眼睛按钮用于显示 / 隐藏密码,铅笔按钮用于修改密码。修改时使用 **6 位数字或英文字母**,并保持连接服务器;等待修改成功、客户端重新连接后,再复制当前密码。已保存旧密码的控制端需重新输入。
|
||||
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||
|
||||
### 从浏览器连接
|
||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||
|
||||
1. 保持被控电脑上的 CrossDesk 运行,并确认已连接服务器;在被控端“设置”中保持 **启用 SRTP** 开启。
|
||||
2. 打开 [Web 客户端](https://web.crossdesk.cn/),输入 **远程设备 ID** 和 **密码**,点击“连接”。
|
||||
3. 连接后使用页面提供的显示器、鼠标模式和键盘控件操作远端;手机和平板也可通过浏览器访问。
|
||||
|
||||
如果使用自建服务器,需要让控制端与被控端接入同一服务;Web 部署配置见 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
<a id="session"></a>
|
||||
|
||||
## 会话中的常用操作
|
||||
|
||||
远程窗口通过会话标签切换设备,控制栏提供以下操作;控制栏收起时先点击展开按钮。将鼠标悬停在图标上可查看提示。
|
||||
|
||||
| 入口 | 用途 |
|
||||
| --- | --- |
|
||||
| 显示器 | 切换远端显示器 |
|
||||
| 键盘 | 发送组合键;Windows 的 `Ctrl+Alt+Del` 依赖远端服务 |
|
||||
| 鼠标 | 控制 / 释放远端鼠标 |
|
||||
| 扬声器 | 播放声音 / 静音 |
|
||||
| 文件夹 | 选择文件发送到远端,并查看传输进度 |
|
||||
| 网络状态 | 查看流量、丢包率、帧率、分辨率及直连 / 中继状态 |
|
||||
| 全屏 | 进入 / 退出全屏 |
|
||||
| 断开连接 | 结束当前远程会话 |
|
||||
|
||||
桌面端可同步文本剪贴板。接收文件的位置可在“设置 → 文件保存路径”调整。关闭主窗口会隐藏窗口并保持程序运行;需要完全退出时使用系统托盘或菜单栏中的退出入口。
|
||||
|
||||
<a id="settings"></a>
|
||||
|
||||
## 调整画面与连接设置
|
||||
|
||||
点击主窗口右上角 **☰ → 设置**,修改后点击“确认”保存。建议在建立连接前调整;会话进行中部分选项不可修改。设置列表可向下滚动,查看自托管、启动方式与文件保存路径。
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-zh.png" width="720" alt="当前设置界面:画质、帧率、画面偏好、编解码、中继与 SRTP" />
|
||||
</p>
|
||||
|
||||
| 设置 | 说明 |
|
||||
| --- | --- |
|
||||
| 画面质量 / 帧率 | 选择低、中、高画质与 30 / 60 fps;实际帧率取决于网络和设备 |
|
||||
| 画面偏好 | 在帧率优先、画质优先、平衡之间选择 |
|
||||
| 视频编码格式 | H.264 / AV1;硬件编解码是否可用取决于平台、硬件与构建选项 |
|
||||
| 启用中继服务 | 允许借助 TURN 中继建立连接;P2P 连接失败时先检查此项 |
|
||||
| 启用 SRTP | 媒体传输加密选项,两端配置需兼容 |
|
||||
| 自托管配置 | 设置服务器地址、信令端口与中继端口,并勾选启用 |
|
||||
| 开机自启 / 启用守护进程 | 调整启动与进程守护行为,按界面提示重启后生效 |
|
||||
| 文件保存路径 | 选择接收文件的目录 |
|
||||
|
||||
<a id="ios"></a>
|
||||
|
||||
## 原生 iOS / iPadOS 客户端
|
||||
|
||||
仓库的 [`apps/ios`](apps/ios/README.md) 提供原生控制端,与桌面端使用同一套 MiniRTC 协议。它可以控制远端电脑;当前不提供将 iPhone / iPad 作为被控桌面的功能。
|
||||
|
||||
1. 按 [iOS 开发说明](apps/ios/README.md) 使用 Xcode 构建并签名,安装到 iOS 16+ 的真机。
|
||||
2. 在首页输入对端 ID,点击连接并填写密码;成功连接后可从“最近连接”中重连。
|
||||
3. 在设置中选择鼠标模式:**相对位置**像触控板一样移动光标,**绝对位置**将触摸位置直接映射到远端画面。
|
||||
4. 点击悬浮图标展开菜单,可切换显示器、显示键盘、控制声音或发送文件。收到的文件位于应用的 `Documents/Received`,可通过传输状态旁的分享按钮导出。
|
||||
|
||||
当前支持接收远端文本到 iOS 剪贴板;界面暂未提供主动发送本机剪贴板的按钮。
|
||||
|
||||
当前支持单指点击、双指右键、长按拖动与捏合缩放;缩放后的双指平移用于移动视图。原生端与浏览器端分别维护,使用入口和手势可能不同。
|
||||
|
||||
<a id="windows-service"></a>
|
||||
|
||||
## Windows 锁屏与登录界面
|
||||
|
||||
**CrossDesk Service**(服务名 `CrossDeskService`)提供锁屏、登录、凭据输入及安全桌面的状态上报、`Ctrl+Alt+Del` 发送和键鼠输入转发。
|
||||
|
||||
安装版会注册按需启动的服务,客户端启动时尝试启动它;本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。该服务依赖客户端运行。
|
||||
|
||||
<details>
|
||||
<summary>手动部署与服务管理命令</summary>
|
||||
|
||||
保留完整安装目录;自行部署构建产物时,将 `CrossDesk.exe`、`crossdesk_service.exe`、`crossdesk_session_helper.exe` 及同次构建输出的全部 `.dll` 放在同一目录。在该目录打开管理员 PowerShell,按需执行:
|
||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||
|
||||
```powershell
|
||||
# 安装
|
||||
.\CrossDesk.exe --service-install
|
||||
# 启动
|
||||
.\CrossDesk.exe --service-start
|
||||
# 查看状态
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# 停止或卸载服务
|
||||
# 停止
|
||||
.\CrossDesk.exe --service-stop
|
||||
# 卸载
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
</details>
|
||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||
|
||||
出现“远端 Windows 服务不可用”时,检查被控电脑的安装和服务状态;普通桌面连接仍可使用,受保护界面的控制能力会受限。
|
||||
---
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
## 如何编译
|
||||
|
||||
### 构建依赖
|
||||
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
|
||||
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev \
|
||||
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 捕获时,再安装对应依赖:
|
||||
|
||||
```bash
|
||||
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 版本:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
|
||||
cd crossdesk
|
||||
|
||||
# 已经克隆过仓库时执行这一行
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
|
||||
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
|
||||
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
|
||||
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
|
||||
|
||||
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
||||
|
||||
启用 Wayland 和 DRM 的配置示例:
|
||||
|
||||
```bash
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
生成 Debian 安装包(必须在对应架构上先完成 Release 编译):
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./apps/desktop/scripts/linux/pkg_arm64.sh 1.0.0
|
||||
```
|
||||
|
||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
|
||||
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
|
||||
|
||||
### 通用编译选项
|
||||
|
||||
```text
|
||||
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
|
||||
--USE_WAYLAND=true/false: 在 Linux 上启用 Wayland/PipeWire 捕获,默认不启用
|
||||
--USE_DRM=true/false: 在 Linux 上启用 DRM 捕获,默认不启用
|
||||
--CROSSDESK_PORTABLE=true/false: 构建便携版本,默认不启用
|
||||
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
|
||||
|
||||
# 示例
|
||||
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
xmake r crossdesk
|
||||
```
|
||||
|
||||
### 无 CUDA 环境下的开发支持
|
||||
|
||||
对于**未安装 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 XMAKE_GLOBALDIR=/data
|
||||
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
|
||||
|
||||
```powershell
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
|
||||
安装完成后执行:
|
||||
|
||||
```powershell
|
||||
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" />
|
||||
|
||||
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
|
||||
|
||||
```powershell
|
||||
set CUDA_PATH=path_to_cuda_installdir
|
||||
```
|
||||
|
||||
重新执行:
|
||||
|
||||
```powershell
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 注意
|
||||
|
||||
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
### 关于 Xmake
|
||||
|
||||
#### 安装 Xmake
|
||||
|
||||
使用 curl:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
|
||||
使用 wget:
|
||||
|
||||
```bash
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
|
||||
使用 powershell:
|
||||
|
||||
```powershell
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### 编译选项
|
||||
|
||||
```text
|
||||
# 切换编译模式
|
||||
xmake f -m debug/release
|
||||
|
||||
# 可选编译参数
|
||||
-r :重新构建目标
|
||||
-v :显示详细的构建日志
|
||||
-y :自动确认提示
|
||||
|
||||
# 示例
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 运行选项
|
||||
|
||||
```bash
|
||||
# 使用调试模式运行
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
|
||||
更多使用方法可参考 [Xmake 官方文档](https://xmake.io/guide/quick-start.html)。
|
||||
|
||||
---
|
||||
|
||||
## 自托管服务器
|
||||
|
||||
可自行部署信令服务与 TURN 中继,并在桌面客户端 **☰ → 设置 → 自托管配置** 中填写连接信息。控制端与被控端都需要启用同一套配置。
|
||||
### 服务端
|
||||
|
||||
- [自托管指南](docs/SELF_HOSTING.md):当前 Compose 部署入口、客户端设置、证书信任与排查。
|
||||
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server):服务端源码与发布配置。
|
||||
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client):浏览器端源码与自托管说明。
|
||||
推荐使用 Docker 部署 CrossDesk Server。
|
||||
|
||||
## 开发与反馈
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e CROSSDESK_SERVER_PORT=xxxx \
|
||||
-e COTURN_PORT=xxxx \
|
||||
-e MIN_PORT=xxxxx \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
- [源码构建](docs/BUILD.md):Windows、macOS、Linux、可选编译参数与打包。
|
||||
- [iOS 开发说明](apps/ios/README.md):原生端构建和真机验证。
|
||||
- [GUI 架构](docs/gui-architecture.md):桌面 UI、平台实现与工程结构。
|
||||
- [常见问题](docs/FAQ.md):连接失败、黑屏、文件位置与编译问题。
|
||||
- [提交问题](https://github.com/kunkundi/crossdesk/issues):请附上两端系统、客户端版本、连接方式和可复现步骤。
|
||||
上述命令中,用户需注意的参数如下:
|
||||
|
||||
## 致谢与许可
|
||||
#### 配置参数
|
||||
|
||||
感谢 [HelloGitHub](https://hellogithub.com/)、[阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 与 [LinuxDo](https://linux.do) 社区的推荐、交流与支持。
|
||||
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
|
||||
- INTERNAL_IP:服务器内网 IP
|
||||
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
|
||||
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
|
||||
- MIN_PORT/MAX_PORT:COTURN 服务使用的端口范围,例如:MIN_PORT=50000, MAX_PORT=60000,范围可根据客户端数量调整。
|
||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
|
||||
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
|
||||
|
||||
CrossDesk 使用 [GPL-3.0](LICENSE) 许可。隐私说明见 [隐私政策](PRIVACY.md#中文)。
|
||||
#### 部署示例
|
||||
|
||||
### Code signing policy
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=114.114.114.114 \
|
||||
-e INTERNAL_IP=10.0.0.1 \
|
||||
-e CROSSDESK_SERVER_PORT=9099 \
|
||||
-e COTURN_PORT=3478 \
|
||||
-e MIN_PORT=50000 \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
#### 注意事项
|
||||
|
||||
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
|
||||
- 如果不挂载 volume,容器删除后数据会丢失
|
||||
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
|
||||
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
|
||||
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
|
||||
|
||||
#### 目录权限
|
||||
|
||||
如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
||||
|
||||
- 证书生成失败,容器启动脚本会报错退出
|
||||
- 数据库目录创建失败,程序会抛出异常并崩溃
|
||||
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
||||
|
||||
在启动容器前手动设置权限:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
### 客户端
|
||||
|
||||
1. 点击右上角设置进入设置页面。<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||
|
||||
2. 点击`自托管服务器配置`按钮。<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
|
||||
|
||||
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
|
||||
|
||||
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
|
||||
|
||||
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
|
||||
|
||||
**Windows:** 使用管理员权限打开 PowerShell 执行:
|
||||
|
||||
```powershell
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
### 自托管 Web 客户端
|
||||
|
||||
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md)。
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
||||
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
||||
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
||||
|
||||
---
|
||||
|
||||
## Code signing policy
|
||||
|
||||
CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供代码签名。
|
||||
|
||||
@@ -213,3 +431,4 @@ CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供
|
||||
|
||||
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
|
||||
- **隐私政策:** [查看 CrossDesk 隐私政策](PRIVACY.md#中文)
|
||||
|
||||
+390
-154
@@ -1,215 +1,451 @@
|
||||
<div align="center">
|
||||
<img src="apps/desktop/resources/linux/crossdesk_128x128.png" width="80" alt="CrossDesk logo" />
|
||||
<h1>CrossDesk</h1>
|
||||
<p>A lightweight, cross-platform remote desktop for computers, browsers, and iPhone / iPad.</p>
|
||||
<p><a href="https://www.crossdesk.cn/">Website</a> · <a href="https://github.com/kunkundi/crossdesk/releases">Download</a> · <a href="https://web.crossdesk.cn/">Open Web Client</a> · <a href="README.md">中文</a></p>
|
||||
</div>
|
||||
# CrossDesk
|
||||
|
||||
<div align="center">
|
||||
<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>
|
||||
|
||||
[](#download)
|
||||
[](https://github.com/kunkundi/crossdesk/releases)
|
||||
[](https://github.com/kunkundi/crossdesk/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](https://www.crossdesk.cn/)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](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)
|
||||
|
||||
</div>
|
||||
[ [中文](README.md) / English ]
|
||||
|
||||
CrossDesk connects Windows, macOS, and Linux desktops, with browser and native iOS clients for controlling computers. Built on [MiniRTC](https://github.com/kunkundi/minirtc), it supports real-time video and audio, keyboard and mouse control, file transfer, and self-hosting. The project is under active development.
|
||||
|
||||
> This guide follows the current repository source. Desktop screenshots render the current Slint UI with sample IDs, passwords, online statuses, and recent devices; thumbnails show public pages. Check each release's notes for the features and requirements of its binaries. Native iOS build and signing instructions are below.
|
||||
|
||||
[Preview](#preview) · [Download](#download) · [Quick start](#quick-start) · [Session controls](#session) · [Settings](#settings) · [iOS](#ios) · [Self-hosting](#self-hosting) · [Build](docs/BUILD_EN.md) · [FAQ](docs/FAQ.md#english)
|
||||
|
||||
<a id="preview"></a>
|
||||
---
|
||||
|
||||
## Preview
|
||||
|
||||
### Desktop client
|
||||
### PC Client
|
||||
|
||||
The main window brings together your device ID, connection password, remote connection entry, and recent devices.
|
||||

|
||||
|
||||
### Web Client
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-main-en.png" width="800" alt="Current desktop UI example showing a connected server and three online devices with page thumbnails" />
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
### Web client
|
||||
---
|
||||
|
||||
Enter a remote device ID and password in your browser without installing the desktop app on the controlling device.
|
||||
## Introduction
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/web-client.png" width="800" alt="Current CrossDesk Web Client connection page in Chinese" />
|
||||
</p>
|
||||
CrossDesk is a lightweight cross-platform remote desktop software.
|
||||
|
||||
See the [screenshot notes](docs/images/README.md) for image sources and refresh instructions.
|
||||
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)).
|
||||
|
||||
## Features
|
||||
---
|
||||
|
||||
| Capability | Current support |
|
||||
## System Requirements
|
||||
|
||||
| Platform | Minimum Version |
|
||||
| --- | --- |
|
||||
| Cross-platform control | Windows / macOS / Linux desktop control; browser and native iOS controllers |
|
||||
| Live video and audio | H.264 / AV1, 30 / 60 fps settings, hardware codec options, and remote audio; availability depends on hardware and build configuration |
|
||||
| Devices and displays | Recent connections, device aliases, session tabs, and remote display switching |
|
||||
| Input and sharing | Keyboard/mouse input, remote cursor synchronization, shortcuts, text clipboard synchronization, and file transfer |
|
||||
| Networking and deployment | Direct P2P connections, TURN relay, an SRTP encryption option, and self-hosted signaling/relay services |
|
||||
| Windows protected desktops | CrossDesk Service forwards input on lock screens, sign-in screens, and secure desktops |
|
||||
| **Windows** | Windows 10 or later (64-bit) |
|
||||
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
|
||||
| **Linux** | Ubuntu 20.04 or later |
|
||||
|
||||
Controls and capabilities vary by client. Desktop controls are described below; see the [iOS guide](apps/ios/README.md) for native mobile features.
|
||||
---
|
||||
|
||||
<a id="download"></a>
|
||||
## Usage
|
||||
|
||||
## Download and install
|
||||
Enter the remote desktop ID in the menu bar’s “Remote ID” field and click “→” to initiate a remote connection.
|
||||
|
||||
Choose a package matching your OS and CPU architecture from [GitHub Releases](https://github.com/kunkundi/crossdesk/releases) or the [website](https://www.crossdesk.cn/).
|
||||

|
||||
|
||||
| Platform | Current source / CI baseline | Installation |
|
||||
| --- | --- | --- |
|
||||
| Windows | Windows 10+, x64 | `.exe` installer; portable builds are also supported |
|
||||
| macOS | macOS 14.0+, Intel / Apple Silicon | x64 or arm64 `.pkg` |
|
||||
| Linux | Ubuntu 20.04+, amd64 / arm64, glibc 2.31 baseline | `.deb` package |
|
||||
| iOS / iPadOS | iOS 16.0+, arm64 physical device | Native controller; build and sign with Xcode, or sign the unsigned CI app |
|
||||
| Web | A WebRTC-capable browser | Open [web.crossdesk.cn](https://web.crossdesk.cn/) |
|
||||
If the remote desktop requires a connection password, you must enter the correct password on your side to successfully establish the connection.
|
||||
|
||||
On Linux, replace the example filename with the downloaded package name:
|
||||

|
||||
|
||||
```bash
|
||||
sudo apt install "./crossdesk-linux-amd64-<version>.deb"
|
||||
```
|
||||
Before connecting, you can customize configuration options in the settings, such as language and video encoding format.
|
||||
|
||||
**First launch on macOS:** follow the app prompts to grant **Screen Recording** (called **Screen & System Audio Recording** on newer systems) and **Accessibility** under **System Settings → Privacy & Security**. Reopen the app if prompted. These permissions allow desktop capture and remote keyboard/mouse input respectively.
|
||||

|
||||
|
||||
<a id="quick-start"></a>
|
||||
### Using the Web Client
|
||||
|
||||
## Quick start
|
||||
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**:
|
||||
|
||||
### Connect from another computer
|
||||
<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" />
|
||||
|
||||
1. **Prepare the host.** Install and run CrossDesk on the computer to control. Wait for the bottom status bar to show that the server is connected. Share the **Local ID** and **Password** shown under **Local Desktop** with the controller.
|
||||
2. **Enter the remote ID.** On the controlling computer, enter the host's ID in **Remote Desktop → Remote ID**, then click **→**.
|
||||
3. **Verify the password.** Enter the host's current connection password when prompted and confirm. Select **Remember password** if you want to save it.
|
||||
4. **Reconnect later.** Previously connected devices appear under **Recent Connections**. Reconnect from a card, edit its alias, or remove its record.
|
||||
### Windows Service (CrossDesk Service)
|
||||
|
||||
The eye button shows or hides the local password; the pencil button changes it. Use **6 ASCII letters or digits** and stay connected to the server while changing it. Wait for the change to succeed and the client to reconnect, then copy the current password. Controllers that saved the old password must enter it again.
|
||||
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:
|
||||
|
||||
### Connect from a browser
|
||||
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||
- 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.
|
||||
|
||||
1. Keep CrossDesk running and connected to the server on the host computer, with **Enable SRTP** enabled in its Settings.
|
||||
2. Open the [Web Client](https://web.crossdesk.cn/), enter the **Remote Device ID** and **Password**, and click **Connect**.
|
||||
3. Use the page's display, mouse-mode, and keyboard controls during the session. Phone and tablet browsers can also connect.
|
||||
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 self-hosting, connect both sides to the same service. Browser deployment is documented in [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
|
||||
|
||||
<a id="session"></a>
|
||||
|
||||
## Session controls
|
||||
|
||||
Switch devices using session tabs. Expand the control bar if it is collapsed, and hover over icons to see their tooltips.
|
||||
|
||||
| Control | Action |
|
||||
| --- | --- |
|
||||
| Display | Switch the remote monitor |
|
||||
| Keyboard | Send shortcuts; Windows `Ctrl+Alt+Del` requires the remote service |
|
||||
| Mouse | Enable / release remote mouse control |
|
||||
| Speaker | Play / mute remote audio |
|
||||
| Folder | Select a file to send and view transfer progress |
|
||||
| Network statistics | Inspect traffic, packet loss, frame rate, resolution, and direct / relay mode |
|
||||
| Fullscreen | Enter / exit fullscreen |
|
||||
| Disconnect | End the current remote session |
|
||||
|
||||
Desktop clients synchronize text clipboard contents. Configure the receiving directory under **Settings → File Save Path**. Closing the main window hides it and keeps the app running; use the system tray or menu-bar exit command to quit completely.
|
||||
|
||||
<a id="settings"></a>
|
||||
|
||||
## Video and connection settings
|
||||
|
||||
Open **☰ → Settings** in the top-right corner and click **OK** to save. Configure the session before connecting; some settings are disabled during an active session. Scroll down for self-hosting, startup behavior, and the file save path.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/desktop-settings-en.png" width="720" alt="Current settings for quality, frame rate, video preference, codecs, relay, and SRTP" />
|
||||
</p>
|
||||
|
||||
| Setting | Purpose |
|
||||
| --- | --- |
|
||||
| Video Quality / Video Capture Frame Rate | Low, medium, or high quality and 30 / 60 fps; actual performance depends on the connection and device |
|
||||
| Video Preference | Choose frame-rate priority, quality priority, or balanced adaptation |
|
||||
| Video Encode Format | H.264 / AV1; hardware codec availability depends on the platform, device, and build options |
|
||||
| Enable TURN Service | Allow relay-assisted connections; check this setting if P2P fails |
|
||||
| SRTP | Media encryption option; configurations on both ends must be compatible |
|
||||
| Self-Hosted Config | Set the host, signaling port, and TURN port, then enable the checkbox |
|
||||
| Auto Start / Enable Daemon | Configure startup and process supervision; restart as indicated by the UI |
|
||||
| File Save Path | Choose where received files are saved |
|
||||
|
||||
<a id="ios"></a>
|
||||
|
||||
## Native iOS / iPadOS client
|
||||
|
||||
[`apps/ios`](apps/ios/README.md) contains a native controller using the same MiniRTC protocol as the desktop client. It controls remote computers; it does not expose an iPhone or iPad as a remotely controlled desktop.
|
||||
|
||||
1. Follow the [iOS guide](apps/ios/README.md) to build and sign the app with Xcode, then install it on an iOS 16+ physical device.
|
||||
2. Enter a remote ID on the home screen, connect, and enter its password. Successful connections become available in the recent devices list.
|
||||
3. Choose a mouse mode in Settings: **Relative Position** works like a trackpad; **Absolute Position** maps touches directly to the remote screen.
|
||||
4. Tap the floating icon to expand the menu, then switch displays, open the keyboard, toggle audio, or send files. Received files are stored in `Documents/Received` and can be exported using the share button beside the transfer status.
|
||||
|
||||
The app currently receives remote text into the iOS clipboard. Its UI does not yet expose a button to send the local clipboard.
|
||||
|
||||
Gestures include one-finger click, two-finger right-click, long-press drag, and pinch-to-zoom. When zoomed in, two-finger panning moves the viewport. The native and browser clients are maintained separately, so their controls and gestures may differ. The current native UI uses Chinese labels.
|
||||
|
||||
<a id="windows-service"></a>
|
||||
|
||||
## Windows lock screens and sign-in
|
||||
|
||||
**CrossDesk Service** (`CrossDeskService`) reports protected desktop states and forwards `Ctrl+Alt+Del`, keyboard, and mouse input on lock screens, sign-in screens, credential prompts, and secure desktops.
|
||||
|
||||
The installer registers an on-demand service. CrossDesk tries to start it on launch, and the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges. The service depends on a running client.
|
||||
|
||||
<details>
|
||||
<summary>Manual deployment and service commands</summary>
|
||||
|
||||
Keep the complete installation directory. For manual deployment, place `CrossDesk.exe`, `crossdesk_service.exe`, `crossdesk_session_helper.exe`, and all `.dll` files from the same build together. Open an administrator PowerShell in that directory and run the commands you need:
|
||||
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
|
||||
.\CrossDesk.exe --service-install
|
||||
# start
|
||||
.\CrossDesk.exe --service-start
|
||||
# check status
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# Stop or uninstall the service
|
||||
# stop
|
||||
.\CrossDesk.exe --service-stop
|
||||
# uninstall
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
</details>
|
||||
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 **Remote Windows service unavailable** appears, check installation and service status on the host. Ordinary desktop connections remain available, while control of protected screens is limited.
|
||||
---
|
||||
|
||||
<a id="self-hosting"></a>
|
||||
## How to build
|
||||
|
||||
## Self-hosting
|
||||
### Build requirements
|
||||
|
||||
Host your own signaling and TURN services, then enter their connection details under **☰ → Settings → Self-Hosted Config**. Enable the same server configuration on both the controller and host.
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
|
||||
|
||||
- [Self-hosting guide](docs/SELF_HOSTING_EN.md): current Compose deployment, client settings, certificate trust, and troubleshooting.
|
||||
- [CrossDesk Server](https://github.com/kunkundi/crossdesk-server): server source and release configuration.
|
||||
- [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client): browser source and deployment instructions.
|
||||
### Linux
|
||||
|
||||
## Development and feedback
|
||||
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:
|
||||
|
||||
- [Build from source](docs/BUILD_EN.md): Windows, macOS, Linux, optional flags, and packaging.
|
||||
- [iOS development](apps/ios/README.md): native build and physical-device checks.
|
||||
- [GUI architecture](docs/gui-architecture.md): desktop UI and platform organization.
|
||||
- [FAQ](docs/FAQ.md#english): connection failures, blank video, received files, and build issues.
|
||||
- [Report an issue](https://github.com/kunkundi/crossdesk/issues): include both operating systems, client versions, connection mode, and reproduction steps.
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxcb-shm0-dev \
|
||||
libasound2-dev libsndio-dev libpulse-dev \
|
||||
libgl1-mesa-dev
|
||||
```
|
||||
|
||||
## Acknowledgements and license
|
||||
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.
|
||||
|
||||
Thanks to [HelloGitHub](https://hellogithub.com/), [Ruanyf Weekly](https://github.com/ruanyf/weekly), and the [LinuxDo](https://linux.do) community for featuring CrossDesk and contributing feedback.
|
||||
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.
|
||||
|
||||
CrossDesk is licensed under [GPL-3.0](LICENSE). See the [Privacy Policy](PRIVACY.md#english).
|
||||
Install the optional dependencies when Wayland capture or DRM capture is enabled:
|
||||
|
||||
### Code signing policy
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libdrm-dev
|
||||
|
||||
CrossDesk uses SignPath.io to sign official Windows releases built from this repository.
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/kunkundi/crossdesk.git
|
||||
cd crossdesk
|
||||
|
||||
# Run this when the repository has already been cloned
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
|
||||
# header-only SDK used during compilation. Skip this when the distribution's
|
||||
# PipeWire 0.3 development packages are already installed.
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
|
||||
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
|
||||
`--cc`/`--cxx` may still let dependency builds fall back to the system
|
||||
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
|
||||
configuration.
|
||||
|
||||
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
||||
|
||||
Example configuration with Wayland and DRM capture enabled:
|
||||
|
||||
```bash
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
Build a Debian package after compiling a release binary on the matching architecture:
|
||||
|
||||
```bash
|
||||
# amd64
|
||||
./apps/desktop/scripts/linux/pkg_amd64.sh 1.0.0
|
||||
|
||||
# arm64
|
||||
./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.
|
||||
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
|
||||
|
||||
```text
|
||||
--USE_CUDA=true/false: Enable CUDA hardware codec acceleration, disabled by default
|
||||
--USE_WAYLAND=true/false: Enable Wayland/PipeWire capture on Linux, disabled by default
|
||||
--USE_DRM=true/false: Enable DRM capture on Linux, disabled by default
|
||||
--CROSSDESK_PORTABLE=true/false: Build the portable variant, disabled by default
|
||||
--CROSSDESK_VERSION=xxx: Set the CrossDesk version
|
||||
|
||||
# Example
|
||||
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
xmake r crossdesk
|
||||
```
|
||||
|
||||
### Development Without CUDA Environment
|
||||
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
|
||||
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
|
||||
|
||||
After entering the container, download the project and run:
|
||||
|
||||
```bash
|
||||
export CUDA_PATH=/usr/local/cuda
|
||||
export XMAKE_GLOBALDIR=/data
|
||||
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
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"
|
||||
```
|
||||
|
||||
After the installation is complete, execute:
|
||||
|
||||
```powershell
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
|
||||
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" />
|
||||
|
||||
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:
|
||||
|
||||
```powershell
|
||||
set CUDA_PATH=path_to_cuda_installdir
|
||||
```
|
||||
|
||||
Then re-run:
|
||||
|
||||
```powershell
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
### About Xmake
|
||||
|
||||
#### Installing Xmake
|
||||
|
||||
You can install Xmake using one of the following methods:
|
||||
|
||||
Using curl:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
|
||||
Using wget:
|
||||
|
||||
```bash
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
|
||||
Using powershell:
|
||||
|
||||
```powershell
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### Build Options
|
||||
|
||||
```text
|
||||
# Switch build mode
|
||||
xmake f -m debug/release
|
||||
|
||||
# Optional build parameters
|
||||
-r : Rebuild the target
|
||||
-v : Show detailed build logs
|
||||
-y : Automatically confirm prompts
|
||||
|
||||
# Example
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### Run Options
|
||||
|
||||
```bash
|
||||
# Run in debug mode
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
|
||||
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html).
|
||||
|
||||
---
|
||||
|
||||
## Self-Hosted Server
|
||||
|
||||
### Server
|
||||
|
||||
It is recommended to deploy CrossDesk Server using Docker.
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e INTERNAL_IP=xxx.xxx.xxx.xxx \
|
||||
-e CROSSDESK_SERVER_PORT=xxxx \
|
||||
-e COTURN_PORT=xxxx \
|
||||
-e MIN_PORT=xxxxx \
|
||||
-e MAX_PORT=xxxxx \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
The parameters you need to pay attention to are as follows:
|
||||
|
||||
#### Parameters
|
||||
|
||||
- **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.
|
||||
- **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**.
|
||||
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
|
||||
- `-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.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
-e EXTERNAL_IP=114.114.114.114 \
|
||||
-e INTERNAL_IP=10.0.0.1 \
|
||||
-e CROSSDESK_SERVER_PORT=9099 \
|
||||
-e COTURN_PORT=3478 \
|
||||
-e MIN_PORT=50000 \
|
||||
-e MAX_PORT=60000 \
|
||||
-v /var/lib/crossdesk:/var/lib/crossdesk \
|
||||
-v /var/log/crossdesk:/var/log/crossdesk \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
#### Notes
|
||||
|
||||
- **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.
|
||||
- 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`.
|
||||
- Log files will be created and stored at `/var/log/crossdesk/`.
|
||||
|
||||
#### 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.
|
||||
- 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:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
#### TLS certificate
|
||||
|
||||
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
||||
|
||||
### Client
|
||||
|
||||
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>
|
||||
|
||||
2. Click `Self-Hosted Server Configuration` button.<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
|
||||
|
||||
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
|
||||
|
||||
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
|
||||
|
||||
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:
|
||||
|
||||
**Windows:** Open PowerShell with administrator privileges and execute:
|
||||
|
||||
```powershell
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
### Self-Hosted Web Client
|
||||
|
||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
See [FAQ](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md).
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- 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 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)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Keep readelf and ldd output stable for the runtime dependency checks below.
|
||||
export LC_ALL=C
|
||||
|
||||
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
||||
echo "Usage: $0 <debian-arch> <xmake-arch> <version> [extra-recommendation]" >&2
|
||||
exit 2
|
||||
@@ -172,12 +169,11 @@ EOF
|
||||
|
||||
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Version=$DEB_VERSION
|
||||
Name=$APP_NAME
|
||||
Comment=$DESCRIPTION
|
||||
Exec=/usr/bin/$PKG_NAME
|
||||
Icon=$PKG_NAME
|
||||
StartupWMClass=$PKG_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet('PrepareBinaries', 'RestoreBinaries', 'VerifyInstaller')]
|
||||
[string]$Operation,
|
||||
[Parameter(Mandatory)] [string]$Artifact,
|
||||
[string]$Directory,
|
||||
[ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
|
||||
)
|
||||
|
||||
. "$PSScriptRoot/signpath-common.ps1"
|
||||
|
||||
if ($Operation -ne 'PrepareBinaries' -and !$SigningPolicy) {
|
||||
throw 'SigningPolicy is required when verifying signed artifacts.'
|
||||
}
|
||||
if ($Operation -eq 'VerifyInstaller') {
|
||||
Assert-CrossDeskSignature -Path $Artifact -SigningPolicy $SigningPolicy
|
||||
return
|
||||
}
|
||||
if (!$Directory -or !(Test-Path -LiteralPath $Directory -PathType Container)) {
|
||||
throw "Binaries directory does not exist: $Directory"
|
||||
}
|
||||
|
||||
$files = @(Get-CrossDeskSigningFiles)
|
||||
$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) "crossdesk-signing-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
|
||||
try {
|
||||
if ($Operation -eq 'PrepareBinaries') {
|
||||
foreach ($name in $files) {
|
||||
$source = Join-Path $Directory $name
|
||||
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
|
||||
throw "Missing CrossDesk binary: $source"
|
||||
}
|
||||
Copy-Item -LiteralPath $source -Destination (Join-Path $temporaryDirectory $name)
|
||||
}
|
||||
$artifactPath = [IO.Path]::GetFullPath($Artifact)
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $artifactPath) -Force | Out-Null
|
||||
Compress-Archive -Path "$temporaryDirectory/*" -DestinationPath $artifactPath -Force
|
||||
return
|
||||
}
|
||||
|
||||
# Inspect the ZIP before extraction: reject missing, duplicate or unexpected
|
||||
# entries (including paths) so only the four intended files can be replaced.
|
||||
$archive = [IO.Compression.ZipFile]::OpenRead([IO.Path]::GetFullPath($Artifact))
|
||||
try {
|
||||
$entries = @($archive.Entries | ForEach-Object { $_.FullName })
|
||||
$uniqueEntries = @($entries | Sort-Object -Unique)
|
||||
if ($entries.Count -ne $files.Count -or $uniqueEntries.Count -ne $files.Count -or
|
||||
@(Compare-Object $files $entries).Count -ne 0) {
|
||||
throw "Signed archive must contain exactly: $($files -join ', ')"
|
||||
}
|
||||
} finally {
|
||||
$archive.Dispose()
|
||||
}
|
||||
Expand-Archive -LiteralPath $Artifact -DestinationPath $temporaryDirectory
|
||||
|
||||
# Verify every file before replacing any build output.
|
||||
foreach ($name in $files) {
|
||||
Assert-CrossDeskSignature -Path (Join-Path $temporaryDirectory $name) -SigningPolicy $SigningPolicy
|
||||
}
|
||||
foreach ($name in $files) {
|
||||
Copy-Item -LiteralPath (Join-Path $temporaryDirectory $name) -Destination (Join-Path $Directory $name) -Force
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporaryDirectory -Recurse -Force
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<zip-file>
|
||||
<pe-file-set product-name="CrossDesk"
|
||||
product-version="${version}"
|
||||
file-version="${version}"
|
||||
company-name="CrossDesk"
|
||||
copyright="Copyright (C) CrossDesk contributors">
|
||||
<include path="crossdesk.exe" original-filename="CrossDesk.exe" />
|
||||
<include path="crossdesk_service.exe" original-filename="crossdesk_service.exe" />
|
||||
<include path="crossdesk_session_helper.exe" original-filename="crossdesk_session_helper.exe" />
|
||||
<include path="wgc_plugin.dll" original-filename="wgc_plugin.dll" />
|
||||
<for-each>
|
||||
<authenticode-sign />
|
||||
</for-each>
|
||||
</pe-file-set>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
@@ -1,57 +0,0 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-CrossDeskSigningFiles {
|
||||
# Only binaries built from this project's sources. Never include third-party DLLs.
|
||||
return @('crossdesk.exe', 'crossdesk_service.exe', 'crossdesk_session_helper.exe', 'wgc_plugin.dll')
|
||||
}
|
||||
|
||||
function Find-CrossDeskSignTool {
|
||||
$command = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
return $command.Source
|
||||
}
|
||||
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
|
||||
$tool = Get-ChildItem "$sdk/*/x64/signtool.exe" |
|
||||
Sort-Object { [version]$_.Directory.Parent.Name } |
|
||||
Select-Object -Last 1
|
||||
if (!$tool) {
|
||||
throw 'SignTool from the Windows SDK is required to verify release signatures.'
|
||||
}
|
||||
return $tool.FullName
|
||||
}
|
||||
|
||||
function Assert-CrossDeskSignature {
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Path,
|
||||
[Parameter(Mandatory)] [ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
|
||||
)
|
||||
|
||||
if (!(Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "Missing signed file: $Path"
|
||||
}
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $Path
|
||||
if (!$signature.SignerCertificate) {
|
||||
throw "File has no Authenticode signing certificate: $Path"
|
||||
}
|
||||
Write-Host "Signature: $Path | $($signature.Status) | $($signature.SignerCertificate.Subject)"
|
||||
if ($SigningPolicy -eq 'test-signing') {
|
||||
# Self-signed test certificates are not trusted by stock GitHub runners.
|
||||
# HashMismatch, NotSigned, UnknownError, etc. must still stop the build.
|
||||
if ($signature.Status -notin @('Valid', 'NotTrusted')) {
|
||||
throw "Invalid test signature on ${Path}: $($signature.Status)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($signature.Status -ne 'Valid') {
|
||||
throw "Release signature is not trusted on ${Path}: $($signature.Status)"
|
||||
}
|
||||
if (!$signature.TimeStamperCertificate) {
|
||||
throw "Release signature has no timestamp: $Path"
|
||||
}
|
||||
$signTool = Find-CrossDeskSignTool
|
||||
& $signTool verify /pa /all /v $Path
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "SignTool verification failed for $Path (exit $LASTEXITCODE)."
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
@@ -176,10 +175,6 @@ int HandleServiceCliCommand(const std::string& command) {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 &&
|
||||
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
|
||||
return crossdesk::RunSlintRendererProbe();
|
||||
}
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#include "task_queue_lock_free.h"
|
||||
|
||||
#include <exception>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
TaskQueueLockFree::TaskQueueLockFree()
|
||||
: producer_(task_queue_), worker_([this] { WorkerThread(); }) {}
|
||||
|
||||
TaskQueueLockFree::~TaskQueueLockFree() { Stop(); }
|
||||
|
||||
std::future<void> TaskQueueLockFree::PostTask(std::function<void()> task) {
|
||||
TaskItem item;
|
||||
item.run = std::move(task);
|
||||
auto result = item.completion.get_future();
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
if (stopping_) {
|
||||
throw std::runtime_error("Cannot post to a stopped task queue");
|
||||
}
|
||||
|
||||
// Count before publishing: the worker may dequeue as soon as enqueue returns.
|
||||
++pending_tasks_;
|
||||
if (!task_queue_.enqueue(producer_, std::move(item))) {
|
||||
--pending_tasks_;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
wake_.notify_one();
|
||||
return result;
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::Stop() {
|
||||
// Concurrent Stop callers all wait for the same worker shutdown to finish.
|
||||
std::call_once(stop_once_, [this] {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_all();
|
||||
worker_.join();
|
||||
});
|
||||
}
|
||||
|
||||
void TaskQueueLockFree::WorkerThread() {
|
||||
while (true) {
|
||||
TaskItem item;
|
||||
while (task_queue_.try_dequeue(item)) {
|
||||
--pending_tasks_;
|
||||
|
||||
std::exception_ptr error;
|
||||
try {
|
||||
item.run();
|
||||
} catch (...) {
|
||||
error = std::current_exception();
|
||||
}
|
||||
|
||||
// Release captured resources before marking the task complete.
|
||||
item.run = nullptr;
|
||||
if (error) {
|
||||
item.completion.set_exception(error);
|
||||
} else {
|
||||
item.completion.set_value();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock lock(mutex_);
|
||||
wake_.wait(lock, [this] { return stopping_ || pending_tasks_ > 0; });
|
||||
// A stop request must not discard tasks submitted while the worker was idle.
|
||||
if (stopping_ && pending_tasks_ == 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _TASK_QUEUE_LOCK_FREE_H_
|
||||
#define _TASK_QUEUE_LOCK_FREE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include <concurrentqueue.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// A single worker executes tasks in submission order. The concurrent queue
|
||||
// stores tasks; a mutex coordinates submissions, worker wakeups and shutdown.
|
||||
class TaskQueueLockFree {
|
||||
public:
|
||||
TaskQueueLockFree();
|
||||
~TaskQueueLockFree();
|
||||
|
||||
// Returns completion or the task's exception. Throws if the queue is stopped.
|
||||
std::future<void> PostTask(std::function<void()> task);
|
||||
|
||||
// Rejects new tasks, finishes accepted tasks and joins the worker.
|
||||
// May be called repeatedly, but never from the queue's own tasks.
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
struct TaskItem {
|
||||
std::function<void()> run;
|
||||
std::promise<void> completion;
|
||||
};
|
||||
|
||||
void WorkerThread();
|
||||
|
||||
moodycamel::ConcurrentQueue<TaskItem> task_queue_;
|
||||
// All submissions share one producer so different caller threads keep FIFO.
|
||||
moodycamel::ProducerToken producer_;
|
||||
std::atomic<std::size_t> pending_tasks_{0};
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopping_ = false;
|
||||
std::once_flag stop_once_;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -116,6 +116,16 @@ int ConfigCenter::Load() {
|
||||
} else {
|
||||
signal_server_port_ = 0;
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ =
|
||||
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
} else {
|
||||
coturn_server_port_ = 0;
|
||||
}
|
||||
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
@@ -161,6 +171,8 @@ int ConfigCenter::Save() {
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
@@ -304,6 +316,17 @@ int ConfigCenter::SetServerPort(int signal_server_port) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
|
||||
coturn_server_port_ = coturn_server_port;
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
enable_self_hosted_ = enable_self_hosted;
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
@@ -323,9 +346,19 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
signal_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "signal_server_port", 0));
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
}
|
||||
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
@@ -423,6 +456,8 @@ std::string ConfigCenter::GetSignalServerHost() const {
|
||||
|
||||
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
|
||||
|
||||
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
|
||||
|
||||
std::string ConfigCenter::GetDefaultServerHost() const {
|
||||
return signal_server_host_default_;
|
||||
}
|
||||
@@ -431,6 +466,10 @@ int ConfigCenter::GetDefaultSignalServerPort() const {
|
||||
return server_port_default_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetDefaultCoturnServerPort() const {
|
||||
return coturn_server_port_default_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
||||
|
||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
@@ -47,6 +47,7 @@ class ConfigCenter {
|
||||
int SetSrtp(bool enable_srtp);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetCoturnServerPort(int coturn_server_port);
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
@@ -66,8 +67,10 @@ class ConfigCenter {
|
||||
bool IsEnableSrtp() const;
|
||||
std::string GetSignalServerHost() const;
|
||||
int GetSignalServerPort() const;
|
||||
int GetCoturnServerPort() const;
|
||||
std::string GetDefaultServerHost() const;
|
||||
int GetDefaultSignalServerPort() const;
|
||||
int GetDefaultCoturnServerPort() const;
|
||||
bool IsSelfHosted() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
@@ -95,6 +98,8 @@ class ConfigCenter {
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
int signal_server_port_ = 0;
|
||||
int server_port_default_ = 9099;
|
||||
int coturn_server_port_ = 0;
|
||||
int coturn_server_port_default_ = 3478;
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _APPLICATION_STATE_H_
|
||||
#define _APPLICATION_STATE_H_
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -66,6 +60,8 @@ struct InteractionState {
|
||||
bool mouse_controller_is_started_ = false;
|
||||
bool start_screen_capturer_ = false;
|
||||
bool screen_capturer_is_started_ = false;
|
||||
bool start_speaker_capturer_ = false;
|
||||
bool speaker_capturer_is_started_ = false;
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
@@ -196,4 +192,4 @@ struct ApplicationState : MainWindowState,
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
#include "platform/windows/gui/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/macos/gui/tray/mac_tray.h"
|
||||
@@ -48,7 +47,6 @@
|
||||
#include <X11/Xlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "platform/linux/gui/cuda_graphics.h"
|
||||
#include "platform/linux/gui/tray/linux_tray.h"
|
||||
#endif
|
||||
#include "rd_log.h"
|
||||
@@ -994,15 +992,6 @@ int GuiApplication::Run() {
|
||||
if (!InitializeSDL()) {
|
||||
return -1;
|
||||
}
|
||||
#if _WIN32
|
||||
const auto backend = ConfigureWindowsSlintBackend();
|
||||
if (!backend.success) {
|
||||
LOG_ERROR("{}", backend.diagnostic);
|
||||
SDL_Quit();
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
|
||||
#endif
|
||||
InitializeModules();
|
||||
InitializeUi();
|
||||
|
||||
@@ -1031,7 +1020,6 @@ void GuiApplication::InitializeSettings() {
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
|
||||
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||
// position. Use the session's XWayland compatibility server for the GUI so
|
||||
// auxiliary windows such as the controlled-side status window can be
|
||||
@@ -1111,9 +1099,6 @@ void GuiApplication::InitializeUi() {
|
||||
wayland_display_guard.emplace("WAYLAND_DISPLAY");
|
||||
wayland_socket_guard.emplace("WAYLAND_SOCKET");
|
||||
}
|
||||
// Match crossdesk.desktop even when the Debian launcher runs crossdesk-bin.
|
||||
// Set this before creating any windows so X11 and Wayland share the same id.
|
||||
slint::set_xdg_app_id("crossdesk");
|
||||
#endif
|
||||
ui_ = std::make_unique<SlintUi>();
|
||||
ui_->video_presenter =
|
||||
@@ -1154,7 +1139,6 @@ void GuiApplication::InitializeSystemTray() {
|
||||
ui_->main->show();
|
||||
ui_->main->window().set_minimized(false);
|
||||
#if defined(__APPLE__)
|
||||
MacActivateWindow(ui_->main->window().appkit_view());
|
||||
// A window restored from the tray may only obtain its NSWindow after the
|
||||
// callback returns, so let the regular UI tick configure it.
|
||||
ui_->main_native_titlebar_attempts = 30;
|
||||
@@ -1165,12 +1149,16 @@ void GuiApplication::InitializeSystemTray() {
|
||||
ui_->main->hide();
|
||||
}
|
||||
};
|
||||
auto open_settings = [this, show_window] {
|
||||
auto open_settings = [this] {
|
||||
if (!ui_) {
|
||||
return;
|
||||
}
|
||||
show_window();
|
||||
ui_->main->show();
|
||||
ui_->main->window().set_minimized(false);
|
||||
ui_->main->set_settings_open(true);
|
||||
#if defined(__APPLE__)
|
||||
ui_->main_native_titlebar_attempts = 30;
|
||||
#endif
|
||||
};
|
||||
auto exit_app = [this] {
|
||||
exit_ = true;
|
||||
@@ -1230,6 +1218,7 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
||||
ui_->main->set_server_host(signal_server_ip_self_);
|
||||
ui_->main->set_server_port(signal_server_port_self_);
|
||||
ui_->main->set_coturn_port(coturn_server_port_self_);
|
||||
}
|
||||
|
||||
void GuiApplication::BindMainCallbacks() {
|
||||
@@ -1387,6 +1376,7 @@ void GuiApplication::BindMainCallbacks() {
|
||||
main->on_save_self_hosted_settings([this] {
|
||||
const std::string host(ui_->main->get_server_host());
|
||||
const auto signal_port = ParsePort(ui_->main->get_server_port());
|
||||
const auto coturn_port = ParsePort(ui_->main->get_coturn_port());
|
||||
if (!host.empty()) {
|
||||
config_center_->SetServerHost(host);
|
||||
std::memset(signal_server_ip_self_, 0, sizeof(signal_server_ip_self_));
|
||||
@@ -1403,31 +1393,27 @@ void GuiApplication::BindMainCallbacks() {
|
||||
std::snprintf(signal_server_port_, sizeof(signal_server_port_), "%d",
|
||||
*signal_port);
|
||||
}
|
||||
if (coturn_port) {
|
||||
config_center_->SetCoturnServerPort(*coturn_port);
|
||||
std::snprintf(coturn_server_port_self_, sizeof(coturn_server_port_self_),
|
||||
"%d", *coturn_port);
|
||||
std::snprintf(coturn_server_port_, sizeof(coturn_server_port_), "%d",
|
||||
*coturn_port);
|
||||
}
|
||||
});
|
||||
main->on_cancel_self_hosted_settings([this] {
|
||||
const std::string host = config_center_->GetSignalServerHost();
|
||||
ui_->main->set_server_host(UiText(host));
|
||||
const int signal_port = config_center_->GetSignalServerPort();
|
||||
const int coturn_port = config_center_->GetCoturnServerPort();
|
||||
ui_->main->set_server_port(signal_port > 0
|
||||
? UiText(std::to_string(signal_port))
|
||||
: slint::SharedString{});
|
||||
ui_->main->set_coturn_port(coturn_port > 0
|
||||
? UiText(std::to_string(coturn_port))
|
||||
: slint::SharedString{});
|
||||
});
|
||||
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
|
||||
main->on_start_update([this] {
|
||||
#if _WIN32
|
||||
windows_updater_.Start(latest_version_info_);
|
||||
SyncWindowsUpdate();
|
||||
#else
|
||||
OpenUrl("https://crossdesk.cn");
|
||||
ui_->main->set_update_open(false);
|
||||
#endif
|
||||
});
|
||||
main->on_cancel_update([this] {
|
||||
#if _WIN32
|
||||
windows_updater_.Cancel();
|
||||
#endif
|
||||
ui_->main->set_update_open(false);
|
||||
});
|
||||
main->on_connection_cancel([this] {
|
||||
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
||||
if (!props) {
|
||||
@@ -1742,7 +1728,7 @@ void GuiApplication::BindServerCallbacks() {
|
||||
});
|
||||
server->on_disconnect_controller([this] {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
CloseServerController(selected_server_remote_id_);
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1752,7 +1738,6 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
if (!peer_) {
|
||||
@@ -2013,7 +1998,8 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
}
|
||||
show_offline_warning_window_ = true;
|
||||
if (uncertain && peer_) {
|
||||
CloseConnectionPeer();
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2026,7 +2012,8 @@ void GuiApplication::HandlePasswordChangeResult() {
|
||||
|
||||
LOG_INFO("Password changed successfully for [{}]", client_id_);
|
||||
if (peer_) {
|
||||
CloseConnectionPeer();
|
||||
LeaveConnection(peer_, client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2050,7 +2037,7 @@ void GuiApplication::HandleCredentialRecovery() {
|
||||
if (retry_active) {
|
||||
LOG_INFO("Pending credential was not accepted; retrying active credential");
|
||||
if (peer_) {
|
||||
CloseConnectionPeer();
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2115,9 +2102,6 @@ void GuiApplication::SyncMainWindow() {
|
||||
ui_->main->set_signal_tls_error(signal_status_ ==
|
||||
SignalStatus::SignalTlsCertError);
|
||||
ui_->main->set_update_available(update_available_);
|
||||
#if _WIN32
|
||||
SyncWindowsUpdate();
|
||||
#endif
|
||||
ui_->main->set_current_version(CROSSDESK_VERSION);
|
||||
ui_->main->set_latest_version(UiText(latest_version_));
|
||||
ui_->main->set_release_name(UiText(release_name_));
|
||||
@@ -2967,6 +2951,9 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
if (const auto port = ParsePort(main->get_server_port())) {
|
||||
config_center_->SetServerPort(*port);
|
||||
}
|
||||
if (const auto port = ParsePort(main->get_coturn_port())) {
|
||||
config_center_->SetCoturnServerPort(*port);
|
||||
}
|
||||
|
||||
language_button_value_last_ = language_button_value_;
|
||||
video_quality_button_value_last_ = video_quality_button_value_;
|
||||
@@ -3086,6 +3073,9 @@ void GuiApplication::CloseStreamTab(const std::string& remote_id) {
|
||||
}
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
|
||||
CloseRemoteSession(props);
|
||||
WaitForThumbnailSaveTasks();
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
ResetRemoteSessionResources(props);
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
@@ -3275,59 +3265,7 @@ bool GuiApplication::OpenUrl(const std::string& url) {
|
||||
return SDL_OpenURL(url.c_str());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
void GuiApplication::SyncWindowsUpdate() {
|
||||
using State = WindowsUpdater::State;
|
||||
if (windows_updater_.GetState() == State::Ready) {
|
||||
windows_updater_.Launch();
|
||||
if (windows_updater_.GetState() == State::Launched) {
|
||||
ui_->main->set_update_open(false);
|
||||
}
|
||||
}
|
||||
const State state = windows_updater_.GetState();
|
||||
const bool downloading = state == State::Downloading;
|
||||
ui_->main->set_update_downloading(downloading || state == State::Checking ||
|
||||
state == State::Launching);
|
||||
ui_->main->set_update_security_blocked(state == State::SecurityBlocked);
|
||||
ui_->main->set_update_failed(state == State::Failed ||
|
||||
state == State::LaunchFailed ||
|
||||
state == State::SecurityBlocked);
|
||||
const int language = localization_language_index_;
|
||||
std::string status;
|
||||
if (downloading) {
|
||||
status = localization::update_downloading[language];
|
||||
const uint64_t total = windows_updater_.Total();
|
||||
const uint64_t received = windows_updater_.Downloaded();
|
||||
if (total > 0) {
|
||||
const int percent = static_cast<int>(std::min(100.0, 100.0 * received / total));
|
||||
status += " " + std::to_string(percent) + "%";
|
||||
ui_->main->set_update_progress(static_cast<float>(percent) / 100.0f);
|
||||
} else {
|
||||
status += " " + std::to_string(received / (1024 * 1024)) + " MB";
|
||||
ui_->main->set_update_progress(-1.0f);
|
||||
}
|
||||
} else if (state == State::Failed) {
|
||||
status = localization::update_download_failed[language];
|
||||
} else if (state == State::LaunchFailed) {
|
||||
status = localization::update_launch_failed[language];
|
||||
} else if (state == State::Checking) {
|
||||
status = localization::update_checking[language];
|
||||
ui_->main->set_update_progress(1.0f);
|
||||
} else if (state == State::SecurityBlocked) {
|
||||
status = localization::update_security_blocked[language];
|
||||
} else if (state == State::Launching) {
|
||||
status = localization::update_launching[language];
|
||||
} else if (state == State::Launched) {
|
||||
status = localization::update_installer_opened[language];
|
||||
}
|
||||
ui_->main->set_update_status(UiText(status));
|
||||
}
|
||||
#endif
|
||||
|
||||
void GuiApplication::Cleanup() {
|
||||
#if _WIN32
|
||||
windows_updater_.Cancel();
|
||||
#endif
|
||||
if (!ui_) {
|
||||
SDL_Quit();
|
||||
return;
|
||||
@@ -3342,7 +3280,7 @@ void GuiApplication::Cleanup() {
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForSessionCleanup();
|
||||
WaitForThumbnailSaveTasks();
|
||||
devices_.DestroyAudioOutput();
|
||||
if (ui_->stream) {
|
||||
#if defined(__APPLE__)
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_APPLICATION_H_
|
||||
#define _GUI_APPLICATION_H_
|
||||
|
||||
@@ -16,9 +10,6 @@
|
||||
|
||||
#include "runtime/cursor_state_provider.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#if _WIN32
|
||||
#include "platform/windows/windows_updater.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -55,9 +46,6 @@ private:
|
||||
void HandlePasswordChangeResult();
|
||||
void HandleCredentialRecovery();
|
||||
void SyncMainWindow();
|
||||
#if _WIN32
|
||||
void SyncWindowsUpdate();
|
||||
#endif
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
@@ -93,9 +81,6 @@ private:
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
#if _WIN32
|
||||
WindowsUpdater windows_updater_;
|
||||
#endif
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SERVER_WINDOW_STATE_H_
|
||||
#define _SERVER_WINDOW_STATE_H_
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
@@ -31,6 +24,5 @@ inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
}
|
||||
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||
}
|
||||
} // namespace crossdesk::server_window_state
|
||||
|
||||
#endif
|
||||
} // namespace crossdesk::server_window_state
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_GEOMETRY_H_
|
||||
#define _WINDOW_GEOMETRY_H_
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -45,5 +38,3 @@ constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||
}
|
||||
|
||||
} // namespace crossdesk::window_geometry
|
||||
|
||||
#endif
|
||||
@@ -149,6 +149,8 @@ struct TranslationRow {
|
||||
"Server Address:", u8"Адрес сервера:") \
|
||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||
X(ok, u8"确认", "OK", u8"ОК") \
|
||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||
X(new_password, u8"请输入六位密码:", \
|
||||
@@ -184,26 +186,6 @@ struct TranslationRow {
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
|
||||
u8"Загрузка установщика…") \
|
||||
X(update_checking, u8"正在进行 Windows 安全检查…", \
|
||||
"Running Windows security checks…", u8"Проверка безопасности Windows…") \
|
||||
X(update_security_blocked, \
|
||||
u8"Windows 安全检查未通过,请查看安全软件日志并核实发布者。", \
|
||||
"Windows security checks failed. Review security logs and verify the publisher.", \
|
||||
u8"Проверка Windows не пройдена. Проверьте журнал защиты и издателя.") \
|
||||
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
|
||||
"Opening installer. Please confirm the administrator permission request…", \
|
||||
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
|
||||
X(update_download_failed, u8"下载失败,请点击更新重试,或访问官网。", \
|
||||
"Download failed. Click Update to retry, or visit the website.", \
|
||||
u8"Ошибка загрузки. Повторите обновление или откройте сайт.") \
|
||||
X(update_launch_failed, u8"安装包未能启动或授权已取消,请点击更新重试。", \
|
||||
"Installer launch failed or permission was cancelled. Click Update to retry.", \
|
||||
u8"Установщик не запущен или доступ отменён. Повторите обновление.") \
|
||||
X(update_installer_opened, u8"安装包已打开,请在安装向导中完成更新。", \
|
||||
"Installer opened. Follow the setup wizard to finish updating.", \
|
||||
u8"Установщик открыт. Следуйте указаниям мастера установки.") \
|
||||
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -20,23 +19,7 @@ constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
||||
: owner_(owner),
|
||||
speaker_capture_(
|
||||
[] {
|
||||
return std::unique_ptr<SpeakerCapturer>(
|
||||
SpeakerCapturerFactory().Create());
|
||||
},
|
||||
[this](unsigned char *data, size_t size, const char *) {
|
||||
if (!owner_.peer_) return;
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
SendAudioFrame(owner_.peer_, &frame, owner_.audio_label_.c_str());
|
||||
}) {}
|
||||
|
||||
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
|
||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||
std::chrono::steady_clock::time_point now, int fps) {
|
||||
@@ -58,6 +41,7 @@ bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||
void SessionDeviceManager::Initialize() {
|
||||
InitializeAudioOutput();
|
||||
screen_capturer_factory_ = new ScreenCapturerFactory();
|
||||
speaker_capturer_factory_ = new SpeakerCapturerFactory();
|
||||
device_controller_factory_ = new DeviceControllerFactory();
|
||||
keyboard_capturer_ =
|
||||
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
|
||||
@@ -90,8 +74,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
|
||||
const int init_ret = screen_capturer_->Init(
|
||||
fps, [this, fps](unsigned char *data, int size, int width, int height,
|
||||
const char *display_name,
|
||||
const MiniRtcNativeVideoFrame *native_frame) {
|
||||
const char *display_name) {
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
@@ -153,13 +136,12 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
}
|
||||
}
|
||||
|
||||
MiniRtcVideoFrame frame{};
|
||||
XVideoFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(data);
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
frame.native_frame = native_frame;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
@@ -216,12 +198,42 @@ int SessionDeviceManager::StopScreenCapturer() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::StartSpeakerCapturer() {
|
||||
speaker_capture_.SetEnabled(true);
|
||||
int SessionDeviceManager::StartSpeakerCapturer() {
|
||||
if (!speaker_capturer_) {
|
||||
speaker_capturer_ =
|
||||
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
|
||||
const int init_ret = speaker_capturer_->Init(
|
||||
[this](unsigned char *data, size_t size, const char *audio_name) {
|
||||
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
|
||||
size, owner_.audio_label_.c_str());
|
||||
});
|
||||
|
||||
if (init_ret != 0) {
|
||||
speaker_capturer_->Destroy();
|
||||
delete speaker_capturer_;
|
||||
speaker_capturer_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!speaker_capturer_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int ret = speaker_capturer_->Start();
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
owner_.start_speaker_capturer_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::StopSpeakerCapturer() {
|
||||
speaker_capture_.SetEnabled(false);
|
||||
int SessionDeviceManager::StopSpeakerCapturer() {
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Stop();
|
||||
owner_.start_speaker_capturer_ = false;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartMouseController() {
|
||||
@@ -431,6 +443,16 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.screen_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
|
||||
if (StartSpeakerCapturer() == 0) {
|
||||
owner_.speaker_capturer_is_started_ = true;
|
||||
}
|
||||
} else if (!owner_.start_speaker_capturer_ &&
|
||||
owner_.speaker_capturer_is_started_) {
|
||||
StopSpeakerCapturer();
|
||||
owner_.speaker_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
|
||||
if (StartMouseController() == 0) {
|
||||
owner_.mouse_controller_is_started_ = true;
|
||||
@@ -529,7 +551,6 @@ SessionDeviceManager::display_info_list() const {
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DestroyDevices() {
|
||||
speaker_capture_.Shutdown();
|
||||
if (mouse_controller_) {
|
||||
mouse_controller_->Destroy();
|
||||
delete mouse_controller_;
|
||||
@@ -540,6 +561,11 @@ void SessionDeviceManager::DestroyDevices() {
|
||||
delete screen_capturer_;
|
||||
screen_capturer_ = nullptr;
|
||||
}
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Destroy();
|
||||
delete speaker_capturer_;
|
||||
speaker_capturer_ = nullptr;
|
||||
}
|
||||
if (keyboard_capturer_) {
|
||||
delete keyboard_capturer_;
|
||||
keyboard_capturer_ = nullptr;
|
||||
@@ -549,6 +575,8 @@ void SessionDeviceManager::DestroyDevices() {
|
||||
void SessionDeviceManager::DestroyFactories() {
|
||||
delete screen_capturer_factory_;
|
||||
screen_capturer_factory_ = nullptr;
|
||||
delete speaker_capturer_factory_;
|
||||
speaker_capturer_factory_ = nullptr;
|
||||
delete device_controller_factory_;
|
||||
device_controller_factory_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SESSION_DEVICE_MANAGER_H_
|
||||
#define _SESSION_DEVICE_MANAGER_H_
|
||||
|
||||
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
@@ -24,7 +17,7 @@
|
||||
#include "device_controller_factory.h"
|
||||
#include "display_info.h"
|
||||
#include "screen_capturer_factory.h"
|
||||
#include "speaker_capture_controller.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -36,7 +29,6 @@ class GuiRuntime;
|
||||
class SessionDeviceManager {
|
||||
public:
|
||||
explicit SessionDeviceManager(GuiRuntime &owner);
|
||||
~SessionDeviceManager();
|
||||
|
||||
void Initialize();
|
||||
void UpdateInteractions();
|
||||
@@ -46,10 +38,8 @@ public:
|
||||
int InitializeScreenCapturer();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
||||
// returning so the connection peer can be destroyed safely.
|
||||
void StartSpeakerCapturer();
|
||||
void StopSpeakerCapturer();
|
||||
int StartSpeakerCapturer();
|
||||
int StopSpeakerCapturer();
|
||||
int StartMouseController();
|
||||
int StopMouseController();
|
||||
int StartKeyboardCapturer();
|
||||
@@ -86,7 +76,8 @@ private:
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer *screen_capturer_ = nullptr;
|
||||
SpeakerCaptureController speaker_capture_;
|
||||
SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr;
|
||||
SpeakerCapturer *speaker_capturer_ = nullptr;
|
||||
DeviceControllerFactory *device_controller_factory_ = nullptr;
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
@@ -102,4 +93,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
|
||||
@@ -20,16 +20,6 @@ namespace crossdesk {
|
||||
|
||||
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
FileTransferManager::~FileTransferManager() {
|
||||
std::vector<std::future<void>> workers;
|
||||
{
|
||||
std::lock_guard lock(workers_mutex_);
|
||||
stopping_ = true;
|
||||
workers.swap(workers_);
|
||||
}
|
||||
for (auto& worker : workers) worker.wait();
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
|
||||
return global_state_;
|
||||
}
|
||||
@@ -102,16 +92,8 @@ void FileTransferManager::Start(
|
||||
std::shared_ptr<RemoteSession> props,
|
||||
const std::filesystem::path &file_path, const std::string &file_label,
|
||||
const std::string &remote_id) {
|
||||
if (props && props->closing_) return;
|
||||
const bool is_global = !props;
|
||||
PeerPtr* peer = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events;
|
||||
{
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
if (props && props->closing_) return;
|
||||
peer = is_global ? owner_.peer_ : props->peer_;
|
||||
peer_events = is_global ? owner_.peer_events_ : props->peer_events_;
|
||||
}
|
||||
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
|
||||
if (!peer) {
|
||||
LOG_ERROR("StartFileTransfer: invalid peer");
|
||||
return;
|
||||
@@ -126,14 +108,7 @@ void FileTransferManager::Start(
|
||||
}
|
||||
|
||||
const auto props_weak = std::weak_ptr<RemoteSession>(props);
|
||||
std::lock_guard workers_lock(workers_mutex_);
|
||||
if (stopping_) return;
|
||||
// Reap finished senders without blocking the UI.
|
||||
std::erase_if(workers_, [](auto& worker) {
|
||||
return worker.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
});
|
||||
workers_.push_back(std::async(std::launch::async,
|
||||
[this, peer, peer_events, file_path, file_label, props_weak, remote_id,
|
||||
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
|
||||
is_global]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
FileTransferState *state = nullptr;
|
||||
@@ -191,11 +166,7 @@ void FileTransferManager::Start(
|
||||
|
||||
const int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, peer_events, file_label, remote_id, props_locked](const char *buffer, size_t size) {
|
||||
// Close drains each in-flight send before detaching the peer.
|
||||
if (props_locked && props_locked->closing_) return -1;
|
||||
auto callback = peer_events->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
[peer, file_label, remote_id](const char *buffer, size_t size) {
|
||||
if (remote_id.empty()) {
|
||||
return SendReliableDataFrame(peer, buffer, size,
|
||||
file_label.c_str());
|
||||
@@ -229,13 +200,12 @@ void FileTransferManager::Start(
|
||||
}
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
if (peer_events->IsActive()) ProcessQueue(props_locked);
|
||||
}));
|
||||
ProcessQueue(props_locked);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props && props->closing_) return;
|
||||
FileTransferState &state = state_for(props);
|
||||
if (state.file_sending_.load()) {
|
||||
return;
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -24,7 +21,6 @@ public:
|
||||
using RemoteSession = gui_detail::RemoteSession;
|
||||
|
||||
explicit FileTransferManager(GuiRuntime &owner);
|
||||
~FileTransferManager();
|
||||
|
||||
FileTransferState &global_state();
|
||||
FileTransferState &
|
||||
@@ -44,9 +40,6 @@ private:
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void Unregister(uint32_t file_id, bool per_peer);
|
||||
|
||||
std::mutex workers_mutex_;
|
||||
std::vector<std::future<void>> workers_;
|
||||
bool stopping_ = false;
|
||||
GuiRuntime &owner_;
|
||||
FileTransferState global_state_;
|
||||
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
|
||||
|
||||
@@ -150,7 +150,6 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
|
||||
props_it->second->connection_status_.load() !=
|
||||
@@ -190,7 +189,6 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
const std::string target_id = owner_.controlled_remote_id_.empty()
|
||||
? owner_.focused_remote_id_
|
||||
: owner_.controlled_remote_id_;
|
||||
std::shared_lock sessions_lock(owner_.remote_sessions_mutex_);
|
||||
const auto props_it = owner_.remote_sessions_.find(target_id);
|
||||
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
|
||||
props_it->second->connection_status_.load() ==
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _RENDER_H_
|
||||
#define _RENDER_H_
|
||||
#ifndef CROSSDESK_GUI_RENDER_H_
|
||||
#define CROSSDESK_GUI_RENDER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -31,4 +25,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_RENDER_H_
|
||||
|
||||
@@ -119,9 +119,21 @@ struct SlintVideoPresenter::Impl {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||
if (state == slint::RenderingState::AfterRendering && stream &&
|
||||
opengl_renderer &&
|
||||
(*stream)->get_native_video_enabled() &&
|
||||
opengl_renderer->ShouldContinueRendering()) {
|
||||
// Queue the next frame directly from the display callback. A
|
||||
// 16/17 ms polling timer can drift across vsync and make Windows
|
||||
// coalesce every second request, effectively capping video near
|
||||
// 30 fps even when decode and submission stay near 60 fps.
|
||||
(*stream)->window().request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
if (state == slint::RenderingState::RenderingSetup) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_VIDEO_PRESENTER_H_
|
||||
#define _SLINT_VIDEO_PRESENTER_H_
|
||||
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -78,4 +72,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
@@ -14,39 +13,42 @@
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/device_presence_request.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
|
||||
constexpr auto kPresenceRefreshInterval = std::chrono::seconds(30);
|
||||
constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
|
||||
} // namespace
|
||||
|
||||
void GuiRuntime::HandleConnectionStatusChange() {
|
||||
if (!signal_connected_ || !peer_) {
|
||||
return;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_presence_request_attempt_at_ < kPresenceRetryInterval) {
|
||||
return;
|
||||
}
|
||||
// Consume the flag before sending so a concurrent reconnect cannot lose its
|
||||
// refresh request. Keep it pending while disconnected or after a send error.
|
||||
const bool requested = need_to_send_recent_connections_.exchange(false);
|
||||
if (!requested && now < next_presence_refresh_at_) {
|
||||
return;
|
||||
}
|
||||
last_presence_request_attempt_at_ = now;
|
||||
const auto message =
|
||||
BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true).dump();
|
||||
if (SendSignalMessage(peer_, message.data(), message.size()) == 0) {
|
||||
next_presence_refresh_at_ = now + kPresenceRefreshInterval;
|
||||
} else {
|
||||
need_to_send_recent_connections_ = true;
|
||||
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
||||
if (!recent_connection_ids_.empty()) {
|
||||
nlohmann::json j;
|
||||
j["type"] = "recent_connections_presence";
|
||||
j["user_id"] = client_id_;
|
||||
j["devices"] = nlohmann::json::array();
|
||||
for (const auto& id : recent_connection_ids_) {
|
||||
std::string pure_id = id;
|
||||
size_t pos_y = pure_id.find('Y');
|
||||
size_t pos_n = pure_id.find('N');
|
||||
size_t pos = std::string::npos;
|
||||
if (pos_y != std::string::npos &&
|
||||
(pos_n == std::string::npos || pos_y < pos_n)) {
|
||||
pos = pos_y;
|
||||
} else if (pos_n != std::string::npos) {
|
||||
pos = pos_n;
|
||||
}
|
||||
if (pos != std::string::npos) {
|
||||
pure_id = pure_id.substr(0, pos);
|
||||
}
|
||||
j["devices"].push_back(pure_id);
|
||||
}
|
||||
auto s = j.dump();
|
||||
SendSignalMessage(peer_, s.data(), s.size());
|
||||
}
|
||||
}
|
||||
need_to_send_recent_connections_ = false;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandlePendingPresenceProbe() {
|
||||
@@ -168,11 +170,14 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
||||
#else
|
||||
start_screen_capturer_ = false;
|
||||
#endif
|
||||
devices_.StopSpeakerCapturer();
|
||||
start_speaker_capturer_ = false;
|
||||
start_mouse_controller_ = false;
|
||||
start_keyboard_capturer_ = false;
|
||||
remote_client_id_.clear();
|
||||
audio_capture_ = false;
|
||||
if (audio_capture_) {
|
||||
devices_.StopSpeakerCapturer();
|
||||
audio_capture_ = false;
|
||||
}
|
||||
devices_.ResetToInitialDisplay();
|
||||
}
|
||||
|
||||
@@ -194,9 +199,11 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||
pending_presence_remember_password_ = remember_password;
|
||||
}
|
||||
|
||||
const auto s = BuildDevicePresenceRequest(
|
||||
client_id_, recent_connection_ids_, false, remote_id)
|
||||
.dump();
|
||||
nlohmann::json j;
|
||||
j["type"] = "recent_connections_presence";
|
||||
j["user_id"] = client_id_;
|
||||
j["devices"] = nlohmann::json::array({remote_id});
|
||||
auto s = j.dump();
|
||||
|
||||
int ret = SendSignalMessage(peer_, s.data(), s.size());
|
||||
if (ret != 0) {
|
||||
@@ -213,11 +220,6 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
if (!props || props->closing_.exchange(true)) return;
|
||||
if (props->peer_events_) props->peer_events_->Deactivate();
|
||||
props->connection_established_ = false;
|
||||
props->streaming_ = false;
|
||||
props->connection_status_.store(ConnectionStatus::Closed);
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
@@ -243,161 +245,85 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
}
|
||||
|
||||
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
|
||||
video_height > 0) {
|
||||
std::vector<unsigned char> buffer_copy(*frame_snapshot);
|
||||
std::string remote_id = props->remote_id_;
|
||||
std::string remote_host_name = props->remote_host_name_;
|
||||
std::string password =
|
||||
props->remember_password_ ? props->remote_password_ : "";
|
||||
|
||||
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
||||
remote_host_name, password,
|
||||
thumbnail = thumbnail_]() {
|
||||
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
|
||||
video_height, remote_id, remote_host_name,
|
||||
password);
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
thumbnail_save_threads_.emplace_back(std::move(save_thread));
|
||||
}
|
||||
}
|
||||
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(props->remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
PeerPtr* peer = nullptr;
|
||||
{
|
||||
// Audio/clipboard senders hold a shared map lock while using this pointer.
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(props->peer_, nullptr);
|
||||
if (props->peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
|
||||
LeaveConnection(props->peer_, props->remote_id_.c_str());
|
||||
LOG_INFO("Destroy peer [{}]", props->local_id_);
|
||||
DestroyPeer(&props->peer_);
|
||||
}
|
||||
const auto queued_at = std::chrono::steady_clock::now();
|
||||
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
|
||||
[props, peer, frame_snapshot, video_width, video_height,
|
||||
thumbnail = thumbnail_, queued_at]() mutable {
|
||||
if (peer) {
|
||||
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
|
||||
props->remote_id_);
|
||||
LeaveConnection(peer, props->remote_id_.c_str());
|
||||
DestroyPeer(&peer);
|
||||
}
|
||||
if (thumbnail && frame_snapshot && !frame_snapshot->empty() &&
|
||||
video_width > 0 && video_height > 0) {
|
||||
thumbnail->SaveToThumbnail(
|
||||
reinterpret_cast<char*>(frame_snapshot->data()), video_width,
|
||||
video_height, props->remote_id_, props->remote_host_name_,
|
||||
props->remember_password_ ? props->remote_password_ : "");
|
||||
}
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - queued_at).count();
|
||||
LOG_INFO("[{}] Session cleanup completed in {} ms", props->remote_id_,
|
||||
elapsed);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseServerController(const std::string& remote_id) {
|
||||
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
|
||||
return;
|
||||
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
|
||||
[peer = peer_, remote_id] { LeaveConnection(peer, remote_id.c_str()); });
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseConnectionPeer() {
|
||||
if (!peer_) return;
|
||||
peer_events_->Deactivate();
|
||||
std::vector<std::string> controllers;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
|
||||
}
|
||||
for (const auto& id : controllers)
|
||||
HandleServerControllerDisconnected(id, "peer_closed");
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
signal_connected_ = false;
|
||||
device_presence_cache_.SetSignalConnected(false);
|
||||
signal_status_ = SignalStatus::SignalClosed;
|
||||
PeerPtr* peer;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer = std::exchange(peer_, nullptr);
|
||||
}
|
||||
// The same serial queue also owns outstanding controller disconnects, so
|
||||
// these finish before the shared peer is destroyed.
|
||||
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
|
||||
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
|
||||
LOG_INFO("[{}] Background connection peer cleanup", id);
|
||||
LeaveConnection(peer, id.c_str());
|
||||
DestroyPeer(&peer);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseAllRemoteSessions() {
|
||||
CloseConnectionPeer();
|
||||
is_client_mode_ = false;
|
||||
pending_reconnects_.clear();
|
||||
std::vector<std::shared_ptr<RemoteSession>> sessions;
|
||||
{
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (const auto& [id, props] : remote_sessions_) sessions.push_back(props);
|
||||
if (peer_) {
|
||||
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
|
||||
LeaveConnection(peer_, client_id_);
|
||||
is_client_mode_ = false;
|
||||
devices_.StopMouseController();
|
||||
devices_.StopScreenCapturer();
|
||||
devices_.StopSpeakerCapturer();
|
||||
devices_.StopKeyboardCapturer();
|
||||
LOG_INFO("Destroy peer [{}]", client_id_);
|
||||
DestroyPeer(&peer_);
|
||||
}
|
||||
for (const auto& props : sessions) CloseRemoteSession(props);
|
||||
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto& it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
CloseRemoteSession(props);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// std::unique_lock lock(remote_sessions_mutex_);
|
||||
remote_sessions_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleSessionCleanup() {
|
||||
auto completed = [](std::future<void>& task) {
|
||||
if (!task.valid() || task.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) return false;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
return true;
|
||||
};
|
||||
completed(connection_peer_cleanup_);
|
||||
for (auto it = controller_cleanup_tasks_.begin();
|
||||
it != controller_cleanup_tasks_.end();) {
|
||||
if (completed(it->second)) it = controller_cleanup_tasks_.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
std::vector<std::pair<std::string, PendingReconnect>> reconnects;
|
||||
for (auto it = session_cleanup_tasks_.begin();
|
||||
it != session_cleanup_tasks_.end();) {
|
||||
if (it->second.wait_for(std::chrono::seconds(0)) !=
|
||||
std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
it->second.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", it->first, error.what());
|
||||
}
|
||||
if (auto pending = pending_reconnects_.find(it->first);
|
||||
pending != pending_reconnects_.end()) {
|
||||
reconnects.emplace_back(pending->first, std::move(pending->second));
|
||||
pending_reconnects_.erase(pending);
|
||||
}
|
||||
it = session_cleanup_tasks_.erase(it);
|
||||
reload_recent_connections_ = true;
|
||||
recent_connection_image_save_time_ = 0;
|
||||
}
|
||||
for (const auto& [id, request] : reconnects) {
|
||||
ConnectTo(id, request.password.c_str(), request.remember_password);
|
||||
}
|
||||
}
|
||||
void GuiRuntime::WaitForThumbnailSaveTasks() {
|
||||
std::vector<std::thread> threads_to_join;
|
||||
|
||||
void GuiRuntime::WaitForSessionCleanup() {
|
||||
pending_reconnects_.clear();
|
||||
auto drain = [](std::future<void>& task) {
|
||||
if (!task.valid()) return;
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Connection cleanup failed: {}", error.what());
|
||||
}
|
||||
};
|
||||
drain(connection_peer_cleanup_);
|
||||
for (auto& [id, task] : controller_cleanup_tasks_) drain(task);
|
||||
controller_cleanup_tasks_.clear();
|
||||
for (auto& [id, task] : session_cleanup_tasks_) {
|
||||
try {
|
||||
task.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("[{}] Session cleanup failed: {}", id, error.what());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
|
||||
threads_to_join.swap(thumbnail_save_threads_);
|
||||
}
|
||||
|
||||
if (threads_to_join.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& thread : threads_to_join) {
|
||||
if (thread.joinable()) {
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
session_cleanup_tasks_.clear();
|
||||
}
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
@@ -439,11 +365,6 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||
|
||||
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (session_cleanup_tasks_.count(remote_id)) {
|
||||
pending_reconnects_[remote_id] = {password ? password : "", remember_password};
|
||||
LOG_INFO("[{}] Reconnect queued until previous peer is destroyed", remote_id);
|
||||
return 0;
|
||||
}
|
||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
@@ -461,14 +382,16 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
bool exists = FindRemoteSession(remote_id) != nullptr;
|
||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||
// shared_lock.unlock();
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr* peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
|
||||
auto props = remote_sessions_[remote_id];
|
||||
@@ -476,8 +399,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
props->params_.user_data = props->peer_events_.get();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
@@ -524,8 +445,8 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
|
||||
int ret = -1;
|
||||
auto props = FindRemoteSession(remote_id);
|
||||
if (!props || props->closing_) return -1;
|
||||
// std::shared_lock read_lock(remote_sessions_mutex_);
|
||||
auto props = remote_sessions_[remote_id];
|
||||
if (!props->connection_established_) {
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
@@ -549,6 +470,7 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
}
|
||||
}
|
||||
}
|
||||
// read_lock.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CURSOR_POSITION_H_
|
||||
#define _CURSOR_POSITION_H_
|
||||
|
||||
@@ -70,4 +64,4 @@ inline bool NormalizeCursorPosition(
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CURSOR_STATE_PROVIDER_H_
|
||||
#define _CURSOR_STATE_PROVIDER_H_
|
||||
|
||||
@@ -36,4 +30,4 @@ class CursorStateProvider {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* @Date: 2026-02-28
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_PRESENCE_CACHE_H_
|
||||
#define _DEVICE_PRESENCE_CACHE_H_
|
||||
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -16,29 +15,15 @@ namespace crossdesk {
|
||||
|
||||
class DevicePresenceCache {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static constexpr auto kMaxAge = std::chrono::seconds(60);
|
||||
|
||||
void SetSignalConnected(bool connected) {
|
||||
void SetOnline(const std::string &device_id, bool online) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
signal_connected_ = connected;
|
||||
cache_.clear();
|
||||
cache_[device_id] = online;
|
||||
}
|
||||
|
||||
void SetOnline(const std::string &device_id, bool online,
|
||||
Clock::time_point now = Clock::now()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (signal_connected_) {
|
||||
cache_[device_id] = {online, now};
|
||||
}
|
||||
}
|
||||
|
||||
bool IsOnline(const std::string &device_id,
|
||||
Clock::time_point now = Clock::now()) const {
|
||||
bool IsOnline(const std::string &device_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto it = cache_.find(device_id);
|
||||
return signal_connected_ && it != cache_.end() && it->second.online &&
|
||||
now - it->second.updated_at < kMaxAge;
|
||||
return it != cache_.end() && it->second;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
@@ -47,15 +32,10 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
bool online;
|
||||
Clock::time_point updated_at;
|
||||
};
|
||||
bool signal_connected_ = false;
|
||||
std::unordered_map<std::string, Entry> cache_;
|
||||
std::unordered_map<std::string, bool> cache_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_PRESENCE_REQUEST_H_
|
||||
#define _DEVICE_PRESENCE_REQUEST_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline nlohmann::json BuildDevicePresenceRequest(
|
||||
const std::string& client_id, const std::vector<std::string>& recent_ids,
|
||||
bool subscribe, const std::string& probe_id = {}) {
|
||||
std::vector<std::string> devices;
|
||||
for (const auto& id : recent_ids) {
|
||||
// Recent connection keys contain Y/N plus display settings after the ID.
|
||||
const std::string pure_id = id.substr(0, id.find_first_of("YN"));
|
||||
if (!pure_id.empty() &&
|
||||
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
|
||||
devices.push_back(pure_id);
|
||||
}
|
||||
}
|
||||
if (!probe_id.empty() &&
|
||||
std::find(devices.begin(), devices.end(), probe_id) == devices.end()) {
|
||||
devices.push_back(probe_id);
|
||||
}
|
||||
// Include recent devices even in a one-shot probe: older servers ignore
|
||||
// subscribe=false and replace their subscriptions with this device list.
|
||||
return {{"type", "recent_connections_presence"},
|
||||
{"user_id", client_id},
|
||||
{"devices", devices},
|
||||
{"subscribe", subscribe}};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -19,25 +19,21 @@ namespace crossdesk {
|
||||
|
||||
GuiRuntime::GuiRuntime()
|
||||
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
|
||||
keyboard_(*this) {}
|
||||
keyboard_(*this), peer_events_(*this) {}
|
||||
|
||||
GuiRuntime::~GuiRuntime() { WaitForSessionCleanup(); }
|
||||
GuiRuntime::~GuiRuntime() = default;
|
||||
|
||||
int GuiRuntime::CreateConnectionPeer() {
|
||||
// Tick retries after the old peer's leave notification and socket shutdown.
|
||||
if (connection_peer_cleanup_.valid()) return 0;
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_events_ = std::make_shared<PeerEventHandler>(*this);
|
||||
}
|
||||
params_.use_cfg_file = false;
|
||||
|
||||
std::string signal_server_ip;
|
||||
int signal_server_port;
|
||||
int coturn_server_port;
|
||||
|
||||
if (config_center_->IsSelfHosted()) {
|
||||
signal_server_ip = config_center_->GetSignalServerHost();
|
||||
signal_server_port = config_center_->GetSignalServerPort();
|
||||
coturn_server_port = config_center_->GetCoturnServerPort();
|
||||
|
||||
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
|
||||
const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity();
|
||||
@@ -62,6 +58,7 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
} else {
|
||||
signal_server_ip = config_center_->GetDefaultServerHost();
|
||||
signal_server_port = config_center_->GetDefaultSignalServerPort();
|
||||
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
|
||||
settings_.ActivateCachedPublicIdentity();
|
||||
params_.user_id = client_id_with_password_;
|
||||
}
|
||||
@@ -112,19 +109,41 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
} else {
|
||||
signal_server_port_self_[0] = '\0';
|
||||
}
|
||||
int coturn_port = config_center_->GetCoturnServerPort();
|
||||
if (coturn_port > 0) {
|
||||
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
|
||||
sizeof(coturn_server_port_self_) - 1);
|
||||
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
|
||||
} else {
|
||||
coturn_server_port_self_[0] = '\0';
|
||||
}
|
||||
|
||||
// peer config
|
||||
strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.signal_server_ip) - 1);
|
||||
params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0';
|
||||
params_.signal_server_port = signal_server_port;
|
||||
strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.stun_server_ip) - 1);
|
||||
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
|
||||
params_.stun_server_port = coturn_server_port;
|
||||
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
|
||||
sizeof(params_.turn_server_ip) - 1);
|
||||
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
|
||||
params_.turn_server_port = coturn_server_port;
|
||||
// TURN credentials are issued by the signaling server after login. Keep the
|
||||
// initial values empty so a reusable static password is never embedded in
|
||||
// the client binary.
|
||||
params_.turn_server_username[0] = '\0';
|
||||
params_.turn_server_password[0] = '\0';
|
||||
|
||||
strncpy(params_.log_path, dll_log_path_.c_str(),
|
||||
sizeof(params_.log_path) - 1);
|
||||
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
|
||||
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
|
||||
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
|
||||
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
|
||||
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
|
||||
// platforms fall back to a packed CPU copy when native upload is unavailable.
|
||||
#if defined(_WIN32)
|
||||
// Windows accepts ref-counted CPU NV12 and CUDA device NV12. The OpenGL
|
||||
// renderer retains the decoded frame until the UI thread has uploaded it.
|
||||
params_.native_video_output = true;
|
||||
#else
|
||||
params_.native_video_output = false;
|
||||
@@ -169,19 +188,14 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||
|
||||
params_.user_data = peer_events_.get();
|
||||
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;
|
||||
device_presence_cache_.SetSignalConnected(false);
|
||||
need_to_send_recent_connections_ = true;
|
||||
signal_status_ = SignalStatus::SignalConnecting;
|
||||
|
||||
{
|
||||
std::unique_lock lock(remote_sessions_mutex_);
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
}
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
Init(peer_);
|
||||
@@ -225,7 +239,7 @@ void GuiRuntime::UpdateLabels() {
|
||||
|
||||
|
||||
void GuiRuntime::HandleRecentConnections() {
|
||||
if (session_cleanup_tasks_.empty() && reload_recent_connections_ && thumbnail_) {
|
||||
if (reload_recent_connections_ && thumbnail_) {
|
||||
uint32_t now_time = SDL_GetTicks();
|
||||
if (now_time - recent_connection_image_save_time_ >= 50) {
|
||||
int ret = thumbnail_->LoadThumbnail(recent_connections_,
|
||||
@@ -254,22 +268,11 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
|
||||
if (1) {
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
uint64_t captured_timestamp = 0;
|
||||
for (const auto &it : runtime->remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
if (props->peer_) {
|
||||
if (captured_timestamp == 0) {
|
||||
const int64_t timestamp_us = GetSystemTimeMicros(props->peer_);
|
||||
if (timestamp_us > 0) {
|
||||
captured_timestamp = static_cast<uint64_t>(timestamp_us);
|
||||
}
|
||||
}
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char *>(stream);
|
||||
frame.size = static_cast<size_t>(len);
|
||||
frame.captured_timestamp = captured_timestamp;
|
||||
SendAudioFrame(props->peer_, &frame,
|
||||
SendAudioFrame(props->peer_, (const char *)stream, len,
|
||||
runtime->audio_label_.c_str());
|
||||
}
|
||||
}
|
||||
@@ -286,6 +289,29 @@ void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
|
||||
[[maybe_unused]] Uint8 *stream,
|
||||
[[maybe_unused]] int len) {
|
||||
// GuiApplication *runtime = (GuiApplication *)userdata;
|
||||
// for (auto it : runtime->remote_sessions_) {
|
||||
// auto props = it.second;
|
||||
// if (props->connection_status_ == SignalStatus::SignalConnected) {
|
||||
// SendAudioFrame(props->peer_, (const char *)stream, len);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!runtime->audio_buffer_fresh_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// SDL_memset(stream, 0, len);
|
||||
|
||||
// if (runtime->audio_len_ == 0) {
|
||||
// return;
|
||||
// } else {
|
||||
// }
|
||||
|
||||
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
|
||||
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
|
||||
// SDL_MIX_MAXVOLUME);
|
||||
// runtime->audio_buffer_fresh_ = false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_RUNTIME_H_
|
||||
#define _GUI_RUNTIME_H_
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
@@ -61,11 +55,8 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
void CloseConnectionPeer();
|
||||
void CloseServerController(const std::string& remote_id);
|
||||
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
|
||||
void HandleSessionCleanup();
|
||||
void WaitForSessionCleanup();
|
||||
void WaitForThumbnailSaveTasks();
|
||||
std::shared_ptr<RemoteSession> FindRemoteSession(
|
||||
const std::string& remote_id);
|
||||
|
||||
@@ -92,7 +83,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
FileTransferManager transfers_;
|
||||
SettingsManager settings_;
|
||||
KeyboardController keyboard_;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
PeerEventHandler peer_events_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||
|
||||
@@ -107,4 +98,4 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
* Aggregate state visible to GuiRuntime and GuiApplication.
|
||||
*
|
||||
* Definitions live beside their owners: SDL/window state in application and
|
||||
* connection/session state in runtime. This header is intentionally only the
|
||||
* composition point.
|
||||
*/
|
||||
|
||||
#ifndef _GUI_STATE_H_
|
||||
#define _GUI_STATE_H_
|
||||
#ifndef CROSSDESK_GUI_STATE_H_
|
||||
#define CROSSDESK_GUI_STATE_H_
|
||||
|
||||
#include "application/application_state.h"
|
||||
#include "runtime/runtime_state.h"
|
||||
@@ -16,4 +18,4 @@ struct GuiState : ApplicationState, RuntimeState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_STATE_H_
|
||||
|
||||
@@ -79,9 +79,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size, void *user_data) {
|
||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -148,8 +145,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
|
||||
auto props_it = runtime->remote_sessions_.find(remote_id);
|
||||
if (props_it != runtime->remote_sessions_.end()) {
|
||||
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -298,9 +296,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
runtime->devices_.SendMouseCommand(remote_action,
|
||||
runtime->selected_display_);
|
||||
} else if (remote_action.type == ControlType::audio_capture) {
|
||||
if (remote_action.a)
|
||||
if (remote_action.a && !runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StartSpeakerCapturer();
|
||||
else
|
||||
else if (!remote_action.a && runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StopSpeakerCapturer();
|
||||
} else if (remote_action.type == ControlType::keyboard) {
|
||||
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
|
||||
|
||||
@@ -60,9 +60,6 @@ PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
|
||||
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime || !message || size == 0) {
|
||||
return;
|
||||
@@ -156,9 +153,6 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -167,8 +161,6 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
std::string client_id(user_id, user_id_size);
|
||||
if (client_id == runtime->client_id_) {
|
||||
runtime->signal_status_ = status;
|
||||
runtime->device_presence_cache_.SetSignalConnected(
|
||||
status == SignalStatus::SignalConnected);
|
||||
if (SignalStatus::SignalConnecting == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalConnected == status) {
|
||||
@@ -206,8 +198,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||
}
|
||||
|
||||
std::string remote_id(client_id.begin() + 2, client_id.end());
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
props->signal_status_ = status;
|
||||
if (SignalStatus::SignalConnecting == status) {
|
||||
props->signal_connected_ = false;
|
||||
@@ -234,9 +230,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
const size_t user_id_size,
|
||||
void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) return;
|
||||
|
||||
@@ -311,7 +304,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
|
||||
if (!runtime->need_to_create_stream_window_) {
|
||||
if (!runtime->need_to_create_stream_window_ &&
|
||||
!runtime->remote_sessions_.empty()) {
|
||||
runtime->need_to_create_stream_window_ = true;
|
||||
}
|
||||
props->connection_established_ = true;
|
||||
@@ -468,7 +462,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
std::memory_order_release);
|
||||
runtime->is_server_mode_ = true;
|
||||
runtime->start_screen_capturer_ = true;
|
||||
runtime->devices_.StartSpeakerCapturer();
|
||||
runtime->start_speaker_capturer_ = true;
|
||||
runtime->remote_client_id_ = remote_id;
|
||||
runtime->start_mouse_controller_ = true;
|
||||
{
|
||||
@@ -499,12 +493,9 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
|
||||
void PeerEventHandler::OnNetStatusReport(
|
||||
const char* client_id, size_t client_id_size, TraversalMode mode,
|
||||
const MiniRtcNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||
const XNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||
const size_t user_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
@@ -565,8 +556,12 @@ void PeerEventHandler::OnNetStatusReport(
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
if (props->traversal_mode_ != mode) {
|
||||
props->traversal_mode_ = mode;
|
||||
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PEER_EVENT_HANDLER_H_
|
||||
#define _PEER_EVENT_HANDLER_H_
|
||||
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -22,11 +15,7 @@ class PeerEventHandler {
|
||||
public:
|
||||
explicit PeerEventHandler(GuiRuntime &owner);
|
||||
|
||||
bool IsActive() const { return callbacks_.IsActive(); }
|
||||
auto EnterCallback() { return callbacks_.Enter(); }
|
||||
void Deactivate() { callbacks_.Close(); }
|
||||
|
||||
static void OnReceiveVideoBuffer(const MiniRtcVideoFrame *video_frame,
|
||||
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
|
||||
const char *user_id, size_t user_id_size,
|
||||
const char *src_id, size_t src_id_size,
|
||||
void *user_data);
|
||||
@@ -46,7 +35,7 @@ public:
|
||||
size_t user_id_size, void *user_data);
|
||||
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const MiniRtcNetTrafficStats *net_traffic_stats,
|
||||
const XNetTrafficStats *net_traffic_stats,
|
||||
const char *user_id, size_t user_id_size,
|
||||
void *user_data);
|
||||
|
||||
@@ -54,9 +43,8 @@ private:
|
||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
gui_detail::SessionCallbackGate callbacks_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
|
||||
@@ -14,35 +14,42 @@ namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
const MiniRtcNativeVideoFrame* GetNativeVideoFrame(const MiniRtcVideoFrame* frame) {
|
||||
if (!frame || !frame->native_frame) {
|
||||
#if defined(_WIN32)
|
||||
const XWindowsVideoFrame* GetWindowsNativeFrame(const XVideoFrame* frame) {
|
||||
if (!frame ||
|
||||
frame->native_handle_type != XVideoFrameNativeHandleWindowsNv12 ||
|
||||
!frame->native_handle) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto* native = frame->native_frame;
|
||||
const auto* native =
|
||||
static_cast<const XWindowsVideoFrame*>(frame->native_handle);
|
||||
return native->struct_size >=
|
||||
static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) &&
|
||||
native->owner && native->copy_to_nv12
|
||||
static_cast<uint32_t>(sizeof(XWindowsVideoFrame)) &&
|
||||
native->owner && native->copy_to_cpu
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
const MiniRtcVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||
const XVideoFrame* video_frame, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
auto props = runtime->FindRemoteSession(remote_id);
|
||||
if (!props) return;
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
GuiRuntime::RemoteSession* props =
|
||||
runtime->remote_sessions_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
bool background_snapshot_only = false;
|
||||
@@ -50,10 +57,12 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
VideoRenderer::SubmitResult submit_result =
|
||||
VideoRenderer::SubmitResult::failed;
|
||||
if (GetNativeVideoFrame(video_frame)) {
|
||||
submit_result = native_renderer->SubmitNativeFrame(
|
||||
remote_id, *video_frame->native_frame);
|
||||
#if defined(_WIN32)
|
||||
if (GetWindowsNativeFrame(video_frame)) {
|
||||
submit_result =
|
||||
native_renderer->SubmitNativeFrame(remote_id, *video_frame);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
@@ -107,12 +116,14 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
}
|
||||
|
||||
size_t frame_size = video_frame->size;
|
||||
const MiniRtcNativeVideoFrame* native_frame =
|
||||
GetNativeVideoFrame(video_frame);
|
||||
if (native_frame) {
|
||||
frame_size = static_cast<size_t>(native_frame->width) *
|
||||
native_frame->height * 3U / 2U;
|
||||
#if defined(_WIN32)
|
||||
const XWindowsVideoFrame* windows_native_frame =
|
||||
GetWindowsNativeFrame(video_frame);
|
||||
if (windows_native_frame) {
|
||||
frame_size = static_cast<size_t>(windows_native_frame->width) *
|
||||
windows_native_frame->height * 3U / 2U;
|
||||
}
|
||||
#endif
|
||||
if (frame_size == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -126,13 +137,15 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->back_frame_->resize(frame_size);
|
||||
}
|
||||
|
||||
if (native_frame) {
|
||||
if (native_frame->copy_to_nv12(
|
||||
native_frame->owner, props->back_frame_->data(),
|
||||
#if defined(_WIN32)
|
||||
if (windows_native_frame) {
|
||||
if (windows_native_frame->copy_to_cpu(
|
||||
windows_native_frame->owner, props->back_frame_->data(),
|
||||
props->back_frame_->size()) != 0) {
|
||||
return;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (!video_frame->data || video_frame->size < frame_size) {
|
||||
return;
|
||||
@@ -172,9 +185,6 @@ void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
const char* data, size_t size, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
if (!handler) return;
|
||||
auto callback = handler->EnterCallback();
|
||||
if (!callback) return;
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_ACTION_CODEC_H_
|
||||
#define _REMOTE_ACTION_CODEC_H_
|
||||
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
@@ -20,4 +14,4 @@ void Free(RemoteAction &action);
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _REMOTE_SESSION_H_
|
||||
#define _REMOTE_SESSION_H_
|
||||
|
||||
@@ -24,8 +18,6 @@
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk { class PeerEventHandler; }
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct FileTransferState {
|
||||
@@ -69,8 +61,6 @@ struct FileTransferState {
|
||||
struct RemoteSession {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||
std::atomic<bool> closing_{false};
|
||||
std::string audio_label_ = kAudioStream;
|
||||
std::string data_label_ = kDataStream;
|
||||
std::string mouse_label_ = kMouseStream;
|
||||
@@ -172,7 +162,7 @@ struct RemoteSession {
|
||||
int fps_ = 0;
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
MiniRtcNetTrafficStats net_traffic_stats_{};
|
||||
XNetTrafficStats net_traffic_stats_{};
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
@@ -184,4 +174,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _RUNTIME_STATE_H_
|
||||
#define _RUNTIME_STATE_H_
|
||||
|
||||
@@ -22,11 +16,9 @@
|
||||
#include <stream_names.h>
|
||||
|
||||
#include "config_center.h"
|
||||
#include "common/task_queue_lock_free.h"
|
||||
#include "path_manager.h"
|
||||
#include "runtime/device_presence_cache.h"
|
||||
#include "runtime/remote_session.h"
|
||||
#include "runtime/session_lifecycle.h"
|
||||
#include "thumbnail.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
@@ -55,15 +47,13 @@ struct RecentConnectionsState {
|
||||
int recent_connection_image_height_ = 90;
|
||||
uint32_t recent_connection_image_save_time_ = 0;
|
||||
DevicePresenceCache device_presence_cache_;
|
||||
std::atomic<bool> need_to_send_recent_connections_{true};
|
||||
std::chrono::steady_clock::time_point next_presence_refresh_at_{};
|
||||
std::chrono::steady_clock::time_point last_presence_request_attempt_at_{};
|
||||
bool need_to_send_recent_connections_ = true;
|
||||
};
|
||||
|
||||
struct PeerState {
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
std::string signal_status_str_;
|
||||
std::atomic<bool> signal_connected_{false};
|
||||
bool signal_connected_ = false;
|
||||
PeerPtr *peer_ = nullptr;
|
||||
PeerPtr *peer_reserved_ = nullptr;
|
||||
std::string video_primary_label_ = "primary_display";
|
||||
@@ -139,6 +129,7 @@ struct UserSettingsState {
|
||||
bool enable_srtp_ = false;
|
||||
char signal_server_ip_[256] = "api.crossdesk.cn";
|
||||
char signal_server_port_[6] = "9099";
|
||||
char coturn_server_port_[6] = "3478";
|
||||
bool enable_self_hosted_ = false;
|
||||
int language_button_value_last_ = 0;
|
||||
int video_quality_button_value_last_ = 0;
|
||||
@@ -157,6 +148,7 @@ struct UserSettingsState {
|
||||
std::string file_transfer_save_path_last_;
|
||||
char signal_server_ip_self_[256] = "";
|
||||
char signal_server_port_self_[6] = "";
|
||||
char coturn_server_port_self_[6] = "";
|
||||
bool settings_window_pos_reset_ = true;
|
||||
bool self_hosted_server_config_window_pos_reset_ = true;
|
||||
std::string selected_current_file_path_;
|
||||
@@ -186,15 +178,8 @@ struct ConnectionState {
|
||||
std::unordered_map<std::string, RemoteSessionPtr>;
|
||||
RemoteSessionMap remote_sessions_;
|
||||
std::shared_mutex remote_sessions_mutex_;
|
||||
TaskQueueLockFree session_cleanup_queue_;
|
||||
std::unordered_map<std::string, std::future<void>> session_cleanup_tasks_;
|
||||
std::future<void> connection_peer_cleanup_;
|
||||
std::unordered_map<std::string, std::future<void>> controller_cleanup_tasks_;
|
||||
struct PendingReconnect {
|
||||
std::string password;
|
||||
bool remember_password = false;
|
||||
};
|
||||
std::unordered_map<std::string, PendingReconnect> pending_reconnects_;
|
||||
std::vector<std::thread> thumbnail_save_threads_;
|
||||
std::mutex thumbnail_save_threads_mutex_;
|
||||
|
||||
std::shared_mutex connection_status_mutex_;
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "runtime/session_lifecycle.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
CallbackGuard::CallbackGuard(SessionCallbackGate& callbacks)
|
||||
: callbacks_(callbacks), accepted_(callbacks.BeginCallback()) {}
|
||||
|
||||
CallbackGuard::~CallbackGuard() {
|
||||
if (accepted_) {
|
||||
callbacks_.EndCallback();
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionCallbackGate::IsActive() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return accepting_callbacks_;
|
||||
}
|
||||
|
||||
CallbackGuard SessionCallbackGate::Enter() {
|
||||
return CallbackGuard(*this);
|
||||
}
|
||||
|
||||
bool SessionCallbackGate::BeginCallback() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!accepting_callbacks_) {
|
||||
return false;
|
||||
}
|
||||
++running_callbacks_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionCallbackGate::EndCallback() {
|
||||
std::lock_guard lock(mutex_);
|
||||
--running_callbacks_;
|
||||
if (running_callbacks_ == 0) {
|
||||
callbacks_finished_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void SessionCallbackGate::Close() {
|
||||
std::unique_lock lock(mutex_);
|
||||
accepting_callbacks_ = false;
|
||||
// Waiting releases the lock so running callbacks can call EndCallback().
|
||||
callbacks_finished_.wait(lock, [this] { return running_callbacks_ == 0; });
|
||||
}
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SESSION_LIFECYCLE_H_
|
||||
#define _SESSION_LIFECYCLE_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
class SessionCallbackGate;
|
||||
|
||||
// Decrements the count on scope exit, including early returns and exceptions.
|
||||
class CallbackGuard {
|
||||
public:
|
||||
explicit CallbackGuard(SessionCallbackGate& callbacks);
|
||||
~CallbackGuard();
|
||||
|
||||
CallbackGuard(const CallbackGuard&) = delete;
|
||||
CallbackGuard& operator=(const CallbackGuard&) = delete;
|
||||
|
||||
explicit operator bool() const { return accepted_; }
|
||||
|
||||
private:
|
||||
SessionCallbackGate& callbacks_;
|
||||
bool accepted_;
|
||||
};
|
||||
|
||||
// Each peer counts callbacks as they enter and leave.
|
||||
// Closing rejects new callbacks, then waits for running callbacks to finish.
|
||||
class SessionCallbackGate {
|
||||
public:
|
||||
bool IsActive() const;
|
||||
CallbackGuard Enter();
|
||||
|
||||
// Called by the session owner, never from within this session's callbacks.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
friend class CallbackGuard;
|
||||
|
||||
bool BeginCallback();
|
||||
void EndCallback();
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable callbacks_finished_;
|
||||
bool accepting_callbacks_ = true;
|
||||
std::size_t running_callbacks_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
@@ -54,6 +54,7 @@ export global UiStrings {
|
||||
in-out property <string> default-desktop: "Desktop";
|
||||
in-out property <string> server-host: "Server host";
|
||||
in-out property <string> server-port: "Signal port";
|
||||
in-out property <string> coturn-port: "TURN port";
|
||||
in-out property <string> version: "Version";
|
||||
in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
|
||||
in-out property <string> license: "Licensed under GNU GPL v3.";
|
||||
@@ -185,7 +186,6 @@ component ConnectionDialogButton inherits Rectangle {
|
||||
|
||||
export component MainWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: 640px;
|
||||
preferred-height: 450px + root.titlebar-height;
|
||||
min-width: 640px;
|
||||
@@ -210,11 +210,6 @@ export component MainWindow inherits Window {
|
||||
in property <bool> signal-tls-error: false;
|
||||
in property <[RecentConnection]> recent-connections;
|
||||
in property <bool> update-available: false;
|
||||
in property <bool> update-downloading: false;
|
||||
in property <bool> update-failed: false;
|
||||
in property <bool> update-security-blocked: false;
|
||||
in property <float> update-progress: 0;
|
||||
in property <string> update-status: "";
|
||||
in property <string> current-version: "";
|
||||
in property <string> latest-version: "";
|
||||
in property <string> release-name: "";
|
||||
@@ -254,6 +249,7 @@ export component MainWindow inherits Window {
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
in-out property <string> server-port: "";
|
||||
in-out property <string> coturn-port: "";
|
||||
in property <bool> settings-session-active: false;
|
||||
// Keep the setting visible on every platform. Unsupported builds expose it
|
||||
// as disabled so the fixed settings layout never turns into a blank row.
|
||||
@@ -273,8 +269,6 @@ export component MainWindow inherits Window {
|
||||
callback cancel-self-hosted-settings;
|
||||
callback browse-save-path;
|
||||
callback open-download;
|
||||
callback start-update;
|
||||
callback cancel-update;
|
||||
callback connection-cancel;
|
||||
callback connection-acknowledge;
|
||||
callback connection-submit-password(string, bool);
|
||||
@@ -1524,7 +1518,7 @@ export component MainWindow inherits Window {
|
||||
x: root.compact-language ? 190px : 140px;
|
||||
y: 105px;
|
||||
width: root.compact-language ? 260px : 360px;
|
||||
height: 138px;
|
||||
height: 168px;
|
||||
border-radius: 7px;
|
||||
|
||||
Rectangle {
|
||||
@@ -1564,8 +1558,21 @@ export component MainWindow inherits Window {
|
||||
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
||||
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
||||
}
|
||||
CompactButton { x: root.compact-language ? 93px : 135px; y: 100px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||
CompactButton { x: root.compact-language ? 134px : 176px; y: 100px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
Rectangle {
|
||||
x: root.compact-language ? 100px : 173px;
|
||||
y: 90px;
|
||||
width: root.compact-language ? 152px : 179px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 4px;
|
||||
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
|
||||
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
|
||||
}
|
||||
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1651,7 +1658,7 @@ export component MainWindow inherits Window {
|
||||
x: 30px;
|
||||
y: 105px;
|
||||
width: 360px;
|
||||
height: root.update-status != "" ? 136px : 190px;
|
||||
height: 190px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: #b9bdc3;
|
||||
@@ -1678,27 +1685,8 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
if root.update-status != "": Text {
|
||||
x: 30px; y: 247px; width: 340px; height: 40px;
|
||||
text: root.update-status;
|
||||
color: root.update-failed ? #b23b3b : #3d434d;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
wrap: word-wrap;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if root.update-downloading: Rectangle {
|
||||
x: 30px; y: 291px; width: 340px; height: 4px;
|
||||
background: #e2e4e7;
|
||||
clip: true;
|
||||
Rectangle {
|
||||
x: root.update-progress < 0 ? (parent.width + self.width) * (mod(animation-tick(), 1500ms) / 1500ms) - self.width : 0px;
|
||||
width: parent.width * (root.update-progress < 0 ? 0.25 : root.update-progress);
|
||||
height: parent.height;
|
||||
background: #334fd1;
|
||||
}
|
||||
}
|
||||
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading && !root.update-security-blocked; clicked => { root.start-update(); } }
|
||||
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
|
||||
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
|
||||
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export struct ControllerEntry {
|
||||
|
||||
export component ServerWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
preferred-height: 150px;
|
||||
min-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
|
||||
@@ -111,7 +111,6 @@ component ControlBarButton inherits Rectangle {
|
||||
|
||||
export component StreamWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: 1280px;
|
||||
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
|
||||
min-width: 640px;
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _UI_LOCALIZATION_H_
|
||||
#define _UI_LOCALIZATION_H_
|
||||
#ifndef CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
#define CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
|
||||
#include <slint.h>
|
||||
|
||||
@@ -78,6 +72,8 @@ inline int ApplyMainWindowStrings(
|
||||
Text(localization::self_hosted_server_address[language]));
|
||||
strings.set_server_port(
|
||||
Text(localization::self_hosted_server_port[language]));
|
||||
strings.set_coturn_port(
|
||||
Text(localization::self_hosted_server_coturn_server_port[language]));
|
||||
strings.set_version(Text(localization::version[language]));
|
||||
strings.set_signal_connected(Text(localization::signal_connected[language]));
|
||||
strings.set_signal_disconnected(
|
||||
@@ -166,5 +162,4 @@ inline int ApplyStreamWindowStrings(
|
||||
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
|
||||
#endif
|
||||
#endif // CROSSDESK_GUI_UI_UI_LOCALIZATION_H_
|
||||
|
||||
@@ -36,12 +36,12 @@ int GuiApplication::SelfHostedServerWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.298f, io.DisplaySize.y * 0.25f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.29f));
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.35f));
|
||||
} else {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.27f, io.DisplaySize.y * 0.3f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.29f));
|
||||
ImVec2(io.DisplaySize.x * 0.465f, io.DisplaySize.y * 0.35f));
|
||||
}
|
||||
|
||||
self_hosted_server_config_window_pos_reset_ = false;
|
||||
@@ -101,6 +101,25 @@ int GuiApplication::SelfHostedServerWindow() {
|
||||
IM_ARRAYSIZE(signal_server_port_self_));
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s", localization::self_hosted_server_coturn_server_port
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
|
||||
}
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
|
||||
|
||||
ImGui::InputText("##coturn_server_port_self_", coturn_server_port_self_,
|
||||
IM_ARRAYSIZE(coturn_server_port_self_));
|
||||
}
|
||||
|
||||
if (stream_window_inited_) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
@@ -122,12 +141,17 @@ int GuiApplication::SelfHostedServerWindow() {
|
||||
|
||||
config_center_->SetServerHost(signal_server_ip_self_);
|
||||
config_center_->SetServerPort(atoi(signal_server_port_self_));
|
||||
config_center_->SetCoturnServerPort(atoi(coturn_server_port_self_));
|
||||
strncpy(signal_server_ip_, signal_server_ip_self_,
|
||||
sizeof(signal_server_ip_) - 1);
|
||||
signal_server_ip_[sizeof(signal_server_ip_) - 1] = '\0';
|
||||
strncpy(signal_server_port_, signal_server_port_self_,
|
||||
sizeof(signal_server_port_) - 1);
|
||||
signal_server_port_[sizeof(signal_server_port_) - 1] = '\0';
|
||||
strncpy(coturn_server_port_, coturn_server_port_self_,
|
||||
sizeof(coturn_server_port_) - 1);
|
||||
coturn_server_port_[sizeof(coturn_server_port_) - 1] = '\0';
|
||||
|
||||
self_hosted_server_config_window_pos_reset_ = true;
|
||||
}
|
||||
|
||||
@@ -149,6 +173,14 @@ int GuiApplication::SelfHostedServerWindow() {
|
||||
} else {
|
||||
signal_server_port_self_[0] = '\0';
|
||||
}
|
||||
int coturn_port = config_center_->GetCoturnServerPort();
|
||||
if (coturn_port > 0) {
|
||||
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
|
||||
sizeof(coturn_server_port_self_) - 1);
|
||||
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
|
||||
} else {
|
||||
coturn_server_port_self_[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "platform/common/gui/cuda_driver.h"
|
||||
|
||||
#if CROSSDESK_HAS_CUDA_DRIVER
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
const CudaDriver* CudaDriver::Get() {
|
||||
static const CudaDriver driver;
|
||||
return driver.loaded_ ? &driver : nullptr;
|
||||
}
|
||||
|
||||
void* CudaDriver::Symbol(const char* name) const {
|
||||
#if defined(_WIN32)
|
||||
return reinterpret_cast<void*>(
|
||||
GetProcAddress(static_cast<HMODULE>(module_), name));
|
||||
#else
|
||||
return dlsym(module_, name);
|
||||
#endif
|
||||
}
|
||||
|
||||
CudaDriver::CudaDriver() {
|
||||
#if defined(_WIN32)
|
||||
module_ = LoadLibraryW(L"nvcuda.dll");
|
||||
#else
|
||||
module_ = dlopen("libcuda.so.1", RTLD_LAZY | RTLD_LOCAL);
|
||||
#endif
|
||||
if (!module_) return;
|
||||
const auto load = [this](auto& function, const char* name) {
|
||||
function = reinterpret_cast<std::decay_t<decltype(function)>>(Symbol(name));
|
||||
return function != nullptr;
|
||||
};
|
||||
loaded_ = load(init, "cuInit") &&
|
||||
load(device_get_count, "cuDeviceGetCount") &&
|
||||
load(device_get, "cuDeviceGet") &&
|
||||
load(context_create, "cuCtxCreate_v2") &&
|
||||
load(context_destroy, "cuCtxDestroy_v2") &&
|
||||
load(context_push, "cuCtxPushCurrent_v2") &&
|
||||
load(context_pop, "cuCtxPopCurrent_v2") &&
|
||||
load(error_name, "cuGetErrorName") &&
|
||||
load(mem_alloc, "cuMemAlloc_v2") &&
|
||||
load(mem_free, "cuMemFree_v2") &&
|
||||
load(memcpy_2d, "cuMemcpy2D_v2") &&
|
||||
load(memcpy_2d_async, "cuMemcpy2DAsync_v2") &&
|
||||
load(stream_create, "cuStreamCreate") &&
|
||||
load(stream_destroy, "cuStreamDestroy_v2") &&
|
||||
load(stream_synchronize, "cuStreamSynchronize") &&
|
||||
load(register_gl_buffer, "cuGraphicsGLRegisterBuffer") &&
|
||||
load(unregister_resource, "cuGraphicsUnregisterResource") &&
|
||||
load(map_resources, "cuGraphicsMapResources") &&
|
||||
load(unmap_resources, "cuGraphicsUnmapResources") &&
|
||||
load(mapped_pointer, "cuGraphicsResourceGetMappedPointer_v2");
|
||||
}
|
||||
|
||||
CudaDriver::~CudaDriver() {
|
||||
if (!module_) return;
|
||||
#if defined(_WIN32)
|
||||
FreeLibrary(static_cast<HMODULE>(module_));
|
||||
#else
|
||||
dlclose(module_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CudaDriver::Check(CUresult result, const char* operation) const {
|
||||
if (result == CUDA_SUCCESS) return true;
|
||||
const char* name = nullptr;
|
||||
error_name(result, &name);
|
||||
LOG_WARN("CUDA/OpenGL {} failed: {} ({})", operation,
|
||||
name ? name : "unknown CUDA error", static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_DRIVER_H_
|
||||
#define _CUDA_DRIVER_H_
|
||||
|
||||
#if defined(USE_CUDA) && USE_CUDA && \
|
||||
(defined(_WIN32) || \
|
||||
(defined(__linux__) && (defined(__x86_64__) || defined(__amd64__))))
|
||||
#define CROSSDESK_HAS_CUDA_DRIVER 1
|
||||
|
||||
#include <cuda.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Application-owned driver entry points. No codec library or MiniRTC internals
|
||||
// are involved; the module remains loaded for this singleton's lifetime.
|
||||
class CudaDriver {
|
||||
public:
|
||||
static const CudaDriver* Get();
|
||||
bool Check(CUresult result, const char* operation) const;
|
||||
|
||||
decltype(&cuInit) init = nullptr;
|
||||
decltype(&cuDeviceGetCount) device_get_count = nullptr;
|
||||
decltype(&cuDeviceGet) device_get = nullptr;
|
||||
CUresult(CUDAAPI* context_create)(CUcontext*, unsigned int, CUdevice) = nullptr;
|
||||
decltype(&cuCtxDestroy) context_destroy = nullptr;
|
||||
decltype(&cuCtxPushCurrent) context_push = nullptr;
|
||||
decltype(&cuCtxPopCurrent) context_pop = nullptr;
|
||||
decltype(&cuGetErrorName) error_name = nullptr;
|
||||
decltype(&cuMemAlloc) mem_alloc = nullptr;
|
||||
decltype(&cuMemFree) mem_free = nullptr;
|
||||
decltype(&cuMemcpy2D) memcpy_2d = nullptr;
|
||||
decltype(&cuMemcpy2DAsync) memcpy_2d_async = nullptr;
|
||||
decltype(&cuStreamCreate) stream_create = nullptr;
|
||||
decltype(&cuStreamDestroy) stream_destroy = nullptr;
|
||||
decltype(&cuStreamSynchronize) stream_synchronize = nullptr;
|
||||
CUresult(CUDAAPI* register_gl_buffer)(CUgraphicsResource*, unsigned int,
|
||||
unsigned int) = nullptr;
|
||||
decltype(&cuGraphicsUnregisterResource) unregister_resource = nullptr;
|
||||
decltype(&cuGraphicsMapResources) map_resources = nullptr;
|
||||
decltype(&cuGraphicsUnmapResources) unmap_resources = nullptr;
|
||||
decltype(&cuGraphicsResourceGetMappedPointer) mapped_pointer = nullptr;
|
||||
|
||||
private:
|
||||
CudaDriver();
|
||||
~CudaDriver();
|
||||
CudaDriver(const CudaDriver&) = delete;
|
||||
CudaDriver& operator=(const CudaDriver&) = delete;
|
||||
void* Symbol(const char* name) const;
|
||||
void* module_ = nullptr;
|
||||
bool loaded_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
#else
|
||||
#define CROSSDESK_HAS_CUDA_DRIVER 0
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,198 +0,0 @@
|
||||
#include "platform/common/gui/cuda_opengl_interop.h"
|
||||
#include "platform/common/gui/cuda_driver.h"
|
||||
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include "platform/common/gui/native_video_frame_ref.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#if CROSSDESK_HAS_CUDA_DRIVER
|
||||
|
||||
namespace {
|
||||
|
||||
class ScopedCudaContext {
|
||||
public:
|
||||
ScopedCudaContext(const CudaDriver& driver, CUcontext context)
|
||||
: cuda(driver),
|
||||
active_(cuda.Check(cuda.context_push(context), "cuCtxPushCurrent")) {}
|
||||
~ScopedCudaContext() { Pop(); }
|
||||
explicit operator bool() const { return active_; }
|
||||
bool Pop() {
|
||||
if (!active_) return true;
|
||||
active_ = false;
|
||||
CUcontext context = nullptr;
|
||||
return cuda.Check(cuda.context_pop(&context), "cuCtxPopCurrent");
|
||||
}
|
||||
|
||||
private:
|
||||
const CudaDriver& cuda;
|
||||
bool active_;
|
||||
};
|
||||
|
||||
bool IsCudaFrame(const MiniRtcNativeVideoFrame* frame) {
|
||||
return frame && GetOpenGlNativeFrame(*frame) &&
|
||||
frame->type == MiniRtcNativeVideoFrameCudaNv12;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CudaOpenGlVideoBuffer {
|
||||
const CudaDriver* driver = nullptr;
|
||||
CUcontext context = nullptr;
|
||||
CUstream stream = nullptr;
|
||||
CUgraphicsResource resource = nullptr;
|
||||
bool mapped = false;
|
||||
bool failed = false;
|
||||
NativeVideoFrameRef source_frame;
|
||||
};
|
||||
|
||||
bool IsCudaDeviceAvailable() {
|
||||
const auto* cuda = CudaDriver::Get();
|
||||
int count = 0;
|
||||
return cuda && cuda->init(0) == CUDA_SUCCESS &&
|
||||
cuda->device_get_count(&count) == CUDA_SUCCESS && count > 0;
|
||||
}
|
||||
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer) {
|
||||
const auto* driver = CudaDriver::Get();
|
||||
if (!IsCudaFrame(frame) || gl_buffer == 0 || !driver) {
|
||||
return nullptr;
|
||||
}
|
||||
std::unique_ptr<CudaOpenGlVideoBuffer, decltype(&DestroyCudaOpenGlVideoBuffer)>
|
||||
buffer(new (std::nothrow) CudaOpenGlVideoBuffer,
|
||||
&DestroyCudaOpenGlVideoBuffer);
|
||||
if (!buffer) return nullptr;
|
||||
buffer->driver = driver;
|
||||
const auto& cuda = *driver;
|
||||
buffer->context = static_cast<CUcontext>(frame->payload.cuda_nv12.context);
|
||||
buffer->source_frame = NativeVideoFrameRef(frame);
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context ||
|
||||
!cuda.Check(cuda.stream_create(&buffer->stream,
|
||||
CU_STREAM_NON_BLOCKING),
|
||||
"cuStreamCreate") ||
|
||||
!cuda.Check(cuda.register_gl_buffer(
|
||||
&buffer->resource, gl_buffer,
|
||||
CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD),
|
||||
"cuGraphicsGLRegisterBuffer") ||
|
||||
!context.Pop()) {
|
||||
return nullptr;
|
||||
}
|
||||
return buffer.release();
|
||||
}
|
||||
|
||||
int UploadCudaFrameToOpenGlBuffer(
|
||||
CudaOpenGlVideoBuffer* buffer, const MiniRtcNativeVideoFrame* frame) {
|
||||
if (!buffer || buffer->failed || !IsCudaFrame(frame) ||
|
||||
frame->payload.cuda_nv12.context != buffer->context) {
|
||||
return -1;
|
||||
}
|
||||
const auto& cuda = *buffer->driver;
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context) {
|
||||
buffer->failed = true;
|
||||
return -1;
|
||||
}
|
||||
// The caller has completed this buffer's previous GL use. Hold the new
|
||||
// surface through asynchronous copies, including partially failed uploads.
|
||||
buffer->source_frame = NativeVideoFrameRef(frame);
|
||||
const char* operation = "cuGraphicsMapResources";
|
||||
CUresult result =
|
||||
cuda.map_resources(1, &buffer->resource, buffer->stream);
|
||||
buffer->mapped = result == CUDA_SUCCESS;
|
||||
CUdeviceptr destination = 0;
|
||||
size_t mapped_size = 0;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
operation = "cuGraphicsResourceGetMappedPointer";
|
||||
result = cuda.mapped_pointer(
|
||||
&destination, &mapped_size, buffer->resource);
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(frame->width) * frame->height;
|
||||
if (result == CUDA_SUCCESS && mapped_size < y_size + y_size / 2) {
|
||||
operation = "validate mapped buffer size";
|
||||
result = CUDA_ERROR_INVALID_VALUE;
|
||||
}
|
||||
if (result == CUDA_SUCCESS) {
|
||||
CUDA_MEMCPY2D copy{};
|
||||
copy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.srcDevice = frame->payload.cuda_nv12.y_device_pointer;
|
||||
copy.srcPitch = frame->payload.cuda_nv12.y_stride;
|
||||
copy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
|
||||
copy.dstDevice = destination;
|
||||
copy.dstPitch = frame->width;
|
||||
copy.WidthInBytes = frame->width;
|
||||
copy.Height = frame->height;
|
||||
operation = "cuMemcpy2DAsync (Y plane)";
|
||||
result = cuda.memcpy_2d_async(©, buffer->stream);
|
||||
if (result == CUDA_SUCCESS) {
|
||||
copy.srcDevice = frame->payload.cuda_nv12.uv_device_pointer;
|
||||
copy.srcPitch = frame->payload.cuda_nv12.uv_stride;
|
||||
copy.dstDevice = destination + y_size;
|
||||
copy.Height = frame->height / 2;
|
||||
operation = "cuMemcpy2DAsync (UV plane)";
|
||||
result = cuda.memcpy_2d_async(©, buffer->stream);
|
||||
}
|
||||
}
|
||||
if (buffer->mapped) {
|
||||
const CUresult unmap =
|
||||
cuda.unmap_resources(1, &buffer->resource, buffer->stream);
|
||||
buffer->mapped = unmap != CUDA_SUCCESS;
|
||||
if (result == CUDA_SUCCESS) {
|
||||
operation = "cuGraphicsUnmapResources";
|
||||
result = unmap;
|
||||
} else {
|
||||
cuda.Check(unmap, "cuGraphicsUnmapResources (cleanup)");
|
||||
}
|
||||
}
|
||||
const bool copied = cuda.Check(result, operation);
|
||||
const bool popped = context.Pop();
|
||||
buffer->failed = !copied || !popped;
|
||||
return buffer->failed ? -1 : 0;
|
||||
}
|
||||
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer) {
|
||||
if (!buffer) return;
|
||||
// Keep the frame's device context alive until after the context is popped.
|
||||
std::unique_ptr<CudaOpenGlVideoBuffer> owner(buffer);
|
||||
const auto& cuda = *buffer->driver;
|
||||
ScopedCudaContext context(cuda, buffer->context);
|
||||
if (!context) return;
|
||||
if (buffer->stream) {
|
||||
cuda.Check(cuda.stream_synchronize(buffer->stream),
|
||||
"cuStreamSynchronize (destroy)");
|
||||
}
|
||||
if (buffer->mapped) {
|
||||
cuda.Check(cuda.unmap_resources(
|
||||
1, &buffer->resource, buffer->stream),
|
||||
"cuGraphicsUnmapResources (destroy)");
|
||||
cuda.Check(cuda.stream_synchronize(buffer->stream),
|
||||
"cuStreamSynchronize (unmap)");
|
||||
}
|
||||
if (buffer->resource) {
|
||||
cuda.Check(cuda.unregister_resource(buffer->resource),
|
||||
"cuGraphicsUnregisterResource");
|
||||
}
|
||||
if (buffer->stream) {
|
||||
cuda.Check(cuda.stream_destroy(buffer->stream), "cuStreamDestroy");
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool IsCudaDeviceAvailable() { return false; }
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame*, uint32_t) {
|
||||
return nullptr;
|
||||
}
|
||||
int UploadCudaFrameToOpenGlBuffer(
|
||||
CudaOpenGlVideoBuffer*, const MiniRtcNativeVideoFrame*) {
|
||||
return -1;
|
||||
}
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer*) {}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_OPENGL_INTEROP_H_
|
||||
#define _CUDA_OPENGL_INTEROP_H_
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool IsCudaDeviceAvailable();
|
||||
|
||||
struct CudaOpenGlVideoBuffer;
|
||||
|
||||
// These operations require the matching GL context to be current. The caller
|
||||
// owns the GL buffer and must allocate its storage before registering it.
|
||||
CudaOpenGlVideoBuffer* CreateCudaOpenGlVideoBuffer(
|
||||
const MiniRtcNativeVideoFrame* frame, uint32_t gl_buffer);
|
||||
|
||||
// Asynchronous, GPU-only NV12 copy. Retains the source through the next upload
|
||||
// or destruction, including on failure. Before reusing the buffer, the caller
|
||||
// must wait for the previous GL fence. Frames must use the same CUDA context.
|
||||
int UploadCudaFrameToOpenGlBuffer(CudaOpenGlVideoBuffer* buffer,
|
||||
const MiniRtcNativeVideoFrame* frame);
|
||||
|
||||
// Finish prior GL use before calling. Waits for CUDA work and releases retained
|
||||
// frames; the caller may then resize/delete the GL buffer. Accepts null.
|
||||
void DestroyCudaOpenGlVideoBuffer(CudaOpenGlVideoBuffer* buffer);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _NATIVE_VIDEO_FRAME_REF_H_
|
||||
#define _NATIVE_VIDEO_FRAME_REF_H_
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class NativeVideoFrameRef {
|
||||
public:
|
||||
NativeVideoFrameRef() = default;
|
||||
|
||||
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame *frame) {
|
||||
if (frame && frame->owner && frame->retain && frame->release) {
|
||||
frame_ = *frame;
|
||||
frame_.struct_size = sizeof(frame_);
|
||||
frame_.retain(frame_.owner);
|
||||
valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
NativeVideoFrameRef(const NativeVideoFrameRef &other)
|
||||
: NativeVideoFrameRef(other.Get()) {}
|
||||
|
||||
NativeVideoFrameRef(NativeVideoFrameRef &&other) noexcept
|
||||
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
|
||||
other.frame_ = {};
|
||||
}
|
||||
|
||||
NativeVideoFrameRef &operator=(NativeVideoFrameRef other) noexcept {
|
||||
Swap(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~NativeVideoFrameRef() { Reset(); }
|
||||
|
||||
void Reset() {
|
||||
if (valid_ && frame_.owner && frame_.release) {
|
||||
frame_.release(frame_.owner);
|
||||
}
|
||||
frame_ = {};
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void Swap(NativeVideoFrameRef &other) noexcept {
|
||||
std::swap(frame_, other.frame_);
|
||||
std::swap(valid_, other.valid_);
|
||||
}
|
||||
|
||||
const MiniRtcNativeVideoFrame *Get() const { return valid_ ? &frame_ : nullptr; }
|
||||
explicit operator bool() const { return valid_; }
|
||||
|
||||
private:
|
||||
MiniRtcNativeVideoFrame frame_{};
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
inline const MiniRtcNativeVideoFrame *GetOpenGlNativeFrame(
|
||||
const MiniRtcNativeVideoFrame &frame) {
|
||||
const MiniRtcNativeVideoFrame *native = &frame;
|
||||
if (native->struct_size < static_cast<uint32_t>(sizeof(MiniRtcNativeVideoFrame)) ||
|
||||
!native->owner || !native->retain || !native->release ||
|
||||
!native->copy_to_nv12 || native->width == 0 || native->height == 0 ||
|
||||
(native->width & 1U) != 0 || (native->height & 1U) != 0 ||
|
||||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
|
||||
native->height > static_cast<uint32_t>(std::numeric_limits<int>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
if (native->type == MiniRtcNativeVideoFrameCpuNv12) {
|
||||
return native->payload.cpu_nv12.y_plane &&
|
||||
native->payload.cpu_nv12.uv_plane &&
|
||||
native->payload.cpu_nv12.y_stride >= native->width &&
|
||||
native->payload.cpu_nv12.uv_stride >= native->width
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
if (native->type == MiniRtcNativeVideoFrameCudaNv12) {
|
||||
return native->payload.cuda_nv12.y_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.uv_device_pointer != 0 &&
|
||||
native->payload.cuda_nv12.y_stride >= native->width &&
|
||||
native->payload.cuda_nv12.uv_stride >= native->width &&
|
||||
native->payload.cuda_nv12.context
|
||||
? native
|
||||
: nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,11 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// NV12 underlay for Slint's FemtoVG OpenGL renderer on Windows and Linux.
|
||||
// MiniRTC submits cross-platform native NV12 descriptors or tightly packed CPU
|
||||
// fallback frames from its decode callback thread. CUDA frames rotate through
|
||||
// fenced OpenGL upload resources; CPU descriptors expose their planes without
|
||||
// an intermediate queue copy. The UI thread always consumes the newest frame.
|
||||
// MiniRTC submits either ref-counted Windows native NV12 frames or packed CPU
|
||||
// NV12 fallback frames from its decode callback thread. CUDA-backed frames use
|
||||
// CUDA/OpenGL interop; CPU-backed frames are retained without an intermediate
|
||||
// queue copy. The Slint UI thread updates Y and UV textures and performs color
|
||||
// conversion and scaling before Slint draws its overlay.
|
||||
class OpenGlVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
OpenGlVideoRenderer();
|
||||
@@ -32,6 +33,11 @@ public:
|
||||
bool IsReady() const override;
|
||||
bool IsActive() const override;
|
||||
|
||||
// Keeps Slint's native surface on a display-driven redraw loop while decoded
|
||||
// frames are arriving. This avoids timer/vsync phase drift that otherwise
|
||||
// causes every second redraw request to be coalesced on Windows.
|
||||
bool ShouldContinueRendering() const;
|
||||
|
||||
// Only the selected stream is queued for upload. Frames for other tabs are
|
||||
// retained by GuiRuntime at a throttled rate for thumbnail/tab restoration.
|
||||
bool SetSelectedStream(std::string remote_id) override;
|
||||
@@ -43,7 +49,7 @@ public:
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id, const uint8_t *data,
|
||||
size_t size, int width, int height) override;
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const MiniRtcNativeVideoFrame &frame) override;
|
||||
const XVideoFrame &frame) override;
|
||||
|
||||
// Draws below Slint while its OpenGL context is current. All dimensions are
|
||||
// physical pixels in the window client area. corner_radius_pixels clips the
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "platform/linux/gui/cuda_graphics.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include "platform/common/gui/cuda_opengl_interop.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
void ConfigureLinuxCudaGraphics(bool hardware_acceleration) {
|
||||
#if USE_CUDA && (defined(__x86_64__) || defined(__amd64__))
|
||||
if (!hardware_acceleration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Respect explicit GPU/driver selection, including a request to use Mesa
|
||||
// or software rendering. Native frames can still use the CPU fallback.
|
||||
for (const char* name : {"__NV_PRIME_RENDER_OFFLOAD",
|
||||
"__NV_PRIME_RENDER_OFFLOAD_PROVIDER",
|
||||
"__GLX_VENDOR_LIBRARY_NAME",
|
||||
"__EGL_VENDOR_LIBRARY_FILENAMES",
|
||||
"__EGL_VENDOR_LIBRARY_DIRS", "DRI_PRIME",
|
||||
"LIBGL_ALWAYS_SOFTWARE",
|
||||
"MESA_LOADER_DRIVER_OVERRIDE"}) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value && value[0] != '\0') {
|
||||
LOG_INFO("Keeping explicit Linux graphics selection: {}={}", name,
|
||||
value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsCudaDeviceAvailable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both EGL and GLX need to render on NVIDIA to share CUDA frame buffers.
|
||||
// These are process-local, not desktop settings.
|
||||
// https://download.nvidia.com/XFree86/Linux-x86_64/595.45.04/README/primerenderoffload.html
|
||||
if (setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 1) != 0 ||
|
||||
setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 1) != 0) {
|
||||
LOG_WARN("Unable to select NVIDIA PRIME rendering for CUDA video");
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Selected NVIDIA PRIME rendering for CUDA/OpenGL video interop");
|
||||
#else
|
||||
(void)hardware_acceleration;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CUDA_GRAPHICS_H_
|
||||
#define _CUDA_GRAPHICS_H_
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Call before SDL or Slint initializes EGL/GLX. Hybrid laptops otherwise
|
||||
// create the GUI context on the integrated GPU while NVDEC uses NVIDIA.
|
||||
void ConfigureLinuxCudaGraphics(bool hardware_acceleration);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -392,10 +392,7 @@ struct LinuxTrayImpl {
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {
|
||||
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
: "Exit") {}
|
||||
|
||||
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
|
||||
std::function<void()> hide_window_callback,
|
||||
@@ -419,9 +416,7 @@ struct LinuxTrayImpl {
|
||||
exit_menu_label(GetExitMenuLabel(language_index_value)),
|
||||
exit_menu_ascii_label(IsAsciiPrintable(exit_menu_label)
|
||||
? exit_menu_label
|
||||
: "Exit") {
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
: "Exit") {}
|
||||
|
||||
~LinuxTrayImpl() { RemoveTrayIcon(); }
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ bool ScreenCapturerDrm::CaptureOutputFrame(const DrmOutput& output,
|
||||
|
||||
if (emit_callback && callback_) {
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), capture_width,
|
||||
capture_height, output.stream_id.c_str(), nullptr);
|
||||
capture_height, output.stream_id.c_str());
|
||||
}
|
||||
|
||||
UnmapFramebuffer(mapped_ptr, mapped_size, prime_fd);
|
||||
|
||||
@@ -50,8 +50,7 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
callback_orig_ = std::move(cb);
|
||||
callback_ = [this](unsigned char* data, int size, int width, int height,
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
const char* reported_stream_id) {
|
||||
const std::string mapped_stream_id = MapStreamId(reported_stream_id);
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
@@ -65,8 +64,7 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
if (callback_orig_) {
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str(),
|
||||
native_frame);
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1047,7 +1047,7 @@ void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(0);
|
||||
callback_(nv12.data(), static_cast<int>(nv12.size()), even_width,
|
||||
even_height, stream_id.c_str(), nullptr);
|
||||
even_height, stream_id.c_str());
|
||||
}
|
||||
pipewire_last_frame_ms_.store(NowMs());
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ void ScreenCapturerX11::OnFrame() {
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(monitor_index);
|
||||
callback_(nv12.data(), width_ * height_ * 3 / 2, width_, height_,
|
||||
stream_id.c_str(), nullptr);
|
||||
stream_id.c_str());
|
||||
}
|
||||
|
||||
XDestroyImage(image);
|
||||
|
||||
@@ -3,240 +3,269 @@
|
||||
#include <pulse/error.h>
|
||||
#include <pulse/introspect.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <condition_variable>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr int kSampleRate = 48000;
|
||||
constexpr pa_sample_format_t kFormat = PA_SAMPLE_S16LE;
|
||||
constexpr int kChannels = 1;
|
||||
constexpr size_t kFrameSizeBytes = 480 * sizeof(int16_t);
|
||||
constexpr auto kStartupTimeout = std::chrono::seconds(2);
|
||||
constexpr auto kPollInterval = std::chrono::milliseconds(5);
|
||||
|
||||
void ReleaseOperation(pa_operation* operation) {
|
||||
if (!operation) return;
|
||||
if (pa_operation_get_state(operation) == PA_OPERATION_RUNNING)
|
||||
pa_operation_cancel(operation);
|
||||
pa_operation_unref(operation);
|
||||
SpeakerCapturerLinux::SpeakerCapturerLinux()
|
||||
: inited_(false), paused_(false), stop_flag_(false) {}
|
||||
SpeakerCapturerLinux::~SpeakerCapturerLinux() {
|
||||
Stop();
|
||||
Destroy();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SpeakerCapturerLinux::~SpeakerCapturerLinux() { Destroy(); }
|
||||
|
||||
int SpeakerCapturerLinux::Init(speaker_data_cb cb) {
|
||||
if (inited_) return 0;
|
||||
cb_ = std::move(cb);
|
||||
cb_ = cb;
|
||||
inited_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Use nonblocking iterations instead of pa_threaded_mainloop_wait(): a server
|
||||
// that never replies, disappears, or never reaches STREAM_READY must not leave
|
||||
// startup/Stop waiting for a signal that will never be delivered.
|
||||
bool SpeakerCapturerLinux::PumpUntil(
|
||||
const std::function<bool()>& ready,
|
||||
std::chrono::steady_clock::time_point deadline) {
|
||||
while (!stop_flag_) {
|
||||
if (ready()) return true;
|
||||
if (std::chrono::steady_clock::now() >= deadline ||
|
||||
!PA_CONTEXT_IS_GOOD(pa_context_get_state(context_)))
|
||||
return false;
|
||||
if (pa_mainloop_iterate(mainloop_, 0, nullptr) < 0) return false;
|
||||
std::this_thread::sleep_for(kPollInterval);
|
||||
}
|
||||
return false;
|
||||
int SpeakerCapturerLinux::Destroy() {
|
||||
inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SpeakerCapturerLinux::Prepare() {
|
||||
const auto deadline = std::chrono::steady_clock::now() + kStartupTimeout;
|
||||
mainloop_ = pa_mainloop_new();
|
||||
if (!mainloop_) return false;
|
||||
context_ =
|
||||
pa_context_new(pa_mainloop_get_api(mainloop_), "CrossDesk system audio");
|
||||
if (!context_ ||
|
||||
pa_context_connect(context_, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
|
||||
return false;
|
||||
if (!PumpUntil(
|
||||
[&] { return pa_context_get_state(context_) == PA_CONTEXT_READY; },
|
||||
deadline))
|
||||
return false;
|
||||
std::string SpeakerCapturerLinux::GetDefaultMonitorSourceName() {
|
||||
std::string monitor_name;
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
bool ready = false;
|
||||
|
||||
struct ServerResult {
|
||||
bool done = false;
|
||||
std::string sink;
|
||||
} server;
|
||||
pa_operation* operation = pa_context_get_server_info(
|
||||
context_,
|
||||
[](pa_context*, const pa_server_info* info, void* data) {
|
||||
auto& result = *static_cast<ServerResult*>(data);
|
||||
if (info && info->default_sink_name)
|
||||
result.sink = info->default_sink_name;
|
||||
result.done = true;
|
||||
pa_mainloop* mainloop = pa_mainloop_new();
|
||||
pa_mainloop_api* api = pa_mainloop_get_api(mainloop);
|
||||
pa_context* context = pa_context_new(api, "GetMonitor");
|
||||
|
||||
struct CallbackState {
|
||||
std::string* name;
|
||||
std::mutex* mtx;
|
||||
std::condition_variable* cv;
|
||||
bool* ready;
|
||||
} state{&monitor_name, &mtx, &cv, &ready};
|
||||
|
||||
pa_context_set_state_callback(
|
||||
context,
|
||||
[](pa_context* c, void* userdata) {
|
||||
auto* state = static_cast<CallbackState*>(userdata);
|
||||
if (pa_context_get_state(c) == PA_CONTEXT_READY) {
|
||||
pa_operation* operation = pa_context_get_server_info(
|
||||
c,
|
||||
[](pa_context*, const pa_server_info* info, void* userdata) {
|
||||
auto* state = static_cast<CallbackState*>(userdata);
|
||||
if (info && info->default_sink_name) {
|
||||
*(state->name) =
|
||||
std::string(info->default_sink_name) + ".monitor";
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*(state->mtx));
|
||||
*(state->ready) = true;
|
||||
}
|
||||
state->cv->notify_one();
|
||||
},
|
||||
userdata);
|
||||
if (operation) {
|
||||
pa_operation_unref(operation);
|
||||
}
|
||||
}
|
||||
},
|
||||
&server);
|
||||
const bool got_server =
|
||||
operation && PumpUntil([&] { return server.done; }, deadline);
|
||||
ReleaseOperation(operation);
|
||||
if (!got_server || server.sink.empty()) return false;
|
||||
&state);
|
||||
|
||||
// Query the actual monitor name; a changed/default device need not follow
|
||||
// the "<sink>.monitor" naming convention.
|
||||
struct SinkResult {
|
||||
bool done = false;
|
||||
std::string monitor;
|
||||
} sink;
|
||||
operation = pa_context_get_sink_info_by_name(
|
||||
context_, server.sink.c_str(),
|
||||
[](pa_context*, const pa_sink_info* info, int eol, void* data) {
|
||||
auto& result = *static_cast<SinkResult*>(data);
|
||||
if (info && info->monitor_source_name)
|
||||
result.monitor = info->monitor_source_name;
|
||||
if (eol != 0) result.done = true;
|
||||
},
|
||||
&sink);
|
||||
const bool got_sink =
|
||||
operation && PumpUntil([&] { return sink.done; }, deadline);
|
||||
ReleaseOperation(operation);
|
||||
if (!got_sink || sink.monitor.empty()) return false;
|
||||
pa_context_connect(context, nullptr, PA_CONTEXT_NOFLAGS, nullptr);
|
||||
|
||||
const pa_sample_spec spec{PA_SAMPLE_S16LE, 48000, 1};
|
||||
stream_ = pa_stream_new(context_, "System audio", &spec, nullptr);
|
||||
if (!stream_) return false;
|
||||
pa_stream_set_read_callback(
|
||||
stream_,
|
||||
[](pa_stream* stream, size_t, void* data) {
|
||||
static_cast<SpeakerCapturerLinux*>(data)->ReadAudio(stream);
|
||||
},
|
||||
this);
|
||||
pa_buffer_attr attr{};
|
||||
attr.maxlength = static_cast<uint32_t>(-1);
|
||||
attr.fragsize = static_cast<uint32_t>(kFrameSizeBytes);
|
||||
if (pa_stream_connect_record(stream_, sink.monitor.c_str(), &attr,
|
||||
PA_STREAM_ADJUST_LATENCY) < 0)
|
||||
return false;
|
||||
if (!PumpUntil(
|
||||
[&] {
|
||||
const auto state = pa_stream_get_state(stream_);
|
||||
return state == PA_STREAM_READY || !PA_STREAM_IS_GOOD(state);
|
||||
},
|
||||
deadline) ||
|
||||
pa_stream_get_state(stream_) != PA_STREAM_READY)
|
||||
return false;
|
||||
LOG_INFO("System audio monitor ready: {}", sink.monitor);
|
||||
return true;
|
||||
}
|
||||
std::thread loop_thread([&]() {
|
||||
int ret = 0;
|
||||
pa_mainloop_run(mainloop, &ret);
|
||||
});
|
||||
|
||||
void SpeakerCapturerLinux::Run() {
|
||||
const bool ready = Prepare();
|
||||
running_ = ready && !stop_flag_;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mtx_);
|
||||
startup_success_ = running_;
|
||||
startup_done_ = true;
|
||||
std::unique_lock<std::mutex> lock(mtx);
|
||||
cv.wait_for(lock, std::chrono::seconds(2), [&] { return ready; });
|
||||
}
|
||||
startup_wake_.notify_one();
|
||||
if (!ready && !stop_flag_) {
|
||||
LOG_ERROR(
|
||||
"PulseAudio capture startup failed or timed out: {}",
|
||||
context_ ? pa_strerror(pa_context_errno(context_)) : "no context");
|
||||
}
|
||||
while (running_ && !stop_flag_) {
|
||||
if (pa_mainloop_iterate(mainloop_, 0, nullptr) < 0 ||
|
||||
pa_context_get_state(context_) != PA_CONTEXT_READY ||
|
||||
pa_stream_get_state(stream_) != PA_STREAM_READY)
|
||||
break;
|
||||
std::this_thread::sleep_for(kPollInterval);
|
||||
}
|
||||
running_ = false;
|
||||
Cleanup();
|
||||
|
||||
pa_context_disconnect(context);
|
||||
pa_context_unref(context);
|
||||
pa_mainloop_quit(mainloop, 0);
|
||||
loop_thread.join();
|
||||
pa_mainloop_free(mainloop);
|
||||
|
||||
return monitor_name;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Start() {
|
||||
if (!inited_) return -1;
|
||||
if (IsRunning()) return 0;
|
||||
Stop();
|
||||
if (!inited_ || mainloop_thread_.joinable()) return -1;
|
||||
stop_flag_ = false;
|
||||
paused_ = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mtx_);
|
||||
startup_done_ = false;
|
||||
startup_success_ = false;
|
||||
}
|
||||
mainloop_thread_ = std::thread([this] { Run(); });
|
||||
std::unique_lock<std::mutex> lock(state_mtx_);
|
||||
const bool started =
|
||||
startup_wake_.wait_for(lock, kStartupTimeout + std::chrono::seconds(1),
|
||||
[&] { return startup_done_; }) &&
|
||||
startup_success_;
|
||||
lock.unlock();
|
||||
if (!started) {
|
||||
Stop();
|
||||
return -1;
|
||||
}
|
||||
|
||||
mainloop_thread_ = std::thread([this]() {
|
||||
std::string monitor_name = GetDefaultMonitorSourceName();
|
||||
if (monitor_name.empty()) {
|
||||
LOG_ERROR("Failed to get monitor source");
|
||||
return;
|
||||
}
|
||||
|
||||
mainloop_ = pa_threaded_mainloop_new();
|
||||
pa_mainloop_api* api = pa_threaded_mainloop_get_api(mainloop_);
|
||||
context_ = pa_context_new(api, "SpeakerCapturer");
|
||||
|
||||
pa_context_set_state_callback(
|
||||
context_,
|
||||
[](pa_context* c, void* userdata) {
|
||||
auto self = static_cast<SpeakerCapturerLinux*>(userdata);
|
||||
pa_context_state_t state = pa_context_get_state(c);
|
||||
if (state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED ||
|
||||
state == PA_CONTEXT_TERMINATED) {
|
||||
pa_threaded_mainloop_signal(self->mainloop_, 0);
|
||||
}
|
||||
},
|
||||
this);
|
||||
|
||||
if (pa_threaded_mainloop_start(mainloop_) < 0) {
|
||||
LOG_ERROR("Failed to start mainloop");
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
|
||||
if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFLAGS, nullptr) <
|
||||
0) {
|
||||
LOG_ERROR("Failed to connect context");
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pa_context_state_t state = pa_context_get_state(context_);
|
||||
if (state == PA_CONTEXT_READY) break;
|
||||
if (!PA_CONTEXT_IS_GOOD(state) || stop_flag_) {
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
pa_threaded_mainloop_wait(mainloop_);
|
||||
}
|
||||
|
||||
pa_sample_spec ss = {kFormat, kSampleRate, kChannels};
|
||||
stream_ = pa_stream_new(context_, "Capture", &ss, nullptr);
|
||||
|
||||
pa_stream_set_read_callback(
|
||||
stream_,
|
||||
[](pa_stream* s, size_t len, void* u) {
|
||||
auto self = static_cast<SpeakerCapturerLinux*>(u);
|
||||
if (self->paused_ || self->stop_flag_) return;
|
||||
|
||||
const void* data = nullptr;
|
||||
if (pa_stream_peek(s, &data, &len) < 0 || !data) return;
|
||||
|
||||
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||
self->frame_cache_.insert(self->frame_cache_.end(), p, p + len);
|
||||
|
||||
while (self->frame_cache_.size() >= kFrameSizeBytes) {
|
||||
std::vector<uint8_t> temp_frame(
|
||||
self->frame_cache_.begin(),
|
||||
self->frame_cache_.begin() + kFrameSizeBytes);
|
||||
self->cb_(temp_frame.data(), kFrameSizeBytes, "audio");
|
||||
self->frame_cache_.erase(
|
||||
self->frame_cache_.begin(),
|
||||
self->frame_cache_.begin() + kFrameSizeBytes);
|
||||
}
|
||||
|
||||
pa_stream_drop(s);
|
||||
},
|
||||
this);
|
||||
|
||||
pa_buffer_attr attr = {.maxlength = (uint32_t)-1,
|
||||
.tlength = 0,
|
||||
.prebuf = 0,
|
||||
.minreq = 0,
|
||||
.fragsize = (uint32_t)kFrameSizeBytes};
|
||||
|
||||
if (pa_stream_connect_record(stream_, monitor_name.c_str(), &attr,
|
||||
PA_STREAM_ADJUST_LATENCY) < 0) {
|
||||
LOG_ERROR("Failed to connect stream");
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
pa_stream_state_t s = pa_stream_get_state(stream_);
|
||||
if (s == PA_STREAM_READY) break;
|
||||
if (!PA_STREAM_IS_GOOD(s) || stop_flag_) {
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
Cleanup();
|
||||
return;
|
||||
}
|
||||
pa_threaded_mainloop_wait(mainloop_);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
|
||||
while (!stop_flag_)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Stop() {
|
||||
stop_flag_ = true;
|
||||
if (mainloop_thread_.joinable()) mainloop_thread_.join();
|
||||
running_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Destroy() {
|
||||
Stop();
|
||||
cb_ = nullptr;
|
||||
inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SpeakerCapturerLinux::ReadAudio(pa_stream* stream) {
|
||||
const void* data = nullptr;
|
||||
size_t size = 0;
|
||||
if (pa_stream_peek(stream, &data, &size) < 0 || size == 0) return;
|
||||
if (data && !paused_ && !stop_flag_ && cb_) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
frame_cache_.insert(frame_cache_.end(), bytes, bytes + size);
|
||||
size_t consumed = 0;
|
||||
while (!stop_flag_ && frame_cache_.size() - consumed >= kFrameSizeBytes) {
|
||||
cb_(frame_cache_.data() + consumed, kFrameSizeBytes, "audio");
|
||||
consumed += kFrameSizeBytes;
|
||||
}
|
||||
frame_cache_.erase(frame_cache_.begin(), frame_cache_.begin() + consumed);
|
||||
} else {
|
||||
frame_cache_.clear();
|
||||
if (mainloop_) {
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
pa_threaded_mainloop_signal(mainloop_, 0);
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
}
|
||||
// Drop holes and paused data too, otherwise the read queue cannot advance.
|
||||
pa_stream_drop(stream);
|
||||
|
||||
if (mainloop_thread_.joinable()) {
|
||||
mainloop_thread_.join();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SpeakerCapturerLinux::Cleanup() {
|
||||
if (stream_) {
|
||||
pa_stream_set_read_callback(stream_, nullptr, nullptr);
|
||||
pa_stream_disconnect(stream_);
|
||||
pa_stream_unref(stream_);
|
||||
stream_ = nullptr;
|
||||
}
|
||||
if (context_) {
|
||||
pa_context_disconnect(context_);
|
||||
pa_context_unref(context_);
|
||||
context_ = nullptr;
|
||||
}
|
||||
if (mainloop_) {
|
||||
pa_mainloop_free(mainloop_);
|
||||
pa_threaded_mainloop_stop(mainloop_);
|
||||
pa_threaded_mainloop_lock(mainloop_);
|
||||
|
||||
if (stream_) {
|
||||
pa_stream_disconnect(stream_);
|
||||
pa_stream_unref(stream_);
|
||||
stream_ = nullptr;
|
||||
}
|
||||
|
||||
if (context_) {
|
||||
pa_context_disconnect(context_);
|
||||
pa_context_unref(context_);
|
||||
context_ = nullptr;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(mainloop_);
|
||||
pa_threaded_mainloop_free(mainloop_);
|
||||
mainloop_ = nullptr;
|
||||
}
|
||||
|
||||
frame_cache_.clear();
|
||||
}
|
||||
|
||||
bool SpeakerCapturerLinux::IsRunning() const { return running_; }
|
||||
int SpeakerCapturerLinux::Pause() {
|
||||
paused_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerLinux::Resume() {
|
||||
paused_ = false;
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
@@ -4,59 +4,53 @@
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_SPEAKER_CAPTURER_LINUX_H_
|
||||
#define CROSSDESK_SPEAKER_CAPTURER_LINUX_H_
|
||||
#ifndef _SPEAKER_CAPTURER_LINUX_H_
|
||||
#define _SPEAKER_CAPTURER_LINUX_H_
|
||||
|
||||
#include <pulse/pulseaudio.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "speaker_capturer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class SpeakerCapturerLinux : public SpeakerCapturer {
|
||||
public:
|
||||
SpeakerCapturerLinux() = default;
|
||||
~SpeakerCapturerLinux() override;
|
||||
SpeakerCapturerLinux();
|
||||
~SpeakerCapturerLinux();
|
||||
|
||||
int Init(speaker_data_cb cb) override;
|
||||
int Destroy() override;
|
||||
int Start() override;
|
||||
int Stop() override;
|
||||
bool IsRunning() const override;
|
||||
|
||||
int Pause();
|
||||
int Resume();
|
||||
|
||||
private:
|
||||
void Run();
|
||||
bool Prepare();
|
||||
bool PumpUntil(const std::function<bool()>& ready,
|
||||
std::chrono::steady_clock::time_point deadline);
|
||||
std::string GetDefaultMonitorSourceName();
|
||||
void Cleanup();
|
||||
void ReadAudio(pa_stream* stream);
|
||||
|
||||
speaker_data_cb cb_;
|
||||
bool inited_ = false;
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> stop_flag_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
private:
|
||||
speaker_data_cb cb_ = nullptr;
|
||||
|
||||
std::atomic<bool> inited_;
|
||||
std::atomic<bool> paused_;
|
||||
std::atomic<bool> stop_flag_;
|
||||
|
||||
std::thread mainloop_thread_;
|
||||
std::mutex state_mtx_;
|
||||
std::condition_variable startup_wake_;
|
||||
bool startup_done_ = false;
|
||||
bool startup_success_ = false;
|
||||
|
||||
// These objects are created, driven and freed exclusively by Run(). Stop
|
||||
// only changes an atomic flag and joins, never races PulseAudio callbacks.
|
||||
pa_mainloop* mainloop_ = nullptr;
|
||||
pa_threaded_mainloop* mainloop_ = nullptr;
|
||||
pa_context* context_ = nullptr;
|
||||
pa_stream* stream_ = nullptr;
|
||||
|
||||
std::mutex state_mtx_;
|
||||
std::vector<uint8_t> frame_cache_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// macOS video compositor used by CrossDesk only. VideoToolbox CVPixelBuffers
|
||||
// are sampled directly through CVMetalTextureCache; packed CPU NV12 remains a
|
||||
// fallback for software decoding and cached snapshots. AppKit attachment and
|
||||
// rendering are main-thread operations, while frame submission is safe from
|
||||
// MiniRTC's decode callback thread.
|
||||
// macOS video compositor used by CrossDesk only. MiniRTC continues to expose
|
||||
// tightly packed CPU NV12 frames; this renderer performs the single required
|
||||
// copy into shared Metal buffers and converts NV12 to RGB in the fragment
|
||||
// shader. AppKit attachment and rendering are main-thread operations, while
|
||||
// SubmitNv12() is safe to call from MiniRTC's decode callback thread.
|
||||
class MacMetalVideoRenderer final : public VideoRenderer {
|
||||
public:
|
||||
MacMetalVideoRenderer();
|
||||
@@ -37,11 +37,6 @@ class MacMetalVideoRenderer final : public VideoRenderer {
|
||||
SubmitResult SubmitNv12(std::string_view remote_id, const uint8_t* data,
|
||||
size_t size, int width, int height) override;
|
||||
|
||||
// Retains an IOSurface-backed VideoToolbox CVPixelBuffer until the Metal
|
||||
// command buffer using it has completed.
|
||||
SubmitResult SubmitNativeFrame(std::string_view remote_id,
|
||||
const MiniRtcNativeVideoFrame& frame) override;
|
||||
|
||||
// Seeds a newly selected stream from its retained CPU snapshot without
|
||||
// replacing a newer decoded frame that is already queued or rendering.
|
||||
SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "platform/macos/gui/metal_video_renderer.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <CoreVideo/CVMetalTextureCache.h>
|
||||
#import <Metal/Metal.h>
|
||||
#import <QuartzCore/CAMetalLayer.h>
|
||||
|
||||
@@ -195,74 +194,6 @@ namespace {
|
||||
|
||||
constexpr size_t kFrameSlotCount = 3;
|
||||
|
||||
class NativeVideoFrameRef {
|
||||
public:
|
||||
NativeVideoFrameRef() = default;
|
||||
|
||||
explicit NativeVideoFrameRef(const MiniRtcNativeVideoFrame* frame) {
|
||||
if (frame && frame->owner && frame->retain && frame->release) {
|
||||
frame_ = *frame;
|
||||
frame_.struct_size = sizeof(frame_);
|
||||
frame_.retain(frame_.owner);
|
||||
valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
NativeVideoFrameRef(const NativeVideoFrameRef& other)
|
||||
: NativeVideoFrameRef(other.Get()) {}
|
||||
|
||||
NativeVideoFrameRef(NativeVideoFrameRef&& other) noexcept
|
||||
: frame_(other.frame_), valid_(std::exchange(other.valid_, false)) {
|
||||
other.frame_ = {};
|
||||
}
|
||||
|
||||
NativeVideoFrameRef& operator=(NativeVideoFrameRef other) noexcept {
|
||||
Swap(other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
~NativeVideoFrameRef() { Reset(); }
|
||||
|
||||
void Reset() {
|
||||
if (valid_ && frame_.owner && frame_.release) {
|
||||
frame_.release(frame_.owner);
|
||||
}
|
||||
frame_ = {};
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
void Swap(NativeVideoFrameRef& other) noexcept {
|
||||
std::swap(frame_, other.frame_);
|
||||
std::swap(valid_, other.valid_);
|
||||
}
|
||||
|
||||
const MiniRtcNativeVideoFrame* Get() const {
|
||||
return valid_ ? &frame_ : nullptr;
|
||||
}
|
||||
explicit operator bool() const { return valid_; }
|
||||
|
||||
private:
|
||||
MiniRtcNativeVideoFrame frame_{};
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
const MiniRtcNativeVideoFrame* GetMacNativeFrame(
|
||||
const MiniRtcNativeVideoFrame& frame) {
|
||||
const MiniRtcNativeVideoFrame* native = &frame;
|
||||
if (native->struct_size < static_cast<uint32_t>(sizeof(*native)) ||
|
||||
native->type != MiniRtcNativeVideoFrameCVPixelBuffer ||
|
||||
!native->payload.cv_pixel_buffer || !native->owner || !native->retain ||
|
||||
!native->release || !native->copy_to_nv12 || native->width == 0 ||
|
||||
native->height == 0 || (native->width & 1U) != 0 ||
|
||||
(native->height & 1U) != 0 ||
|
||||
native->width > static_cast<uint32_t>(std::numeric_limits<int>::max()) ||
|
||||
native->height >
|
||||
static_cast<uint32_t>(std::numeric_limits<int>::max())) {
|
||||
return nullptr;
|
||||
}
|
||||
return native;
|
||||
}
|
||||
|
||||
// Slint's winit backend creates AppKit windows with transparent backing so
|
||||
// transparent Slint items can be composited. Keep the content out of the
|
||||
// titlebar and choose whether AppKit's material or the semantic window color
|
||||
@@ -331,9 +262,6 @@ struct FrameSlot {
|
||||
id<MTLBuffer> uv_buffer = nil;
|
||||
id<MTLTexture> y_texture = nil;
|
||||
id<MTLTexture> uv_texture = nil;
|
||||
CVMetalTextureRef y_cv_texture = nullptr;
|
||||
CVMetalTextureRef uv_cv_texture = nullptr;
|
||||
NativeVideoFrameRef native_frame;
|
||||
size_t y_stride = 0;
|
||||
size_t uv_stride = 0;
|
||||
int width = 0;
|
||||
@@ -346,15 +274,6 @@ struct FrameSlot {
|
||||
void ReleaseResources() {
|
||||
y_texture = nil;
|
||||
uv_texture = nil;
|
||||
if (y_cv_texture) {
|
||||
CFRelease(y_cv_texture);
|
||||
y_cv_texture = nullptr;
|
||||
}
|
||||
if (uv_cv_texture) {
|
||||
CFRelease(uv_cv_texture);
|
||||
uv_cv_texture = nullptr;
|
||||
}
|
||||
native_frame.Reset();
|
||||
y_buffer = nil;
|
||||
uv_buffer = nil;
|
||||
y_stride = 0;
|
||||
@@ -471,7 +390,7 @@ fragment half4 nv12_fragment(
|
||||
- 16.0 / 255.0);
|
||||
const float2 uv = uv_texture.sample(video_sampler, in.texcoord).rg - 0.5;
|
||||
// BT.601 limited-range NV12, matching the previous libyuv NV12ToABGR/RAW
|
||||
// path. MiniRTC's current MiniRtcVideoFrame API does not carry color metadata, so
|
||||
// path. MiniRTC's current XVideoFrame API does not carry color metadata, so
|
||||
// retaining the established matrix avoids a platform-only color change.
|
||||
const float3 rgb = float3(y + 1.59602678 * uv.y,
|
||||
y - 0.39176229 * uv.x - 0.81296764 * uv.y,
|
||||
@@ -487,7 +406,6 @@ struct MacMetalVideoRenderer::Impl {
|
||||
id<MTLDevice> device = nil;
|
||||
id<MTLCommandQueue> command_queue = nil;
|
||||
id<MTLRenderPipelineState> pipeline = nil;
|
||||
CVMetalTextureCacheRef texture_cache = nullptr;
|
||||
std::shared_ptr<SharedFrameState> frames =
|
||||
std::make_shared<SharedFrameState>();
|
||||
NSView* slint_view = nil;
|
||||
@@ -499,8 +417,6 @@ struct MacMetalVideoRenderer::Impl {
|
||||
uint64_t rendered_content_generation = 0;
|
||||
std::shared_ptr<std::atomic<bool>> native_surface_needs_redraw =
|
||||
std::make_shared<std::atomic<bool>>(true);
|
||||
std::atomic<bool> native_path_logged{false};
|
||||
std::atomic<bool> native_fallback_logged{false};
|
||||
|
||||
Impl() {
|
||||
@autoreleasepool {
|
||||
@@ -514,12 +430,6 @@ struct MacMetalVideoRenderer::Impl {
|
||||
LOG_ERROR("Metal video renderer unavailable: command queue creation failed");
|
||||
return;
|
||||
}
|
||||
const CVReturn cache_status = CVMetalTextureCacheCreate(
|
||||
kCFAllocatorDefault, nullptr, device, nullptr, &texture_cache);
|
||||
if (cache_status != kCVReturnSuccess || !texture_cache) {
|
||||
LOG_WARN("Metal video renderer could not create CV texture cache: {}",
|
||||
cache_status);
|
||||
}
|
||||
|
||||
NSError* library_error = nil;
|
||||
id<MTLLibrary> library =
|
||||
@@ -556,11 +466,6 @@ struct MacMetalVideoRenderer::Impl {
|
||||
~Impl() {
|
||||
DetachViews();
|
||||
frames.reset();
|
||||
if (texture_cache) {
|
||||
CVMetalTextureCacheFlush(texture_cache, 0);
|
||||
CFRelease(texture_cache);
|
||||
texture_cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Ready() const {
|
||||
@@ -750,142 +655,6 @@ MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNv12(
|
||||
return SubmitNv12Internal(remote_id, data, size, width, height, true);
|
||||
}
|
||||
|
||||
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitNativeFrame(
|
||||
std::string_view remote_id, const MiniRtcNativeVideoFrame& frame) {
|
||||
if (!IsReady()) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
const MiniRtcNativeVideoFrame* native = GetMacNativeFrame(frame);
|
||||
if (!native) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
if (impl_->frames->selected_stream != remote_id) {
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(native->width);
|
||||
const int height = static_cast<int>(native->height);
|
||||
const auto submit_cpu_fallback = [&]() {
|
||||
if (!impl_->native_fallback_logged.exchange(true,
|
||||
std::memory_order_acq_rel)) {
|
||||
LOG_WARN("macOS native CVPixelBuffer upload unavailable; using CPU NV12 "
|
||||
"fallback");
|
||||
}
|
||||
const size_t y_size = static_cast<size_t>(width) * height;
|
||||
std::vector<unsigned char> packed_nv12(y_size + y_size / 2);
|
||||
if (native->copy_to_nv12(native->owner, packed_nv12.data(),
|
||||
packed_nv12.size()) != 0) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
return SubmitNv12(remote_id, packed_nv12.data(), packed_nv12.size(), width,
|
||||
height);
|
||||
};
|
||||
|
||||
CVPixelBufferRef pixel_buffer =
|
||||
static_cast<CVPixelBufferRef>(native->payload.cv_pixel_buffer);
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
if (!CVPixelBufferIsPlanar(pixel_buffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2 ||
|
||||
CVPixelBufferGetWidth(pixel_buffer) != native->width ||
|
||||
CVPixelBufferGetHeight(pixel_buffer) != native->height ||
|
||||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
NativeVideoFrameRef retained(native);
|
||||
if (!retained || !impl_->texture_cache) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
CVMetalTextureRef y_cv_texture = nullptr;
|
||||
CVReturn y_status = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
|
||||
MTLPixelFormatR8Unorm, native->width, native->height, 0,
|
||||
&y_cv_texture);
|
||||
if (y_status != kCVReturnSuccess || !y_cv_texture) {
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
CVMetalTextureRef uv_cv_texture = nullptr;
|
||||
CVReturn uv_status = CVMetalTextureCacheCreateTextureFromImage(
|
||||
kCFAllocatorDefault, impl_->texture_cache, pixel_buffer, nullptr,
|
||||
MTLPixelFormatRG8Unorm, native->width / 2U, native->height / 2U, 1,
|
||||
&uv_cv_texture);
|
||||
if (uv_status != kCVReturnSuccess || !uv_cv_texture) {
|
||||
CFRelease(y_cv_texture);
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
id<MTLTexture> y_texture = CVMetalTextureGetTexture(y_cv_texture);
|
||||
id<MTLTexture> uv_texture = CVMetalTextureGetTexture(uv_cv_texture);
|
||||
if (y_texture == nil || uv_texture == nil) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return submit_cpu_fallback();
|
||||
}
|
||||
|
||||
auto& frames = *impl_->frames;
|
||||
std::lock_guard lock(frames.mutex);
|
||||
if (frames.selected_stream != remote_id) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return SubmitResult::not_selected;
|
||||
}
|
||||
|
||||
// Latest-frame-only queue: replace an unconsumed decoded frame to bound
|
||||
// latency, while never recycling a frame still referenced by the GPU.
|
||||
FrameSlot* target = nullptr;
|
||||
for (auto& slot : frames.slots) {
|
||||
if (slot.use == SlotUse::pending) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
uint64_t oldest_sequence = std::numeric_limits<uint64_t>::max();
|
||||
for (auto& slot : frames.slots) {
|
||||
if (slot.use != SlotUse::available) {
|
||||
continue;
|
||||
}
|
||||
if (!slot.valid) {
|
||||
target = &slot;
|
||||
break;
|
||||
}
|
||||
if (slot.sequence < oldest_sequence) {
|
||||
oldest_sequence = slot.sequence;
|
||||
target = &slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target) {
|
||||
CFRelease(y_cv_texture);
|
||||
CFRelease(uv_cv_texture);
|
||||
return SubmitResult::dropped;
|
||||
}
|
||||
|
||||
target->ReleaseResources();
|
||||
target->y_cv_texture = y_cv_texture;
|
||||
target->uv_cv_texture = uv_cv_texture;
|
||||
target->y_texture = y_texture;
|
||||
target->uv_texture = uv_texture;
|
||||
target->native_frame = std::move(retained);
|
||||
target->width = width;
|
||||
target->height = height;
|
||||
target->remote_id.assign(remote_id);
|
||||
target->sequence = frames.next_sequence++;
|
||||
target->use = SlotUse::pending;
|
||||
target->valid = true;
|
||||
if (!impl_->native_path_logged.exchange(true, std::memory_order_acq_rel)) {
|
||||
LOG_INFO("macOS VideoToolbox-to-Metal native NV12 path enabled");
|
||||
}
|
||||
return SubmitResult::submitted;
|
||||
}
|
||||
|
||||
MacMetalVideoRenderer::SubmitResult MacMetalVideoRenderer::SubmitCachedNv12(
|
||||
std::string_view remote_id, const uint8_t* data, size_t size, int width,
|
||||
int height) {
|
||||
@@ -1276,57 +1045,40 @@ bool MacMetalVideoRenderer::CopyLatestNv12(
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeVideoFrameRef native_frame;
|
||||
{
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot* newest = nullptr;
|
||||
for (const auto& slot : impl_->frames->slots) {
|
||||
if (!slot.valid || slot.remote_id != remote_id || slot.sequence == 0 ||
|
||||
(!slot.native_frame &&
|
||||
(slot.y_buffer == nil || slot.uv_buffer == nil))) {
|
||||
continue;
|
||||
}
|
||||
if (newest == nullptr || slot.sequence > newest->sequence) {
|
||||
newest = &slot;
|
||||
}
|
||||
std::lock_guard lock(impl_->frames->mutex);
|
||||
const FrameSlot* newest = nullptr;
|
||||
for (const auto& slot : impl_->frames->slots) {
|
||||
if (!slot.valid || slot.remote_id != remote_id || slot.y_buffer == nil ||
|
||||
slot.uv_buffer == nil || slot.sequence == 0) {
|
||||
continue;
|
||||
}
|
||||
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
native_frame = newest->native_frame;
|
||||
if (!native_frame) {
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(newest->width) * newest->height;
|
||||
output->resize(y_size + y_size / 2);
|
||||
auto* y_source =
|
||||
static_cast<const uint8_t*>(newest->y_buffer.contents);
|
||||
auto* uv_source =
|
||||
static_cast<const uint8_t*>(newest->uv_buffer.contents);
|
||||
for (int row = 0; row < newest->height; ++row) {
|
||||
std::memcpy(output->data() +
|
||||
static_cast<size_t>(row) * newest->width,
|
||||
y_source + static_cast<size_t>(row) * newest->y_stride,
|
||||
newest->width);
|
||||
}
|
||||
uint8_t* uv_destination = output->data() + y_size;
|
||||
for (int row = 0; row < newest->height / 2; ++row) {
|
||||
std::memcpy(uv_destination +
|
||||
static_cast<size_t>(row) * newest->width,
|
||||
uv_source + static_cast<size_t>(row) * newest->uv_stride,
|
||||
newest->width);
|
||||
}
|
||||
return true;
|
||||
if (newest == nullptr || slot.sequence > newest->sequence) {
|
||||
newest = &slot;
|
||||
}
|
||||
}
|
||||
if (newest == nullptr || newest->width <= 0 || newest->height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const MiniRtcNativeVideoFrame* native = native_frame.Get();
|
||||
const size_t y_size = static_cast<size_t>(native->width) * native->height;
|
||||
const size_t y_size =
|
||||
static_cast<size_t>(newest->width) * newest->height;
|
||||
output->resize(y_size + y_size / 2);
|
||||
return native->copy_to_nv12(native->owner, output->data(), output->size()) ==
|
||||
0;
|
||||
auto* y_source = static_cast<const uint8_t*>(newest->y_buffer.contents);
|
||||
auto* uv_source = static_cast<const uint8_t*>(newest->uv_buffer.contents);
|
||||
for (int row = 0; row < newest->height; ++row) {
|
||||
std::memcpy(output->data() + static_cast<size_t>(row) * newest->width,
|
||||
y_source + static_cast<size_t>(row) * newest->y_stride,
|
||||
newest->width);
|
||||
}
|
||||
uint8_t* uv_destination = output->data() + y_size;
|
||||
for (int row = 0; row < newest->height / 2; ++row) {
|
||||
std::memcpy(uv_destination + static_cast<size_t>(row) * newest->width,
|
||||
uv_source + static_cast<size_t>(row) * newest->uv_stride,
|
||||
newest->width);
|
||||
}
|
||||
*width = newest->width;
|
||||
*height = newest->height;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -17,9 +17,6 @@ namespace crossdesk {
|
||||
|
||||
struct MacTrayImpl;
|
||||
|
||||
// Activates the app and focuses the restored Slint window's current NSView.
|
||||
void MacActivateWindow(void* appkit_view);
|
||||
|
||||
class MacTray {
|
||||
public:
|
||||
MacTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
|
||||
@@ -57,8 +57,6 @@ struct MacTrayImpl {
|
||||
} else if (app_window) {
|
||||
SDL_HideWindow(app_window);
|
||||
}
|
||||
// Keep running through the menu-bar tray without a Dock application icon.
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
||||
}
|
||||
|
||||
void RemoveTrayIcon() {
|
||||
@@ -71,8 +69,6 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
void ShowWindow() {
|
||||
// Restore normal application behavior before showing or focusing a window.
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
if (show_window) {
|
||||
show_window();
|
||||
} else if (app_window) {
|
||||
@@ -146,7 +142,6 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
void OpenSettings() {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
if (open_settings) {
|
||||
open_settings();
|
||||
} else {
|
||||
@@ -284,13 +279,6 @@ MacTray::MacTray(std::function<void()> show_window,
|
||||
|
||||
MacTray::~MacTray() = default;
|
||||
|
||||
void MacActivateWindow(void *appkit_view) {
|
||||
NSView *view = (__bridge NSView *)appkit_view;
|
||||
NSWindow *window = [view window];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
[window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
|
||||
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
|
||||
|
||||
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
|
||||
@@ -42,65 +42,6 @@ static std::string NSErrorToString(NSError *error) {
|
||||
return description ? description : "";
|
||||
}
|
||||
|
||||
static void RetainCapturedPixelBuffer(void *owner) {
|
||||
if (owner) {
|
||||
CVPixelBufferRetain(static_cast<CVPixelBufferRef>(owner));
|
||||
}
|
||||
}
|
||||
|
||||
static void ReleaseCapturedPixelBuffer(void *owner) {
|
||||
if (owner) {
|
||||
CVPixelBufferRelease(static_cast<CVPixelBufferRef>(owner));
|
||||
}
|
||||
}
|
||||
|
||||
static int CopyCapturedPixelBufferToNv12(void *owner, uint8_t *destination,
|
||||
size_t destination_size) {
|
||||
auto pixel_buffer = static_cast<CVPixelBufferRef>(owner);
|
||||
if (!pixel_buffer || !destination ||
|
||||
!CVPixelBufferIsPlanar(pixel_buffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixel_buffer) < 2) {
|
||||
return -1;
|
||||
}
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixel_buffer);
|
||||
if (pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t width = CVPixelBufferGetWidth(pixel_buffer);
|
||||
const size_t height = CVPixelBufferGetHeight(pixel_buffer);
|
||||
const size_t required_size = width * height * 3U / 2U;
|
||||
if (destination_size < required_size ||
|
||||
CVPixelBufferLockBaseAddress(pixel_buffer,
|
||||
kCVPixelBufferLock_ReadOnly) !=
|
||||
kCVReturnSuccess) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto *y_plane = static_cast<const uint8_t *>(
|
||||
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 0));
|
||||
const auto *uv_plane = static_cast<const uint8_t *>(
|
||||
CVPixelBufferGetBaseAddressOfPlane(pixel_buffer, 1));
|
||||
const size_t y_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 0);
|
||||
const size_t uv_stride = CVPixelBufferGetBytesPerRowOfPlane(pixel_buffer, 1);
|
||||
int result = 0;
|
||||
if (!y_plane || !uv_plane || y_stride < width || uv_stride < width) {
|
||||
result = -1;
|
||||
} else {
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
std::memcpy(destination + row * width, y_plane + row * y_stride, width);
|
||||
}
|
||||
uint8_t *uv_destination = destination + width * height;
|
||||
for (size_t row = 0; row < height / 2U; ++row) {
|
||||
std::memcpy(uv_destination + row * width,
|
||||
uv_plane + row * uv_stride, width);
|
||||
}
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
||||
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
||||
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
||||
@@ -151,11 +92,14 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::map<int, CGDirectDisplayID> display_id_map_;
|
||||
unsigned char *nv12_frame_ = nullptr;
|
||||
size_t nv12_frame_size_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 60;
|
||||
bool show_cursor_ = false;
|
||||
bool capture_requested_ = false;
|
||||
bool invalid_stream_id_logged_ = false;
|
||||
bool native_output_logged_ = false;
|
||||
|
||||
public:
|
||||
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
|
||||
@@ -175,8 +119,8 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
||||
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
||||
SckHelper *__strong helper_ = nil;
|
||||
// Callback for returning captured frames to the caller. ScreenCaptureKit may
|
||||
// invoke it on its sample-handler queue.
|
||||
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
||||
// thread.
|
||||
cb_desktop_data _on_data = nullptr;
|
||||
// Signals that a permanent error occurred. This may be set on any thread, and is read by
|
||||
// CaptureFrame() which runs on the caller's thread.
|
||||
@@ -300,6 +244,12 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_frame_size_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
@@ -312,7 +262,6 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps > 0 ? fps : 60;
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
native_output_logged_ = false;
|
||||
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||
@@ -647,69 +596,81 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
const size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
const OSType pixel_format = CVPixelBufferGetPixelFormatType(pixelBuffer);
|
||||
if (width == 0 || height == 0 || (width & 1U) != 0 ||
|
||||
(height & 1U) != 0 || !CVPixelBufferIsPlanar(pixelBuffer) ||
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer) < 2 ||
|
||||
(pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange &&
|
||||
pixel_format != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, format={}, planes={}",
|
||||
width, height, pixel_format,
|
||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t required_size = width * height * 3U / 2U;
|
||||
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (status != kCVReturnSuccess) {
|
||||
LOG_ERROR("Failed to lock CVPixelBuffer base address: %d", status);
|
||||
return;
|
||||
}
|
||||
|
||||
cb_desktop_data callback;
|
||||
std::string stream_id;
|
||||
bool log_native_output = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data || !capture_requested_) {
|
||||
return;
|
||||
}
|
||||
stream_id = ResolveDisplayStreamId(current_stream_id_.c_str(),
|
||||
display_info_list_.size(),
|
||||
current_monitor_index_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_) {
|
||||
LOG_ERROR("Dropping macOS frames without a registered stream id, "
|
||||
"display_id={}",
|
||||
current_display_);
|
||||
invalid_stream_id_logged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_ = false;
|
||||
callback = _on_data;
|
||||
if (!native_output_logged_) {
|
||||
native_output_logged_ = true;
|
||||
log_native_output = true;
|
||||
}
|
||||
size_t required_size = width * height * 3 / 2;
|
||||
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
MiniRtcNativeVideoFrame native_frame{};
|
||||
native_frame.struct_size = sizeof(native_frame);
|
||||
native_frame.type = MiniRtcNativeVideoFrameCVPixelBuffer;
|
||||
native_frame.width = static_cast<uint32_t>(width);
|
||||
native_frame.height = static_cast<uint32_t>(height);
|
||||
native_frame.payload.cv_pixel_buffer = pixelBuffer;
|
||||
native_frame.owner = pixelBuffer;
|
||||
native_frame.retain = &RetainCapturedPixelBuffer;
|
||||
native_frame.release = &ReleaseCapturedPixelBuffer;
|
||||
native_frame.copy_to_nv12 = &CopyCapturedPixelBufferToNv12;
|
||||
if (log_native_output) {
|
||||
LOG_INFO("macOS ScreenCaptureKit native CVPixelBuffer output enabled");
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data || !capture_requested_) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
callback(nullptr, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height), stream_id.c_str(), &native_frame);
|
||||
|
||||
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[required_size];
|
||||
nv12_frame_size_ = required_size;
|
||||
}
|
||||
width_ = static_cast<int>(width);
|
||||
height_ = static_cast<int>(height);
|
||||
|
||||
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
|
||||
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
|
||||
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char *dst_y = nv12_frame_;
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
memcpy(dst_y + row * width, static_cast<unsigned char *>(base_y) + row * stride_y, width);
|
||||
}
|
||||
|
||||
unsigned char *dst_uv = nv12_frame_ + width * height;
|
||||
for (size_t row = 0; row < height / 2; ++row) {
|
||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||
}
|
||||
|
||||
const std::string stream_id = ResolveDisplayStreamId(
|
||||
current_stream_id_.c_str(), display_info_list_.size(),
|
||||
current_monitor_index_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_) {
|
||||
LOG_ERROR("Dropping macOS frames without a registered stream id, "
|
||||
"display_id={}",
|
||||
current_display_);
|
||||
invalid_stream_id_logged_ = true;
|
||||
}
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_ = false;
|
||||
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height), stream_id.c_str());
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
|
||||
@@ -17,19 +17,21 @@ namespace crossdesk {
|
||||
class SpeakerCapturerMacosx : public SpeakerCapturer {
|
||||
public:
|
||||
SpeakerCapturerMacosx();
|
||||
~SpeakerCapturerMacosx() override;
|
||||
~SpeakerCapturerMacosx();
|
||||
|
||||
public:
|
||||
int Init(speaker_data_cb cb) override;
|
||||
int Destroy() override;
|
||||
int Start() override;
|
||||
int Stop() override;
|
||||
bool IsRunning() const override;
|
||||
virtual int Init(speaker_data_cb cb);
|
||||
virtual int Destroy();
|
||||
virtual int Start();
|
||||
virtual int Stop();
|
||||
|
||||
int Pause();
|
||||
int Resume();
|
||||
|
||||
private:
|
||||
public:
|
||||
speaker_data_cb cb_ = nullptr;
|
||||
bool inited_ = false;
|
||||
|
||||
class Impl;
|
||||
Impl* impl_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -2,98 +2,47 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "speaker_capturer_macosx.h"
|
||||
|
||||
namespace {
|
||||
constexpr auto kCaptureTimeout = std::chrono::seconds(2);
|
||||
|
||||
std::string NSErrorToString(NSError* error) {
|
||||
if (!error) return "";
|
||||
NSString* message = [NSString
|
||||
stringWithFormat:@"%@ (%@:%ld), reason=%@", error.localizedDescription, error.domain,
|
||||
(long)error.code, error.localizedFailureReason ?: @""];
|
||||
return message.UTF8String ?: "";
|
||||
namespace crossdesk {
|
||||
class SpeakerCapturerMacosx;
|
||||
}
|
||||
|
||||
// Late system replies own only their result, never the capturer or stack data.
|
||||
// Complete() also tells a late successful start to stop its abandoned stream.
|
||||
template <typename T>
|
||||
class CaptureReply {
|
||||
public:
|
||||
bool Complete(T value) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (expired_) return false;
|
||||
value_ = std::move(value);
|
||||
ready_ = true;
|
||||
wake_.notify_one();
|
||||
return true;
|
||||
}
|
||||
bool Wait(T& value) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
if (!wake_.wait_for(lock, kCaptureTimeout, [&] { return ready_; })) {
|
||||
expired_ = true;
|
||||
return false;
|
||||
}
|
||||
value = value_;
|
||||
return true;
|
||||
namespace {
|
||||
std::string NSErrorToString(NSError* error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool ready_ = false;
|
||||
bool expired_ = false;
|
||||
T value_{};
|
||||
};
|
||||
|
||||
struct ContentReply {
|
||||
SCShareableContent* content = nil;
|
||||
NSError* error = nil;
|
||||
};
|
||||
struct StreamReply {
|
||||
NSError* error = nil;
|
||||
};
|
||||
struct AudioCallbackState {
|
||||
std::mutex mutex;
|
||||
crossdesk::SpeakerCapturer::speaker_data_cb callback;
|
||||
std::atomic<bool> running{false};
|
||||
};
|
||||
const char* description = [error.localizedDescription UTF8String];
|
||||
return description ? description : "";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput> {
|
||||
std::shared_ptr<AudioCallbackState> state_;
|
||||
}
|
||||
- (instancetype)initWithState:(std::shared_ptr<AudioCallbackState>)state;
|
||||
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
||||
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
|
||||
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
|
||||
@end
|
||||
|
||||
@implementation SpeakerCaptureDelegate
|
||||
- (instancetype)initWithState:(std::shared_ptr<AudioCallbackState>)state {
|
||||
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner {
|
||||
self = [super init];
|
||||
if (self) state_ = std::move(state);
|
||||
if (self) {
|
||||
_owner = owner;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
|
||||
state_->running = false;
|
||||
SPDLOG_LOGGER_WARN(crossdesk::get_logger(), "System audio stream stopped: {}",
|
||||
NSErrorToString(error));
|
||||
}
|
||||
|
||||
- (void)stream:(SCStream*)stream
|
||||
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
||||
ofType:(SCStreamOutputType)type {
|
||||
if (type != SCStreamOutputTypeAudio) return;
|
||||
const auto state = state_;
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
if (!state->callback || !state->running) return;
|
||||
|
||||
crossdesk::SpeakerCapturerMacosx* owner = _owner;
|
||||
if (!owner || !owner->cb_) {
|
||||
return;
|
||||
}
|
||||
|
||||
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
||||
if (!blockBuffer) {
|
||||
@@ -102,7 +51,8 @@ struct AudioCallbackState {
|
||||
|
||||
size_t length = CMBlockBufferGetDataLength(blockBuffer);
|
||||
char* dataPtr = NULL;
|
||||
OSStatus dataStatus = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
OSStatus dataStatus =
|
||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
if (dataStatus != noErr || dataPtr == nullptr || length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -118,7 +68,7 @@ struct AudioCallbackState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->callback) {
|
||||
if (owner->cb_) {
|
||||
std::vector<short> out_pcm16;
|
||||
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
||||
int channels = asbd->mChannelsPerFrame;
|
||||
@@ -168,156 +118,191 @@ struct AudioCallbackState {
|
||||
size_t total_bytes = out_pcm16.size() * sizeof(short);
|
||||
unsigned char* p = (unsigned char*)out_pcm16.data();
|
||||
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
|
||||
if (!state->callback) {
|
||||
if (!owner->cb_) {
|
||||
return;
|
||||
}
|
||||
state->callback(p + offset, frame_bytes, "audio");
|
||||
owner->cb_(p + offset, frame_bytes, "audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class SpeakerCapturerMacosx::Impl {
|
||||
public:
|
||||
speaker_data_cb callback;
|
||||
SCStreamConfiguration* config = nil;
|
||||
SCStream* stream = nil;
|
||||
SpeakerCaptureDelegate* delegate = nil;
|
||||
dispatch_queue_t queue = nil;
|
||||
std::shared_ptr<AudioCallbackState> state;
|
||||
SCShareableContent* content = nil;
|
||||
SCDisplay* mainDisplay = nil;
|
||||
|
||||
~Impl() {
|
||||
if (stream) {
|
||||
[stream stopCaptureWithCompletionHandler:^(NSError* _Nullable error){
|
||||
}];
|
||||
stream = nil;
|
||||
}
|
||||
delegate = nil;
|
||||
if (queue) {
|
||||
queue = nil;
|
||||
}
|
||||
content = nil;
|
||||
mainDisplay = nil;
|
||||
config = nil;
|
||||
}
|
||||
};
|
||||
|
||||
SpeakerCapturerMacosx::SpeakerCapturerMacosx() : impl_(new Impl) {}
|
||||
SpeakerCapturerMacosx::SpeakerCapturerMacosx() {
|
||||
impl_ = new Impl();
|
||||
inited_ = false;
|
||||
cb_ = nullptr;
|
||||
}
|
||||
|
||||
SpeakerCapturerMacosx::~SpeakerCapturerMacosx() {
|
||||
Destroy();
|
||||
delete impl_;
|
||||
impl_ = nullptr;
|
||||
}
|
||||
|
||||
int SpeakerCapturerMacosx::Init(speaker_data_cb cb) {
|
||||
impl_->callback = std::move(cb);
|
||||
if (!impl_->queue)
|
||||
if (inited_) {
|
||||
return 0;
|
||||
}
|
||||
cb_ = cb;
|
||||
|
||||
impl_->config = [[SCStreamConfiguration alloc] init];
|
||||
impl_->config.capturesAudio = YES;
|
||||
impl_->config.sampleRate = 48000;
|
||||
impl_->config.channelCount = 1;
|
||||
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
__block NSError* error = nil;
|
||||
[SCShareableContent
|
||||
getShareableContentWithCompletionHandler:^(SCShareableContent* c, NSError* e) {
|
||||
impl_->content = c;
|
||||
error = e;
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (error || !impl_->content) {
|
||||
LOG_ERROR("Failed to get shareable content: {}",
|
||||
NSErrorToString(error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID mainDisplayId = CGMainDisplayID();
|
||||
impl_->mainDisplay = nil;
|
||||
for (SCDisplay* d in impl_->content.displays) {
|
||||
if (d.displayID == mainDisplayId) {
|
||||
impl_->mainDisplay = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!impl_->mainDisplay) {
|
||||
LOG_ERROR("Main display not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!impl_->queue) {
|
||||
impl_->queue = dispatch_queue_create("SpeakerAudio.Queue", DISPATCH_QUEUE_SERIAL);
|
||||
}
|
||||
|
||||
inited_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerMacosx::Start() {
|
||||
@autoreleasepool {
|
||||
if (!impl_->callback) return -1;
|
||||
if (IsRunning()) return 0;
|
||||
if (Stop() != 0) return -1;
|
||||
|
||||
// A cached SCDisplay becomes invalid after hotplug, sleep or a display
|
||||
// reconfiguration. Obtain a fresh snapshot on every start/reconnection.
|
||||
auto content_reply = std::make_shared<CaptureReply<ContentReply>>();
|
||||
[SCShareableContent
|
||||
getShareableContentWithCompletionHandler:^(SCShareableContent* content, NSError* error) {
|
||||
content_reply->Complete({content, error});
|
||||
}];
|
||||
ContentReply content;
|
||||
if (!content_reply->Wait(content)) {
|
||||
LOG_ERROR("Timed out refreshing system audio capture displays");
|
||||
return -1;
|
||||
}
|
||||
if (content.error || !content.content) {
|
||||
LOG_ERROR("Failed to refresh system audio capture displays: {}",
|
||||
NSErrorToString(content.error));
|
||||
return -1;
|
||||
}
|
||||
SCDisplay* display = nil;
|
||||
const auto main_id = CGMainDisplayID();
|
||||
for (SCDisplay* candidate in content.content.displays) {
|
||||
if (candidate.displayID == main_id) {
|
||||
display = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!display) display = content.content.displays.firstObject;
|
||||
if (!display) {
|
||||
LOG_ERROR("No display available for system audio capture");
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO("Starting system audio capture with current display {}", display.displayID);
|
||||
|
||||
SCStreamConfiguration* config = [[SCStreamConfiguration alloc] init];
|
||||
config.capturesAudio = YES;
|
||||
config.sampleRate = 48000;
|
||||
config.channelCount = 1;
|
||||
SCContentFilter* filter = [[SCContentFilter alloc] initWithDisplay:display
|
||||
excludingWindows:@[]];
|
||||
impl_->state = std::make_shared<AudioCallbackState>();
|
||||
impl_->state->callback = impl_->callback;
|
||||
impl_->delegate = [[SpeakerCaptureDelegate alloc] initWithState:impl_->state];
|
||||
impl_->stream = [[SCStream alloc] initWithFilter:filter
|
||||
configuration:config
|
||||
delegate:impl_->delegate];
|
||||
NSError* output_error = nil;
|
||||
if (!impl_->stream || ![impl_->stream addStreamOutput:impl_->delegate
|
||||
type:SCStreamOutputTypeAudio
|
||||
sampleHandlerQueue:impl_->queue
|
||||
error:&output_error]) {
|
||||
LOG_ERROR("Failed to attach system audio output: {}", NSErrorToString(output_error));
|
||||
Stop();
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto start_reply = std::make_shared<CaptureReply<StreamReply>>();
|
||||
SCStream* stream = impl_->stream;
|
||||
impl_->state->running = true;
|
||||
[stream startCaptureWithCompletionHandler:^(NSError* error) {
|
||||
if (!start_reply->Complete({error}) && !error) {
|
||||
[stream stopCaptureWithCompletionHandler:nil];
|
||||
}
|
||||
}];
|
||||
StreamReply started;
|
||||
if (!start_reply->Wait(started)) {
|
||||
LOG_ERROR("Timed out starting system audio capture");
|
||||
Stop();
|
||||
return -1;
|
||||
}
|
||||
if (started.error || !impl_->state->running) {
|
||||
LOG_ERROR("Failed to start system audio capture: {}", NSErrorToString(started.error));
|
||||
Stop();
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
if (!inited_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (impl_->stream) {
|
||||
dispatch_semaphore_t semaStop = dispatch_semaphore_create(0);
|
||||
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||
dispatch_semaphore_signal(semaStop);
|
||||
}];
|
||||
dispatch_semaphore_wait(semaStop, DISPATCH_TIME_FOREVER);
|
||||
impl_->stream = nil;
|
||||
impl_->delegate = nil;
|
||||
}
|
||||
|
||||
impl_->delegate = [[SpeakerCaptureDelegate alloc] initWithOwner:this];
|
||||
SCContentFilter* filter = [[SCContentFilter alloc] initWithDisplay:impl_->mainDisplay
|
||||
excludingWindows:@[]];
|
||||
impl_->stream = [[SCStream alloc] initWithFilter:filter
|
||||
configuration:impl_->config
|
||||
delegate:impl_->delegate];
|
||||
|
||||
NSError* addOutputError = nil;
|
||||
BOOL ok = [impl_->stream addStreamOutput:impl_->delegate
|
||||
type:SCStreamOutputTypeAudio
|
||||
sampleHandlerQueue:impl_->queue
|
||||
error:&addOutputError];
|
||||
if (!ok || addOutputError) {
|
||||
LOG_ERROR("addStreamOutput error: {}",
|
||||
NSErrorToString(addOutputError));
|
||||
impl_->stream = nil;
|
||||
impl_->delegate = nil;
|
||||
return -1;
|
||||
}
|
||||
|
||||
dispatch_semaphore_t semaStart = dispatch_semaphore_create(0);
|
||||
__block int ret = 0;
|
||||
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
|
||||
if (error) {
|
||||
LOG_ERROR("startCaptureWithCompletionHandler error: {}",
|
||||
NSErrorToString(error));
|
||||
ret = -1;
|
||||
}
|
||||
dispatch_semaphore_signal(semaStart);
|
||||
}];
|
||||
dispatch_semaphore_wait(semaStart, DISPATCH_TIME_FOREVER);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int SpeakerCapturerMacosx::Stop() {
|
||||
@autoreleasepool {
|
||||
if (auto state = std::exchange(impl_->state, {})) {
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->running = false;
|
||||
state->callback = nullptr;
|
||||
}
|
||||
SCStream* stream = impl_->stream;
|
||||
impl_->stream = nil;
|
||||
impl_->delegate = nil;
|
||||
if (!stream) return 0;
|
||||
auto stop_reply = std::make_shared<CaptureReply<StreamReply>>();
|
||||
[stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||
stop_reply->Complete({error});
|
||||
}];
|
||||
StreamReply stopped;
|
||||
if (!stop_reply->Wait(stopped)) {
|
||||
LOG_ERROR("Timed out stopping system audio capture; callbacks revoked");
|
||||
return -1;
|
||||
}
|
||||
// A start failure or daemon restart can leave an already stopped stream.
|
||||
// Its references have been discarded, so future starts can recover.
|
||||
if (stopped.error) LOG_WARN("System audio stop reply: {}", NSErrorToString(stopped.error));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (!inited_) return -1;
|
||||
if (!impl_->stream) return -1;
|
||||
|
||||
bool SpeakerCapturerMacosx::IsRunning() const { return impl_->state && impl_->state->running; }
|
||||
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
|
||||
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||
if (error) {
|
||||
LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
|
||||
NSErrorToString(error));
|
||||
}
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
impl_->stream = nil;
|
||||
impl_->delegate.owner = nullptr;
|
||||
impl_->delegate = nil;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerMacosx::Destroy() {
|
||||
const int result = Stop();
|
||||
impl_->callback = nullptr;
|
||||
impl_->queue = nil;
|
||||
return result;
|
||||
Stop();
|
||||
cb_ = nullptr;
|
||||
|
||||
if (impl_) {
|
||||
impl_->config = nil;
|
||||
impl_->content = nil;
|
||||
impl_->mainDisplay = nil;
|
||||
if (impl_->queue) {
|
||||
impl_->queue = nil;
|
||||
}
|
||||
}
|
||||
inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
int SpeakerCapturerMacosx::Pause() { return Stop(); }
|
||||
|
||||
int SpeakerCapturerMacosx::Pause() { return 0; }
|
||||
|
||||
int SpeakerCapturerMacosx::Resume() { return Start(); }
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SYSTEM_INFO_H_
|
||||
#define _SYSTEM_INFO_H_
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
#include <ifaddrs.h>
|
||||
@@ -62,6 +53,3 @@ std::string GetHostName() {
|
||||
bool IsWaylandSession() { return false; }
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
#endif
|
||||
@@ -58,10 +58,8 @@ public:
|
||||
virtual SubmitResult SubmitCachedNv12(std::string_view remote_id,
|
||||
const uint8_t* data, size_t size,
|
||||
int width, int height) = 0;
|
||||
// Retains a platform-native decoded frame when the backend supports its
|
||||
// storage and handle types. Unsupported backends keep the default fallback.
|
||||
virtual SubmitResult SubmitNativeFrame(std::string_view,
|
||||
const MiniRtcNativeVideoFrame&) {
|
||||
const XVideoFrame&) {
|
||||
return SubmitResult::failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
struct HandleCloser {
|
||||
void operator()(HANDLE handle) const noexcept {
|
||||
if (handle != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
using ScopedHandle = std::unique_ptr<void, HandleCloser>;
|
||||
|
||||
std::string ErrorCode(DWORD code) {
|
||||
std::ostringstream text;
|
||||
text << "0x" << std::hex << std::uppercase << code;
|
||||
return text.str();
|
||||
}
|
||||
|
||||
std::string ProbeSlintRenderer(DWORD timeout_ms) {
|
||||
std::wstring executable(32768, L'\0');
|
||||
const DWORD length = GetModuleFileNameW(
|
||||
nullptr, executable.data(), static_cast<DWORD>(executable.size()));
|
||||
if (length == 0 || length >= executable.size()) {
|
||||
return "Unable to locate renderer probe executable";
|
||||
}
|
||||
executable.resize(length);
|
||||
|
||||
// A job also cleans up the probe if the application is closed while the
|
||||
// graphics driver is stuck. Create it suspended so assignment cannot race.
|
||||
ScopedHandle job(CreateJobObjectW(nullptr, nullptr));
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if (!job ||
|
||||
!SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
|
||||
&limits, sizeof(limits))) {
|
||||
return "Unable to create renderer probe job: " + ErrorCode(GetLastError());
|
||||
}
|
||||
|
||||
constexpr std::string_view argument = kSlintRendererProbeArgument;
|
||||
std::wstring command = L"\"" + executable + L"\" ";
|
||||
command.append(argument.begin(), argument.end());
|
||||
STARTUPINFOW startup = {sizeof(startup)};
|
||||
PROCESS_INFORMATION process = {};
|
||||
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr,
|
||||
FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr,
|
||||
nullptr, &startup, &process)) {
|
||||
return "Unable to start renderer probe: " + ErrorCode(GetLastError());
|
||||
}
|
||||
ScopedHandle process_handle(process.hProcess);
|
||||
ScopedHandle thread_handle(process.hThread);
|
||||
if (!AssignProcessToJobObject(job.get(), process.hProcess) ||
|
||||
ResumeThread(process.hThread) == static_cast<DWORD>(-1)) {
|
||||
const DWORD error = GetLastError();
|
||||
TerminateProcess(process.hProcess, 1);
|
||||
return "Unable to run renderer probe: " + ErrorCode(error);
|
||||
}
|
||||
|
||||
const DWORD wait = WaitForSingleObject(process.hProcess, timeout_ms);
|
||||
if (wait != WAIT_OBJECT_0) {
|
||||
// Closing the job kills only our probe, including any stuck driver thread.
|
||||
return wait == WAIT_TIMEOUT ? "Slint renderer probe timed out"
|
||||
: "Waiting for renderer probe failed: " +
|
||||
ErrorCode(GetLastError());
|
||||
}
|
||||
DWORD exit_code = 0;
|
||||
if (!GetExitCodeProcess(process.hProcess, &exit_code)) {
|
||||
return "Unable to read renderer probe result: " + ErrorCode(GetLastError());
|
||||
}
|
||||
return exit_code == 0
|
||||
? std::string{}
|
||||
: "Slint renderer probe exited with " + ErrorCode(exit_code);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SlintBackendSelection ConfigureWindowsSlintBackend(uint32_t probe_timeout_ms) {
|
||||
if (const char* requested = std::getenv("SLINT_BACKEND");
|
||||
requested && requested[0] != '\0') {
|
||||
return {true, requested, "Using explicitly requested Slint backend"};
|
||||
}
|
||||
|
||||
// Slint creates its own GL context. A successful SDL/WGL capability check
|
||||
// cannot validate it, and a Rust panic cannot be caught by C++ exceptions.
|
||||
// Exercise Slint in a separate process without touching GL in this process.
|
||||
const std::string error = ProbeSlintRenderer(probe_timeout_ms);
|
||||
if (error.empty()) {
|
||||
return {true, "default", "Slint renderer probe rendered successfully"};
|
||||
}
|
||||
|
||||
// _putenv_s updates the CRT and the OS environment read by slint_cpp.dll.
|
||||
// This is process-local so a later launch can retry hardware rendering.
|
||||
if (_putenv_s("SLINT_BACKEND", "winit-software") != 0) {
|
||||
return {false, {}, "Unable to select Slint software rendering: " + error};
|
||||
}
|
||||
return {true, "winit-software", error};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_BACKEND_H_
|
||||
#define _SLINT_BACKEND_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SlintBackendSelection {
|
||||
bool success = true;
|
||||
std::string backend;
|
||||
std::string diagnostic;
|
||||
};
|
||||
|
||||
inline constexpr char kSlintRendererProbeArgument[] = "--slint-renderer-probe";
|
||||
|
||||
// Call before creating any Slint components. The executable's main() must
|
||||
// dispatch kSlintRendererProbeArgument before services, settings or the daemon.
|
||||
SlintBackendSelection ConfigureWindowsSlintBackend(
|
||||
uint32_t probe_timeout_ms = 10000);
|
||||
|
||||
// Internal child-process entry point; never call in the main application.
|
||||
int RunSlintRendererProbe();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,59 +0,0 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "platform/windows/gui/slint_backend.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int RunSlintRendererProbe() {
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = 2;
|
||||
{
|
||||
// Reuse the application's single AOT entry point. Independently generated
|
||||
// headers can contain colliding internal component names across TUs.
|
||||
auto probe = ui::MainWindow::create();
|
||||
probe->set_custom_titlebar(true);
|
||||
// AfterRendering confirms that the real GL context, shader initialization
|
||||
// and first draw all succeeded. GPU readback from an off-screen window can
|
||||
// be black even when rendering works, so do not use its pixels as a test.
|
||||
const auto notifier_error = probe->window().set_rendering_notifier(
|
||||
[&](slint::RenderingState state, slint::GraphicsAPI) {
|
||||
if (state == slint::RenderingState::AfterRendering) {
|
||||
result = 0;
|
||||
}
|
||||
});
|
||||
probe->window().set_position(
|
||||
slint::PhysicalPosition(slint::Point<int32_t>{-32000, -32000}));
|
||||
probe->show();
|
||||
slint::Timer timer;
|
||||
timer.start(slint::TimerMode::SingleShot, std::chrono::milliseconds(100),
|
||||
[&] {
|
||||
// Renderers without a rendering notifier (software) support
|
||||
// snapshots.
|
||||
if (notifier_error) {
|
||||
const auto pixels = probe->window().take_snapshot();
|
||||
if (pixels && pixels->width() > 0 && pixels->height() > 0) {
|
||||
result = 0;
|
||||
} else {
|
||||
std::cerr << "Slint probe could not read rendered pixels"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
slint::quit_event_loop();
|
||||
});
|
||||
slint::run_event_loop();
|
||||
probe->hide();
|
||||
}
|
||||
SDL_Quit();
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -60,8 +60,6 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
nid_.uCallbackMessage = WM_TRAY_CALLBACK;
|
||||
nid_.hIcon = icon_;
|
||||
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
|
||||
|
||||
EnsureTrayIcon();
|
||||
}
|
||||
|
||||
WinTray::WinTray(std::function<void()> show_window,
|
||||
@@ -82,7 +80,7 @@ WinTray::~WinTray() {
|
||||
}
|
||||
|
||||
void WinTray::MinimizeToTray() {
|
||||
EnsureTrayIcon();
|
||||
Shell_NotifyIcon(NIM_ADD, &nid_);
|
||||
if (hide_window_) {
|
||||
hide_window_();
|
||||
} else if (app_hwnd_) {
|
||||
@@ -90,25 +88,7 @@ void WinTray::MinimizeToTray() {
|
||||
}
|
||||
}
|
||||
|
||||
bool WinTray::EnsureTrayIcon() {
|
||||
if (tray_icon_added_) {
|
||||
// NIM_MODIFY also detects an icon lost when Explorer was restarted.
|
||||
if (Shell_NotifyIcon(NIM_MODIFY, &nid_)) {
|
||||
return true;
|
||||
}
|
||||
tray_icon_added_ = false;
|
||||
}
|
||||
|
||||
tray_icon_added_ = Shell_NotifyIcon(NIM_ADD, &nid_) != FALSE;
|
||||
return tray_icon_added_;
|
||||
}
|
||||
|
||||
void WinTray::RemoveTrayIcon() {
|
||||
if (tray_icon_added_) {
|
||||
Shell_NotifyIcon(NIM_DELETE, &nid_);
|
||||
tray_icon_added_ = false;
|
||||
}
|
||||
}
|
||||
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
|
||||
|
||||
void WinTray::ShowApplicationWindow() {
|
||||
if (show_window_) {
|
||||
|
||||
@@ -33,7 +33,6 @@ class WinTray {
|
||||
bool HandleTrayMessage(MSG* msg);
|
||||
|
||||
private:
|
||||
bool EnsureTrayIcon();
|
||||
void ShowApplicationWindow();
|
||||
void OpenSettings();
|
||||
|
||||
@@ -43,7 +42,6 @@ class WinTray {
|
||||
std::wstring tip_;
|
||||
int language_index_;
|
||||
NOTIFYICONDATA nid_;
|
||||
bool tray_icon_added_ = false;
|
||||
std::function<void()> show_window_;
|
||||
std::function<void()> hide_window_;
|
||||
std::function<void()> open_settings_;
|
||||
|
||||
@@ -384,7 +384,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
stream_id.c_str(), nullptr);
|
||||
stream_id.c_str());
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
|
||||
@@ -212,8 +212,7 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
|
||||
if (callback_) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str(),
|
||||
nullptr);
|
||||
callback_(nv12_frame_, nv12_size, width, height, stream_id.c_str());
|
||||
}
|
||||
|
||||
SelectObject(mem_dc, old);
|
||||
|
||||
@@ -406,7 +406,7 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
|
||||
const std::string stream_id = MakeDisplayStreamId(id);
|
||||
on_data_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
stream_id.c_str(), nullptr);
|
||||
stream_id.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "captured_nv12_frame.h"
|
||||
#include "interactive_state.h"
|
||||
#include "named_pipe_deadline.h"
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_dxgi.h"
|
||||
#include "screen_capturer_gdi.h"
|
||||
#include "secure_desktop_frame_schedule.h"
|
||||
#include "secure_desktop_status_poller.h"
|
||||
#include "service_host.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
@@ -44,6 +40,17 @@ constexpr int kSecureDesktopCaptureMinFps = 30;
|
||||
constexpr int kSecureDesktopCaptureMaxIntervalMs =
|
||||
1000 / kSecureDesktopCaptureMinFps;
|
||||
|
||||
struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
bool capture_active = false;
|
||||
bool helper_running = false;
|
||||
DWORD active_session_id = 0xFFFFFFFF;
|
||||
DWORD error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class WgcPluginCapturer final : public ScreenCapturer {
|
||||
public:
|
||||
using CreateFn = ScreenCapturer* (*)();
|
||||
@@ -178,6 +185,41 @@ bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||
error == "pipe_read_failed";
|
||||
}
|
||||
|
||||
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
|
||||
DWORD* error_code_out = nullptr) {
|
||||
if (response_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
response_out->clear();
|
||||
if (error_code_out != nullptr) {
|
||||
*error_code_out = 0;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> chunk(64 * 1024);
|
||||
while (true) {
|
||||
DWORD bytes_read = 0;
|
||||
if (ReadFile(pipe, chunk.data(), static_cast<DWORD>(chunk.size()),
|
||||
&bytes_read, nullptr)) {
|
||||
response_out->insert(response_out->end(), chunk.begin(),
|
||||
chunk.begin() + bytes_read);
|
||||
return true;
|
||||
}
|
||||
|
||||
const DWORD error = GetLastError();
|
||||
response_out->insert(response_out->end(), chunk.begin(),
|
||||
chunk.begin() + bytes_read);
|
||||
if (error == ERROR_MORE_DATA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error_code_out != nullptr) {
|
||||
*error_code_out = error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* width_out, int* height_out,
|
||||
@@ -218,27 +260,25 @@ bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
|
||||
return true;
|
||||
}
|
||||
|
||||
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
||||
SecureDesktopServiceStatus status;
|
||||
std::vector<uint8_t> response;
|
||||
DWORD error_code = 0;
|
||||
if (!QueryNamedPipeWithDeadline(kCrossDeskServicePipeName, "status",
|
||||
kSecureDesktopStatusPipeTimeoutMs, &response,
|
||||
&status.error, &error_code)) {
|
||||
status.error_code = error_code;
|
||||
return status;
|
||||
}
|
||||
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) {
|
||||
status.error = "invalid_service_status_json";
|
||||
return status;
|
||||
bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
if (status == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
status.service_available = json.value("ok", false);
|
||||
if (!status.service_available) {
|
||||
status.error = json.value("error", std::string("service_unavailable"));
|
||||
status.error_code = json.value("code", 0u);
|
||||
return status;
|
||||
*status = {};
|
||||
const std::string response =
|
||||
QueryCrossDeskService("status", kSecureDesktopStatusPipeTimeoutMs);
|
||||
Json json = Json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.is_object()) {
|
||||
status->error = "invalid_service_status_json";
|
||||
return false;
|
||||
}
|
||||
|
||||
status->service_available = json.value("ok", false);
|
||||
if (!status->service_available) {
|
||||
status->error = json.value("error", std::string("service_unavailable"));
|
||||
status->error_code = json.value("code", 0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ShouldNormalizeUnlockToUserDesktop(
|
||||
@@ -252,27 +292,26 @@ SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
||||
json.value("password_box_visible", false),
|
||||
json.value("unlock_ui_visible", false),
|
||||
json.value("last_session_event", std::string()))) {
|
||||
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status.interactive_stage = "user-desktop";
|
||||
status.interactive_desktop.clear();
|
||||
status.capture_active = false;
|
||||
return status;
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->interactive_stage = "user-desktop";
|
||||
status->interactive_desktop.clear();
|
||||
status->capture_active = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status.helper_running = json.value("secure_input_helper_running", false);
|
||||
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
|
||||
status.interactive_stage = json.value("interactive_stage", std::string());
|
||||
status.interactive_desktop =
|
||||
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
|
||||
status->helper_running = json.value("secure_input_helper_running", false);
|
||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||
status->interactive_desktop =
|
||||
json.value("interactive_input_desktop", std::string());
|
||||
const bool secure_desktop_active =
|
||||
json.value("interactive_secure_desktop_active",
|
||||
json.value("secure_desktop_active", false));
|
||||
status.capture_active =
|
||||
status.active_session_id != 0xFFFFFFFF &&
|
||||
status->capture_active =
|
||||
status->active_session_id != 0xFFFFFFFF &&
|
||||
(secure_desktop_active ||
|
||||
IsSecureDesktopInteractionRequired(status.interactive_stage));
|
||||
return status;
|
||||
IsSecureDesktopInteractionRequired(status->interactive_stage));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
@@ -283,29 +322,60 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
return false;
|
||||
}
|
||||
|
||||
response_out->clear();
|
||||
const std::wstring pipe_name =
|
||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||
std::string error;
|
||||
DWORD code = 0;
|
||||
// The legacy single-frame path includes a full GDI capture and a large
|
||||
// response. Give it a separate deadline from the small control messages.
|
||||
const DWORD timeout_ms =
|
||||
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
|
||||
? 1000
|
||||
: kSecureDesktopHelperPipeTimeoutMs;
|
||||
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
|
||||
response_out, &error, &code);
|
||||
if (!ok && error_out != nullptr) {
|
||||
*error_out = error + ":" + std::to_string(code);
|
||||
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_unavailable:" + std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return ok;
|
||||
|
||||
HANDLE pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
|
||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_connect_failed:" + std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||
|
||||
DWORD bytes_written = 0;
|
||||
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
|
||||
&bytes_written, nullptr)) {
|
||||
const DWORD error = GetLastError();
|
||||
CloseHandle(pipe);
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_write_failed:" + std::to_string(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD read_error = 0;
|
||||
const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error);
|
||||
CloseHandle(pipe);
|
||||
if (!read_ok) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "pipe_read_failed:" + std::to_string(read_error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperFrame(
|
||||
DWORD session_id, int left, int top, int width, int height,
|
||||
bool show_cursor, const std::string& stage, const std::string& desktop,
|
||||
std::vector<uint8_t>* nv12_frame_out, int* captured_width_out,
|
||||
int* captured_height_out, std::string* error_out) {
|
||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
int width, int height, bool show_cursor,
|
||||
const std::string& stage,
|
||||
const std::string& desktop,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* captured_width_out,
|
||||
int* captured_height_out,
|
||||
std::string* error_out) {
|
||||
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
||||
captured_height_out == nullptr) {
|
||||
return false;
|
||||
@@ -333,17 +403,8 @@ ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
native_output_logged_.store(false, std::memory_order_relaxed);
|
||||
native_output_error_logged_.store(false, std::memory_order_relaxed);
|
||||
try {
|
||||
native_frame_pool_ = CapturedNv12FramePool::Create();
|
||||
} catch (...) {
|
||||
LOG_ERROR("Windows capturer: failed to create native NV12 frame pool");
|
||||
return -1;
|
||||
}
|
||||
cb_ = [this](unsigned char* data, int size, int w, int h,
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
const char* reported_stream_id) {
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
@@ -389,8 +450,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
"elapsed_ms={}",
|
||||
raw_stream_id, mapped_stream_id, w, h, size, elapsed_ms);
|
||||
}
|
||||
EmitCapturedFrame(data, size, w, h, mapped_stream_id.c_str(),
|
||||
native_frame);
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_stream_id.c_str());
|
||||
};
|
||||
|
||||
int ret = -1;
|
||||
@@ -453,52 +513,9 @@ int ScreenCapturerWin::Destroy() {
|
||||
stream_id_alias_.clear();
|
||||
handle_to_canonical_index_.clear();
|
||||
}
|
||||
native_frame_pool_.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::EmitCapturedFrame(
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (!cb_orig_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (native_frame) {
|
||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||
LOG_INFO("Windows capturer native frame output enabled (type={})",
|
||||
static_cast<uint32_t>(native_frame->type));
|
||||
}
|
||||
cb_orig_(data, size, width, height, stream_id, native_frame);
|
||||
return;
|
||||
}
|
||||
|
||||
CapturedNv12Frame* owned_frame = nullptr;
|
||||
if (native_frame_pool_ && data && size > 0 && width > 0 && height > 0) {
|
||||
owned_frame = native_frame_pool_->CopyFrom(
|
||||
data, static_cast<size_t>(size), static_cast<uint32_t>(width),
|
||||
static_cast<uint32_t>(height));
|
||||
}
|
||||
if (!owned_frame) {
|
||||
if (!native_output_error_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
LOG_WARN(
|
||||
"Windows capturer could not retain a native NV12 frame; falling "
|
||||
"back to copied CPU input (size={}x{}, bytes={})",
|
||||
width, height, size);
|
||||
}
|
||||
cb_orig_(data, size, width, height, stream_id, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||
LOG_INFO("Windows capturer native NV12 output enabled");
|
||||
}
|
||||
cb_orig_(nullptr, static_cast<int>(owned_frame->Size()), width, height,
|
||||
stream_id, owned_frame->Descriptor());
|
||||
owned_frame->Release();
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
@@ -1091,35 +1108,17 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
const int frame_interval_ms =
|
||||
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
|
||||
: kSecureDesktopCaptureMaxIntervalMs;
|
||||
ULONGLONG last_status_tick = 0;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
bool last_capture_active = false;
|
||||
bool last_service_available = true;
|
||||
std::string last_stage;
|
||||
std::string last_service_error;
|
||||
ULONGLONG capture_stage_started_tick = 0;
|
||||
bool post_secure_restart_pending = false;
|
||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||
ULONGLONG last_post_secure_restart_tick = 0;
|
||||
SecureDesktopServiceStatus status;
|
||||
SecureDesktopStatusPoller status_poller(
|
||||
QuerySecureDesktopServiceStatus,
|
||||
std::chrono::milliseconds(kSecureDesktopStatusIntervalMs));
|
||||
SecureDesktopFrameSchedule frame_schedule;
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
ULONGLONG stats_started = GetTickCount64();
|
||||
unsigned shared_frames = 0, fallback_frames = 0, shared_waits = 0;
|
||||
unsigned shared_restarts = 0;
|
||||
int64_t max_status_ms = 0;
|
||||
auto report_stats = [&](bool force) {
|
||||
const ULONGLONG elapsed = GetTickCount64() - stats_started;
|
||||
if (elapsed == 0 || (!force && elapsed < 5000)) return;
|
||||
if (shared_frames || fallback_frames || shared_waits || shared_restarts) {
|
||||
LOG_INFO(
|
||||
"Secure capture delivery: shared_fps={:.1f}, fallback_fps={:.1f}, "
|
||||
"pending_waits={}, shared_restarts={}, status_max_ms={}",
|
||||
shared_frames * 1000.0 / elapsed, fallback_frames * 1000.0 / elapsed,
|
||||
shared_waits, shared_restarts, max_status_ms);
|
||||
}
|
||||
stats_started = GetTickCount64();
|
||||
shared_frames = fallback_frames = shared_waits = shared_restarts = 0;
|
||||
max_status_ms = 0;
|
||||
};
|
||||
std::vector<uint8_t> secure_frame;
|
||||
|
||||
while (running_.load(std::memory_order_relaxed)) {
|
||||
@@ -1129,60 +1128,67 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
report_stats(false);
|
||||
if (auto sample = status_poller.Take()) {
|
||||
const auto previous = std::exchange(status, std::move(sample->status));
|
||||
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
|
||||
if (status.service_available != previous.service_available ||
|
||||
status.error != previous.error) {
|
||||
if (status.service_available) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service available, "
|
||||
"polling session_id={}",
|
||||
status.active_session_id);
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service temporarily "
|
||||
"unavailable; "
|
||||
"keeping last capture state: error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop service unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
if (last_status_tick == 0 ||
|
||||
now - last_status_tick >= kSecureDesktopStatusIntervalMs) {
|
||||
SecureDesktopServiceStatus latest_status;
|
||||
const bool status_ok = QuerySecureDesktopServiceStatus(&latest_status);
|
||||
status = latest_status;
|
||||
if (status_ok) {
|
||||
const bool service_changed =
|
||||
status.service_available != last_service_available;
|
||||
const bool service_error_changed =
|
||||
!status.service_available && status.error != last_service_error;
|
||||
if (service_changed || service_error_changed) {
|
||||
if (status.service_available) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service available, polling "
|
||||
"session_id={}",
|
||||
status.active_session_id);
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service temporarily unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop service unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
}
|
||||
last_service_available = status.service_available;
|
||||
last_service_error = status.error;
|
||||
}
|
||||
} else if (last_service_available ||
|
||||
last_service_error != "invalid_service_status_json") {
|
||||
LOG_WARN("Windows capturer secure desktop service status query failed");
|
||||
last_service_available = false;
|
||||
last_service_error = "invalid_service_status_json";
|
||||
}
|
||||
|
||||
secure_desktop_capture_active_.store(status.capture_active,
|
||||
std::memory_order_relaxed);
|
||||
if (status.capture_active != previous.capture_active ||
|
||||
status.interactive_stage != previous.interactive_stage ||
|
||||
status.active_session_id != previous.active_session_id ||
|
||||
status.helper_process_id != previous.helper_process_id) {
|
||||
report_stats(true);
|
||||
// A restarted helper can leave our mapping/event handles alive, but
|
||||
// their old producer is gone. Explicitly establish a fresh stream.
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(now);
|
||||
if (status.capture_active != last_capture_active ||
|
||||
status.interactive_stage != last_stage) {
|
||||
const bool secure_capture_started =
|
||||
!previous.capture_active && status.capture_active;
|
||||
!last_capture_active && status.capture_active;
|
||||
const bool secure_capture_ended =
|
||||
previous.capture_active && !status.capture_active;
|
||||
last_capture_active && !status.capture_active;
|
||||
capture_stage_started_tick = now;
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop state: active={}, stage='{}', "
|
||||
"session_id={}",
|
||||
status.capture_active, status.interactive_stage,
|
||||
status.active_session_id);
|
||||
last_capture_active = status.capture_active;
|
||||
last_stage = status.interactive_stage;
|
||||
if (secure_capture_started) {
|
||||
post_secure_restart_pending = false;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
0, std::memory_order_relaxed);
|
||||
} else if (secure_capture_ended) {
|
||||
post_secure_restart_pending = true;
|
||||
post_secure_restart_deadline_tick =
|
||||
@@ -1190,12 +1196,13 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
last_post_secure_restart_tick = 0;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
true, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(now,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
now, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
last_status_tick = now;
|
||||
}
|
||||
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
@@ -1241,64 +1248,41 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::string error_message;
|
||||
bool frame_delivered = false;
|
||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||
const int shared_fps = fps_ > 0
|
||||
? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||
: kSecureDesktopCaptureMinFps;
|
||||
const int shared_fps =
|
||||
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||
: kSecureDesktopCaptureMinFps;
|
||||
|
||||
if (secure_shared_capture_started_ &&
|
||||
frame_schedule.SharedCaptureStalled(now)) {
|
||||
LOG_WARN("Secure shared capture stalled; restarting producer");
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
++shared_restarts;
|
||||
}
|
||||
|
||||
bool shared_ready = false;
|
||||
const bool was_started = secure_shared_capture_started_;
|
||||
if (was_started || frame_schedule.CanStart(now)) {
|
||||
shared_ready = StartSecureDesktopSharedCapture(
|
||||
status.active_session_id, left, top, width, height,
|
||||
status.interactive_stage, status.interactive_desktop, show_cursor,
|
||||
shared_fps, &error_message);
|
||||
if (!shared_ready) {
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
} else if (!was_started) {
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
}
|
||||
}
|
||||
|
||||
if (shared_ready &&
|
||||
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
|
||||
width, height,
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
show_cursor, shared_fps,
|
||||
&error_message) &&
|
||||
ReadSecureDesktopSharedFrame(
|
||||
static_cast<DWORD>((std::max)(50, frame_interval_ms + 20)),
|
||||
&secure_frame, &captured_width, &captured_height, &error_message)) {
|
||||
frame_delivered = true;
|
||||
++shared_frames;
|
||||
frame_schedule.OnSharedFrame(GetTickCount64());
|
||||
}
|
||||
|
||||
const bool frame_pending = shared_ready && !frame_delivered &&
|
||||
IsPendingSecureDesktopFrame(error_message);
|
||||
if (frame_pending) ++shared_waits;
|
||||
if (shared_ready && !frame_delivered && !frame_pending) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
}
|
||||
if (!frame_delivered && !frame_pending &&
|
||||
QuerySecureDesktopHelperFrame(
|
||||
status.active_session_id, left, top, width, height, show_cursor,
|
||||
status.interactive_stage, status.interactive_desktop, &secure_frame,
|
||||
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
|
||||
&captured_width, &captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
frame_delivered = true;
|
||||
++fallback_frames;
|
||||
}
|
||||
|
||||
if (frame_delivered && !secure_frame.empty()) {
|
||||
EmitCapturedFrame(secure_frame.data(),
|
||||
static_cast<int>(secure_frame.size()), captured_width,
|
||||
captured_height, display_name.c_str());
|
||||
if (!frame_delivered &&
|
||||
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
|
||||
width, height, show_cursor,
|
||||
status.interactive_stage,
|
||||
status.interactive_desktop,
|
||||
&secure_frame, &captured_width,
|
||||
&captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
frame_delivered = true;
|
||||
}
|
||||
|
||||
if (!frame_delivered && !frame_pending) {
|
||||
if (!frame_delivered) {
|
||||
const bool transient_error =
|
||||
IsTransientSecureDesktopFrameError(error_message);
|
||||
const bool in_grace_period = capture_stage_started_tick != 0 &&
|
||||
@@ -1329,18 +1313,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
const auto elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - frame_started)
|
||||
.count();
|
||||
const int sleep_ms = SecureDesktopFrameSchedule::RemainingFrameDelay(
|
||||
frame_interval_ms, static_cast<uint64_t>(elapsed_ms));
|
||||
if (sleep_ms > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
|
||||
}
|
||||
|
||||
report_stats(true);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -23,8 +22,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class CapturedNv12FramePool;
|
||||
|
||||
class ScreenCapturerWin : public ScreenCapturer {
|
||||
public:
|
||||
ScreenCapturerWin();
|
||||
@@ -56,8 +53,6 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::mutex alias_mutex_;
|
||||
std::vector<DisplayInfo> canonical_displays_;
|
||||
std::atomic<bool> invalid_stream_id_logged_{false};
|
||||
std::atomic<bool> native_output_logged_{false};
|
||||
std::atomic<bool> native_output_error_logged_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
@@ -82,13 +77,9 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::string secure_shared_stage_;
|
||||
std::string secure_shared_desktop_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr);
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void SecureDesktopCaptureLoop();
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_FRAME_SCHEDULE_H_
|
||||
#define _SECURE_DESKTOP_FRAME_SCHEDULE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
|
||||
return error == "frame_wait_timeout" ||
|
||||
error == "shared_frame_write_in_progress" ||
|
||||
error == "shared_frame_changed_during_read";
|
||||
}
|
||||
|
||||
// A 4K GDI frame can take longer than a single consumer wait. Do not issue
|
||||
// another expensive screenshot while the shared producer is still healthy.
|
||||
class SecureDesktopFrameSchedule {
|
||||
public:
|
||||
void Reset(uint64_t now) {
|
||||
last_shared_frame_ = now;
|
||||
next_start_attempt_ = 0;
|
||||
}
|
||||
|
||||
void OnSharedFrame(uint64_t now) { last_shared_frame_ = now; }
|
||||
bool SharedCaptureStalled(uint64_t now) const {
|
||||
return now - last_shared_frame_ >= 1000;
|
||||
}
|
||||
bool CanStart(uint64_t now) const { return now >= next_start_attempt_; }
|
||||
void OnStartFailure(uint64_t now) { next_start_attempt_ = now + 1000; }
|
||||
|
||||
static int RemainingFrameDelay(int interval_ms, uint64_t elapsed_ms) {
|
||||
return elapsed_ms >= static_cast<uint64_t>(interval_ms)
|
||||
? 0
|
||||
: interval_ms - static_cast<int>(elapsed_ms);
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t last_shared_frame_ = 0;
|
||||
uint64_t next_start_attempt_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_STATUS_POLLER_H_
|
||||
#define _SECURE_DESKTOP_STATUS_POLLER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
bool capture_active = false;
|
||||
bool helper_running = false;
|
||||
uint32_t helper_process_id = 0;
|
||||
uint32_t active_session_id = 0xFFFFFFFF;
|
||||
uint32_t error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::string error;
|
||||
|
||||
void UpdateFrom(SecureDesktopServiceStatus latest) {
|
||||
if (latest.service_available) {
|
||||
*this = std::move(latest);
|
||||
} else {
|
||||
// Failed IPC says nothing about the desktop. Retain the last confirmed
|
||||
// capture state until a valid response reports a change or an unlock.
|
||||
service_available = false;
|
||||
error = std::move(latest.error);
|
||||
error_code = latest.error_code;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The query runs without holding the snapshot mutex. Slow service/helper IPC
|
||||
// must not stop delivery of frames that are already available in shared memory.
|
||||
class SecureDesktopStatusPoller {
|
||||
public:
|
||||
using Query = std::function<SecureDesktopServiceStatus()>;
|
||||
|
||||
struct Sample {
|
||||
SecureDesktopServiceStatus status;
|
||||
int64_t query_ms = 0;
|
||||
};
|
||||
|
||||
explicit SecureDesktopStatusPoller(Query query,
|
||||
std::chrono::milliseconds interval)
|
||||
: thread_([this, query = std::move(query), interval]() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
while (!stopping_) {
|
||||
lock.unlock();
|
||||
const auto started = std::chrono::steady_clock::now();
|
||||
auto latest = query();
|
||||
const auto elapsed =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - started);
|
||||
lock.lock();
|
||||
sample_.status.UpdateFrom(std::move(latest));
|
||||
sample_.query_ms = elapsed.count();
|
||||
pending_ = true;
|
||||
// Even a failed/slow query gets a pause, avoiding a retry storm.
|
||||
wake_.wait_for(lock, interval, [this] { return stopping_; });
|
||||
}
|
||||
}) {}
|
||||
|
||||
~SecureDesktopStatusPoller() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stopping_ = true;
|
||||
}
|
||||
wake_.notify_one();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
SecureDesktopStatusPoller(const SecureDesktopStatusPoller&) = delete;
|
||||
SecureDesktopStatusPoller& operator=(const SecureDesktopStatusPoller&) =
|
||||
delete;
|
||||
|
||||
std::optional<Sample> Take() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!pending_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
pending_ = false;
|
||||
return sample_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
bool stopping_ = false;
|
||||
bool pending_ = false;
|
||||
Sample sample_;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _INTERACTIVE_DESKTOP_H_
|
||||
#define _INTERACTIVE_DESKTOP_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
||||
DWORD bytes_needed = 0;
|
||||
if (desktop == nullptr) {
|
||||
return {};
|
||||
}
|
||||
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
|
||||
if (bytes_needed == 0) {
|
||||
return {};
|
||||
}
|
||||
std::wstring name(bytes_needed / sizeof(wchar_t), L'\0');
|
||||
if (!GetUserObjectInformationW(desktop, UOI_NAME, name.data(), bytes_needed,
|
||||
&bytes_needed)) {
|
||||
return {};
|
||||
}
|
||||
while (!name.empty() && name.back() == L'\0') {
|
||||
name.pop_back();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Use only on worker threads without windows or hooks. Restore the original
|
||||
// desktop before closing the handle: Windows cannot close a desktop in use.
|
||||
class ScopedInteractiveDesktop {
|
||||
public:
|
||||
ScopedInteractiveDesktop()
|
||||
: original_(GetThreadDesktop(GetCurrentThreadId())) {}
|
||||
ScopedInteractiveDesktop(const ScopedInteractiveDesktop&) = delete;
|
||||
ScopedInteractiveDesktop& operator=(const ScopedInteractiveDesktop&) = delete;
|
||||
|
||||
~ScopedInteractiveDesktop() {
|
||||
if (desktop_ != nullptr) {
|
||||
SetThreadDesktop(original_);
|
||||
CloseDesktop(desktop_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Bind(const std::wstring& fallback_name) {
|
||||
constexpr ACCESS_MASK access =
|
||||
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP | DESKTOP_JOURNALPLAYBACK;
|
||||
// SendInput requires playback access on the attached desktop; otherwise
|
||||
// it returns ERROR_ACCESS_DENIED even for a correctly attached SYSTEM helper.
|
||||
// The service's stage/name can lag a lock, unlock, or UAC transition.
|
||||
// Opening a cached desktop can succeed even when it is no longer visible.
|
||||
HDESK desktop = OpenInputDesktop(0, FALSE, access);
|
||||
if (desktop == nullptr && !fallback_name.empty()) {
|
||||
desktop = OpenDesktopW(fallback_name.c_str(), 0, FALSE, access);
|
||||
}
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
const DWORD error = GetLastError();
|
||||
CloseDesktop(desktop);
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
if (desktop_ != nullptr) {
|
||||
CloseDesktop(desktop_);
|
||||
}
|
||||
desktop_ = desktop;
|
||||
name_ = GetDesktopNameW(desktop_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsInputDesktop() const {
|
||||
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
|
||||
if (input == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::wstring input_name = GetDesktopNameW(input);
|
||||
CloseDesktop(input);
|
||||
return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0;
|
||||
}
|
||||
|
||||
const std::wstring& name() const { return name_; }
|
||||
|
||||
private:
|
||||
HDESK original_ = nullptr;
|
||||
HDESK desktop_ = nullptr;
|
||||
std::wstring name_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _NAMED_PIPE_DEADLINE_H_
|
||||
#define _NAMED_PIPE_DEADLINE_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace pipe_deadline_detail {
|
||||
struct Handle {
|
||||
explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : value(handle) {}
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
HANDLE value = INVALID_HANDLE_VALUE;
|
||||
~Handle() {
|
||||
if (value != INVALID_HANDLE_VALUE && value != nullptr) CloseHandle(value);
|
||||
}
|
||||
};
|
||||
|
||||
inline DWORD Remaining(ULONGLONG deadline) {
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
return now >= deadline ? 0 : static_cast<DWORD>(deadline - now);
|
||||
}
|
||||
|
||||
inline bool Transfer(HANDLE pipe, HANDLE event, bool write, void* data,
|
||||
DWORD size, ULONGLONG deadline, DWORD* transferred,
|
||||
DWORD* error) {
|
||||
*transferred = 0;
|
||||
if (Remaining(deadline) == 0) {
|
||||
*error = ERROR_SEM_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
ResetEvent(event);
|
||||
OVERLAPPED operation{};
|
||||
operation.hEvent = event;
|
||||
const BOOL complete =
|
||||
write ? WriteFile(pipe, data, size, transferred, &operation)
|
||||
: ReadFile(pipe, data, size, transferred, &operation);
|
||||
*error = complete ? ERROR_SUCCESS : GetLastError();
|
||||
if (*error == ERROR_IO_PENDING) {
|
||||
const DWORD wait = WaitForSingleObject(event, Remaining(deadline));
|
||||
if (wait != WAIT_OBJECT_0) {
|
||||
*error = wait == WAIT_TIMEOUT ? ERROR_SEM_TIMEOUT : GetLastError();
|
||||
// Keep the OVERLAPPED and buffer alive until cancellation completes.
|
||||
CancelIoEx(pipe, &operation);
|
||||
DWORD ignored = 0;
|
||||
GetOverlappedResult(pipe, &operation, &ignored, TRUE);
|
||||
return false;
|
||||
}
|
||||
} else if (*error != ERROR_SUCCESS && *error != ERROR_MORE_DATA) {
|
||||
return false;
|
||||
}
|
||||
// Immediate MORE_DATA also needs GetOverlappedResult: ReadFile may leave
|
||||
// its byte-count output at zero even though part of the message was read.
|
||||
const BOOL result = GetOverlappedResult(pipe, &operation, transferred, FALSE);
|
||||
*error = result ? ERROR_SUCCESS : GetLastError();
|
||||
return result != FALSE;
|
||||
}
|
||||
} // namespace pipe_deadline_detail
|
||||
|
||||
// One deadline covers connection, write, and every response chunk. A connect
|
||||
// timeout alone does not bound a synchronous ReadFile on an unresponsive peer.
|
||||
inline bool QueryNamedPipeWithDeadline(const std::wstring& name,
|
||||
const std::string& command,
|
||||
DWORD timeout_ms,
|
||||
std::vector<uint8_t>* response,
|
||||
std::string* error, DWORD* error_code) {
|
||||
using namespace pipe_deadline_detail;
|
||||
response->clear();
|
||||
error->clear();
|
||||
*error_code = ERROR_SUCCESS;
|
||||
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
|
||||
Handle pipe;
|
||||
while (true) {
|
||||
const DWORD remaining = Remaining(deadline);
|
||||
if (remaining == 0) break;
|
||||
if (WaitNamedPipeW(name.c_str(), remaining)) {
|
||||
pipe.value =
|
||||
CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr,
|
||||
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
|
||||
if (pipe.value != INVALID_HANDLE_VALUE) break;
|
||||
}
|
||||
*error_code = GetLastError();
|
||||
if (*error_code != ERROR_FILE_NOT_FOUND && *error_code != ERROR_PIPE_BUSY &&
|
||||
*error_code != ERROR_SEM_TIMEOUT)
|
||||
break;
|
||||
Sleep((std::min)(DWORD{10}, Remaining(deadline)));
|
||||
}
|
||||
if (pipe.value == INVALID_HANDLE_VALUE) {
|
||||
*error = "pipe_unavailable";
|
||||
if (*error_code == ERROR_SUCCESS) *error_code = ERROR_SEM_TIMEOUT;
|
||||
return false;
|
||||
}
|
||||
DWORD mode = PIPE_READMODE_MESSAGE;
|
||||
if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) {
|
||||
*error = "pipe_connect_failed";
|
||||
*error_code = GetLastError();
|
||||
return false;
|
||||
}
|
||||
Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
||||
if (event.value == nullptr) {
|
||||
*error = "pipe_connect_failed";
|
||||
*error_code = GetLastError();
|
||||
return false;
|
||||
}
|
||||
DWORD transferred = 0;
|
||||
if (!Transfer(pipe.value, event.value, true,
|
||||
const_cast<char*>(command.data()),
|
||||
static_cast<DWORD>(command.size()), deadline, &transferred,
|
||||
error_code) ||
|
||||
transferred != command.size()) {
|
||||
*error = "pipe_write_failed";
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> chunk(64 * 1024);
|
||||
while (true) {
|
||||
const bool complete = Transfer(pipe.value, event.value, false, chunk.data(),
|
||||
static_cast<DWORD>(chunk.size()), deadline,
|
||||
&transferred, error_code);
|
||||
if (!complete && *error_code != ERROR_MORE_DATA) {
|
||||
response->clear();
|
||||
*error = "pipe_read_failed";
|
||||
return false;
|
||||
}
|
||||
response->insert(response->end(), chunk.begin(),
|
||||
chunk.begin() + transferred);
|
||||
if (complete) return true;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -16,7 +16,6 @@
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "interactive_state.h"
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
@@ -1460,18 +1459,18 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
|
||||
const std::string& interactive_stage) const {
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
if (session_helper_status_ok_ &&
|
||||
session_helper_report_input_desktop_available_ &&
|
||||
!session_helper_report_input_desktop_.empty() &&
|
||||
(IsSecureDesktopInteractionRequired(interactive_stage) ||
|
||||
(interactive_stage == "credential-ui" ||
|
||||
session_helper_report_consent_ui_visible_)) {
|
||||
return session_helper_report_input_desktop_;
|
||||
}
|
||||
|
||||
if (interactive_stage == "lock-screen") {
|
||||
return "Default";
|
||||
}
|
||||
|
||||
if (input_desktop_available_ && !input_desktop_name_.empty() &&
|
||||
(interactive_stage == "credential-ui" || consent_ui_visible_)) {
|
||||
return input_desktop_name_;
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "interactive_desktop.h"
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
@@ -31,7 +30,6 @@ namespace {
|
||||
|
||||
using crossdesk::get_logger;
|
||||
using crossdesk::InitLogger;
|
||||
using crossdesk::ScopedInteractiveDesktop;
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr DWORD kSessionHelperStatePollMs = 1000;
|
||||
@@ -540,8 +538,123 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring GetDesktopNameW(HDESK desktop) {
|
||||
if (desktop == nullptr) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
DWORD bytes_needed = 0;
|
||||
GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed);
|
||||
if (bytes_needed == 0) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0');
|
||||
if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(),
|
||||
bytes_needed, &bytes_needed)) {
|
||||
return L"";
|
||||
}
|
||||
|
||||
while (!desktop_name.empty() && desktop_name.back() == L'\0') {
|
||||
desktop_name.pop_back();
|
||||
}
|
||||
return desktop_name;
|
||||
}
|
||||
|
||||
std::wstring GetCurrentThreadDesktopNameW() {
|
||||
return crossdesk::GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
|
||||
return GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId()));
|
||||
}
|
||||
|
||||
constexpr ACCESS_MASK kCrossDeskInteractiveDesktopAccess =
|
||||
DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS |
|
||||
DESKTOP_SWITCHDESKTOP;
|
||||
|
||||
bool EnsureThreadDesktop(const wchar_t* desktop_name,
|
||||
HDESK* opened_desktop_out = nullptr) {
|
||||
if (desktop_name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring current_desktop = GetCurrentThreadDesktopNameW();
|
||||
if (!current_desktop.empty() &&
|
||||
_wcsicmp(current_desktop.c_str(), desktop_name) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
HDESK desktop =
|
||||
OpenDesktopW(desktop_name, 0, FALSE, kCrossDeskInteractiveDesktopAccess);
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
CloseDesktop(desktop);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnsureThreadInputDesktop(HDESK* opened_desktop_out = nullptr) {
|
||||
HDESK desktop =
|
||||
OpenInputDesktop(0, FALSE, kCrossDeskInteractiveDesktopAccess);
|
||||
if (desktop == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring input_desktop = GetDesktopNameW(desktop);
|
||||
const std::wstring current_desktop = GetCurrentThreadDesktopNameW();
|
||||
if (!input_desktop.empty() && !current_desktop.empty() &&
|
||||
_wcsicmp(input_desktop.c_str(), current_desktop.c_str()) == 0) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SetThreadDesktop(desktop)) {
|
||||
CloseDesktop(desktop);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = desktop;
|
||||
} else {
|
||||
CloseDesktop(desktop);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = nullptr;
|
||||
}
|
||||
|
||||
if (EnsureThreadInputDesktop(opened_desktop_out)) {
|
||||
return true;
|
||||
}
|
||||
const DWORD input_desktop_error = GetLastError();
|
||||
|
||||
if (EnsureThreadDesktop(L"Winlogon", opened_desktop_out)) {
|
||||
return true;
|
||||
}
|
||||
const DWORD winlogon_error = GetLastError();
|
||||
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper desktop, input_error={}, "
|
||||
"winlogon_error={}, current='{}'",
|
||||
input_desktop_error, winlogon_error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(winlogon_error != ERROR_SUCCESS ? winlogon_error
|
||||
: input_desktop_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring DesktopNameForInteractiveStage(
|
||||
@@ -587,10 +700,8 @@ DesktopSwitchDetails BuildDesktopSwitchDetails(
|
||||
return details;
|
||||
}
|
||||
|
||||
InputInjectionResult BuildInputSuccess(DesktopSwitchDetails desktop) {
|
||||
InputInjectionResult result;
|
||||
result.desktop = std::move(desktop);
|
||||
return result;
|
||||
InputInjectionResult BuildInputSuccess() {
|
||||
return {};
|
||||
}
|
||||
|
||||
InputInjectionResult BuildInputFailure(const char* error, DWORD error_code,
|
||||
@@ -621,23 +732,48 @@ Json BuildInputFailureJson(const InputInjectionResult& result) {
|
||||
bool EnsureThreadInteractiveDesktopForStage(
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop,
|
||||
ScopedInteractiveDesktop* desktop,
|
||||
HDESK* opened_desktop_out = nullptr,
|
||||
DesktopSwitchDetails* switch_details = nullptr) {
|
||||
if (opened_desktop_out != nullptr) {
|
||||
*opened_desktop_out = nullptr;
|
||||
}
|
||||
|
||||
DesktopSwitchDetails local_details =
|
||||
BuildDesktopSwitchDetails(interactive_stage, interactive_desktop);
|
||||
if (switch_details != nullptr) {
|
||||
*switch_details = local_details;
|
||||
}
|
||||
|
||||
std::wstring desktop_name =
|
||||
const std::wstring desktop_name =
|
||||
DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
|
||||
if (desktop_name.empty()) {
|
||||
desktop_name = L"Winlogon";
|
||||
}
|
||||
if (desktop->Bind(desktop_name)) {
|
||||
if (!desktop_name.empty()) {
|
||||
if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) {
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->current_desktop =
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DWORD error = GetLastError();
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->target_desktop = WideToUtf8(desktop->name());
|
||||
switch_details->current_desktop = switch_details->target_desktop;
|
||||
switch_details->error_code = error;
|
||||
switch_details->current_desktop =
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper to stage desktop, stage='{}', "
|
||||
"desktop='{}', error={}, current='{}'",
|
||||
interactive_stage, WideToUtf8(desktop_name),
|
||||
error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (EnsureThreadInteractiveDesktop(opened_desktop_out)) {
|
||||
if (switch_details != nullptr) {
|
||||
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -647,15 +783,18 @@ bool EnsureThreadInteractiveDesktopForStage(
|
||||
switch_details->error_code = error;
|
||||
switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
}
|
||||
LOG_WARN(
|
||||
"Failed to switch secure input helper desktop, stage='{}', "
|
||||
"fallback='{}', error={}, current='{}'",
|
||||
interactive_stage, WideToUtf8(desktop_name), error,
|
||||
WideToUtf8(GetCurrentThreadDesktopNameW()));
|
||||
SetLastError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct ScopedDesktopHandle {
|
||||
HDESK handle = nullptr;
|
||||
~ScopedDesktopHandle() {
|
||||
if (handle != nullptr) {
|
||||
CloseDesktop(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
@@ -676,11 +815,11 @@ InputInjectionResult InjectKeyboardInput(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
const std::string& interactive_stage,
|
||||
const std::string& interactive_desktop) {
|
||||
ScopedInteractiveDesktop desktop;
|
||||
ScopedDesktopHandle desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(interactive_stage,
|
||||
interactive_desktop,
|
||||
&desktop,
|
||||
&desktop.handle,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildInputFailure("switch_interactive_desktop_failed",
|
||||
@@ -736,7 +875,7 @@ InputInjectionResult InjectKeyboardInput(
|
||||
desktop_switch);
|
||||
}
|
||||
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
return BuildInputSuccess();
|
||||
}
|
||||
|
||||
void ParseInteractionTail(const std::string& tail,
|
||||
@@ -1053,11 +1192,11 @@ INPUT BuildAbsoluteMouseMoveInput(int x, int y) {
|
||||
}
|
||||
|
||||
InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
|
||||
ScopedInteractiveDesktop desktop;
|
||||
ScopedDesktopHandle desktop;
|
||||
DesktopSwitchDetails desktop_switch;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop,
|
||||
&desktop.handle,
|
||||
&desktop_switch)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildInputFailure("switch_interactive_desktop_failed",
|
||||
@@ -1117,91 +1256,13 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
|
||||
desktop_switch);
|
||||
}
|
||||
|
||||
return BuildInputSuccess(std::move(desktop_switch));
|
||||
return BuildInputSuccess();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> BuildTextResponseBytes(const std::string& response) {
|
||||
return std::vector<uint8_t>(response.begin(), response.end());
|
||||
}
|
||||
|
||||
// Own the desktop before its GDI objects, and release those objects before
|
||||
// restoring the original desktop. Both capture paths use the same lifecycle.
|
||||
struct SecureDesktopGdiResources {
|
||||
ScopedInteractiveDesktop desktop;
|
||||
HDC screen_dc = nullptr;
|
||||
HDC mem_dc = nullptr;
|
||||
HBITMAP dib = nullptr;
|
||||
HGDIOBJ old_bitmap = nullptr;
|
||||
void* bits = nullptr;
|
||||
const char* error = nullptr;
|
||||
DWORD error_code = ERROR_SUCCESS;
|
||||
|
||||
~SecureDesktopGdiResources() {
|
||||
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
}
|
||||
if (dib != nullptr) DeleteObject(dib);
|
||||
if (mem_dc != nullptr) DeleteDC(mem_dc);
|
||||
if (screen_dc != nullptr) ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
bool Fail(const char* reason) {
|
||||
error = reason;
|
||||
error_code = GetLastError();
|
||||
if (error_code == ERROR_SUCCESS) error_code = ERROR_GEN_FAILURE;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Initialize(const SecureCaptureRequest& request) {
|
||||
const auto fallback = DesktopNameForInteractiveStage(
|
||||
request.interactive_stage, request.interactive_desktop);
|
||||
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
|
||||
return Fail("switch_interactive_desktop_failed");
|
||||
}
|
||||
screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) return Fail("get_dc_failed");
|
||||
mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) return Fail("create_dc_failed");
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
dib = CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) return Fail("create_dib_failed");
|
||||
old_bitmap = SelectObject(mem_dc, dib);
|
||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||
return Fail("select_bitmap_failed");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Capture(const SecureCaptureRequest& request) {
|
||||
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
return Fail("bitblt_failed");
|
||||
}
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING &&
|
||||
cursor.hCursor != nullptr) {
|
||||
const int x = cursor.ptScreenPos.x - request.left;
|
||||
const int y = cursor.ptScreenPos.y - request.top;
|
||||
if (x >= -64 && y >= -64 && x < request.width + 64 &&
|
||||
y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Complete GDI writes before libyuv reads the DIB's pixel memory.
|
||||
GdiFlush();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
const SecureCaptureRequest& request,
|
||||
SecureCaptureBuffers* capture_buffers) {
|
||||
@@ -1209,21 +1270,87 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers"));
|
||||
}
|
||||
|
||||
SecureDesktopGdiResources capture;
|
||||
if (!capture.Initialize(request) || !capture.Capture(request)) {
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
const DWORD error = GetLastError();
|
||||
return BuildTextResponseBytes(BuildErrorJson(
|
||||
"switch_interactive_desktop_failed",
|
||||
error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE));
|
||||
}
|
||||
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) {
|
||||
return BuildTextResponseBytes(
|
||||
BuildErrorJson(capture.error, capture.error_code));
|
||||
BuildErrorJson("get_dc_failed", GetLastError()));
|
||||
}
|
||||
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("create_dc_failed", error));
|
||||
}
|
||||
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("create_dib_failed", error));
|
||||
}
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
|
||||
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
const DWORD error = GetLastError();
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return BuildTextResponseBytes(BuildErrorJson("bitblt_failed", error));
|
||||
}
|
||||
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor_info{};
|
||||
cursor_info.cbSize = sizeof(CURSORINFO);
|
||||
if (GetCursorInfo(&cursor_info) && cursor_info.flags == CURSOR_SHOWING &&
|
||||
cursor_info.hCursor != nullptr) {
|
||||
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
|
||||
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
|
||||
if (cursor_x >= -64 && cursor_y >= -64 && cursor_x < request.width + 64 &&
|
||||
cursor_y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, 0,
|
||||
nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const size_t nv12_size =
|
||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
||||
capture_buffers->nv12_frame.resize(nv12_size);
|
||||
const int convert_result = libyuv::ARGBToNV12(
|
||||
static_cast<const uint8_t*>(capture.bits), request.width * 4,
|
||||
static_cast<const uint8_t*>(bits), request.width * 4,
|
||||
capture_buffers->nv12_frame.data(), request.width,
|
||||
capture_buffers->nv12_frame.data() + request.width * request.height,
|
||||
request.width, request.width, request.height);
|
||||
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
|
||||
if (convert_result != 0) {
|
||||
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
|
||||
}
|
||||
@@ -1270,7 +1397,8 @@ void CloseSecureDesktopSharedCaptureResourcesLocked(
|
||||
capture_state->frame_view_size = 0;
|
||||
}
|
||||
|
||||
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
void SecureDesktopSharedCaptureThread(
|
||||
SecureSharedCaptureState* capture_state) {
|
||||
if (capture_state == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -1293,65 +1421,79 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
||||
std::vector<uint8_t> nv12_frame(nv12_size);
|
||||
|
||||
std::unique_ptr<SecureDesktopGdiResources> capture;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
auto stats_started = std::chrono::steady_clock::now();
|
||||
unsigned frame_count = 0;
|
||||
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
|
||||
auto report_stats = [&](bool force) {
|
||||
const auto elapsed = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - stats_started)
|
||||
.count();
|
||||
if (elapsed <= 0 || (!force && elapsed < 5.0)) return;
|
||||
if (frame_count > 0) {
|
||||
LOG_INFO(
|
||||
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
|
||||
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}",
|
||||
frame_count / elapsed, capture_ms / frame_count,
|
||||
convert_ms / frame_count, publish_ms / frame_count, request.width,
|
||||
request.height);
|
||||
}
|
||||
stats_started = std::chrono::steady_clock::now();
|
||||
frame_count = 0;
|
||||
capture_ms = convert_ms = publish_ms = 0;
|
||||
};
|
||||
ScopedDesktopHandle desktop;
|
||||
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
|
||||
request.interactive_desktop,
|
||||
&desktop.handle)) {
|
||||
LOG_ERROR("Secure shared capture desktop switch failed, error={}",
|
||||
GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
if (screen_dc == nullptr) {
|
||||
LOG_ERROR("Secure shared capture GetDC failed, error={}", GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
HDC mem_dc = CreateCompatibleDC(screen_dc);
|
||||
if (mem_dc == nullptr) {
|
||||
LOG_ERROR("Secure shared capture CreateCompatibleDC failed, error={}",
|
||||
GetLastError());
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return;
|
||||
}
|
||||
|
||||
BITMAPINFO bmi{};
|
||||
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
bmi.bmiHeader.biWidth = request.width;
|
||||
bmi.bmiHeader.biHeight = -request.height;
|
||||
bmi.bmiHeader.biPlanes = 1;
|
||||
bmi.bmiHeader.biBitCount = 32;
|
||||
bmi.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* bits = nullptr;
|
||||
HBITMAP dib =
|
||||
CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||
if (dib == nullptr || bits == nullptr) {
|
||||
LOG_ERROR("Secure shared capture CreateDIBSection failed, error={}",
|
||||
GetLastError());
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
return;
|
||||
}
|
||||
|
||||
HGDIOBJ old_bitmap = SelectObject(mem_dc, dib);
|
||||
while (!capture_state->stop_requested.load(std::memory_order_relaxed)) {
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
report_stats(false);
|
||||
// A lock/credential/UAC transition can change the desktop before the
|
||||
// service's next status poll. Release its DCs before binding the new one.
|
||||
if (capture && !capture->desktop.IsInputDesktop()) {
|
||||
capture.reset();
|
||||
}
|
||||
if (!capture) {
|
||||
capture = std::make_unique<SecureDesktopGdiResources>();
|
||||
if (!capture->Initialize(request)) {
|
||||
const DWORD error = capture->error_code;
|
||||
capture.reset();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
|
||||
LOG_WARN(
|
||||
"Secure shared capture initialization failed; retrying, "
|
||||
"error={}",
|
||||
error);
|
||||
last_error_tick = now;
|
||||
if (BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor_info{};
|
||||
cursor_info.cbSize = sizeof(CURSORINFO);
|
||||
if (GetCursorInfo(&cursor_info) &&
|
||||
cursor_info.flags == CURSOR_SHOWING &&
|
||||
cursor_info.hCursor != nullptr) {
|
||||
const int cursor_x = cursor_info.ptScreenPos.x - request.left;
|
||||
const int cursor_y = cursor_info.ptScreenPos.y - request.top;
|
||||
if (cursor_x >= -64 && cursor_y >= -64 &&
|
||||
cursor_x < request.width + 64 &&
|
||||
cursor_y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0,
|
||||
0, nullptr, DI_NORMAL);
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (capture->Capture(request)) {
|
||||
const auto captured_at = std::chrono::steady_clock::now();
|
||||
const int convert_result = libyuv::ARGBToNV12(
|
||||
static_cast<const uint8_t*>(capture->bits), request.width * 4,
|
||||
static_cast<const uint8_t*>(bits), request.width * 4,
|
||||
nv12_frame.data(), request.width,
|
||||
nv12_frame.data() + request.width * request.height, request.width,
|
||||
request.width, request.height);
|
||||
if (convert_result == 0) {
|
||||
const auto converted_at = std::chrono::steady_clock::now();
|
||||
auto* header = reinterpret_cast<
|
||||
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
|
||||
auto* header =
|
||||
reinterpret_cast<crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(
|
||||
frame_view);
|
||||
uint8_t* payload = frame_view + sizeof(*header);
|
||||
header->writing = 1;
|
||||
MemoryBarrier();
|
||||
@@ -1367,41 +1509,25 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
MemoryBarrier();
|
||||
header->writing = 0;
|
||||
SetEvent(capture_state->frame_ready_event);
|
||||
++frame_count;
|
||||
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
|
||||
frame_started)
|
||||
.count();
|
||||
convert_ms += std::chrono::duration<double, std::milli>(converted_at -
|
||||
captured_at)
|
||||
.count();
|
||||
publish_ms += std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - converted_at)
|
||||
.count();
|
||||
}
|
||||
} else {
|
||||
const DWORD error = capture->error_code;
|
||||
capture.reset();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_error_tick == 0 || now - last_error_tick >= 5000) {
|
||||
LOG_WARN(
|
||||
"Secure shared capture BitBlt failed; recreating capture, "
|
||||
"error={}",
|
||||
error);
|
||||
last_error_tick = now;
|
||||
}
|
||||
LOG_WARN("Secure shared capture BitBlt failed, error={}", GetLastError());
|
||||
}
|
||||
|
||||
const auto elapsed_ms =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - frame_started)
|
||||
.count();
|
||||
const int retry_interval_ms = capture ? interval_ms : 100;
|
||||
if (elapsed_ms < retry_interval_ms) {
|
||||
if (elapsed_ms < interval_ms) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(retry_interval_ms - elapsed_ms));
|
||||
std::chrono::milliseconds(interval_ms - elapsed_ms));
|
||||
}
|
||||
}
|
||||
report_stats(true);
|
||||
|
||||
SelectObject(mem_dc, old_bitmap);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> StopSecureDesktopSharedCapture(
|
||||
@@ -1577,7 +1703,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["extended"] = extended;
|
||||
json["stage"] = interactive_stage;
|
||||
json["interactive_desktop"] = interactive_desktop;
|
||||
json["desktop"] = inject_result.desktop.current_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
|
||||
@@ -1605,7 +1731,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
json["flag"] = mouse_request.flag;
|
||||
json["stage"] = mouse_request.interactive_stage;
|
||||
json["interactive_desktop"] = mouse_request.interactive_desktop;
|
||||
json["desktop"] = inject_result.desktop.current_desktop;
|
||||
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
|
||||
return BuildTextResponseBytes(json.dump());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,95 +1,104 @@
|
||||
#include "speaker_capturer_wasapi.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "miniaudio.h"
|
||||
|
||||
#define SAVE_AUDIO_FILE 0
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Each attempt owns its device. A failed/old session must never uninitialize
|
||||
// a device belonging to another capturer or reuse already released storage.
|
||||
class SpeakerCapturerWasapi::Impl {
|
||||
public:
|
||||
ma_device device{};
|
||||
bool initialized = false;
|
||||
speaker_data_cb callback;
|
||||
};
|
||||
static ma_device_config device_config_;
|
||||
static ma_device device_;
|
||||
static ma_format format_ = ma_format_s16;
|
||||
static ma_uint32 sample_rate_ = ma_standard_sample_rate_48000;
|
||||
static ma_uint32 channels_ = 1;
|
||||
static FILE* fp_ = nullptr;
|
||||
|
||||
void data_callback(ma_device* pDevice, void* pOutput, const void* pInput,
|
||||
ma_uint32 frameCount) {
|
||||
SpeakerCapturerWasapi* ptr = (SpeakerCapturerWasapi*)pDevice->pUserData;
|
||||
if (ptr) {
|
||||
if (SAVE_AUDIO_FILE) {
|
||||
fwrite(pInput, frameCount * ma_get_bytes_per_frame(format_, channels_), 1,
|
||||
fp_);
|
||||
}
|
||||
|
||||
ptr->GetCallback()((unsigned char*)pInput,
|
||||
frameCount * ma_get_bytes_per_frame(format_, channels_),
|
||||
"audio");
|
||||
}
|
||||
|
||||
(void)pOutput;
|
||||
}
|
||||
|
||||
SpeakerCapturerWasapi::speaker_data_cb SpeakerCapturerWasapi::GetCallback() {
|
||||
return cb_;
|
||||
}
|
||||
|
||||
SpeakerCapturerWasapi::SpeakerCapturerWasapi() {}
|
||||
|
||||
SpeakerCapturerWasapi::SpeakerCapturerWasapi() : impl_(new Impl) {}
|
||||
SpeakerCapturerWasapi::~SpeakerCapturerWasapi() {
|
||||
Destroy();
|
||||
delete impl_;
|
||||
if (SAVE_AUDIO_FILE) {
|
||||
fclose(fp_);
|
||||
}
|
||||
}
|
||||
|
||||
int SpeakerCapturerWasapi::Init(speaker_data_cb cb) {
|
||||
if (impl_->initialized) return 0;
|
||||
impl_->callback = std::move(cb);
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_loopback);
|
||||
config.capture.pDeviceID =
|
||||
nullptr; // Resolve the current default each attempt.
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = 1;
|
||||
config.sampleRate = 48000;
|
||||
config.pUserData = impl_;
|
||||
config.dataCallback = [](ma_device* device, void*, const void* input,
|
||||
ma_uint32 frames) {
|
||||
auto* state = static_cast<Impl*>(device->pUserData);
|
||||
if (state && state->callback && input && frames > 0)
|
||||
state->callback((unsigned char*)input, frames * sizeof(int16_t), "audio");
|
||||
};
|
||||
if (inited_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
cb_ = cb;
|
||||
|
||||
if (SAVE_AUDIO_FILE) {
|
||||
fopen_s(&fp_, "system_audio.pcm", "wb");
|
||||
}
|
||||
|
||||
ma_result result;
|
||||
ma_backend backends[] = {ma_backend_wasapi};
|
||||
const ma_result result =
|
||||
ma_device_init_ex(backends, 1, nullptr, &config, &impl_->device);
|
||||
|
||||
device_config_ = ma_device_config_init(ma_device_type_loopback);
|
||||
device_config_.capture.pDeviceID = NULL;
|
||||
device_config_.capture.format = format_;
|
||||
device_config_.capture.channels = channels_;
|
||||
device_config_.sampleRate = sample_rate_;
|
||||
device_config_.dataCallback = data_callback;
|
||||
device_config_.pUserData = this;
|
||||
|
||||
result = ma_device_init_ex(backends, sizeof(backends) / sizeof(backends[0]),
|
||||
NULL, &device_config_, &device_);
|
||||
if (result != MA_SUCCESS) {
|
||||
impl_->callback = nullptr;
|
||||
LOG_ERROR("Failed to initialize WASAPI loopback device: {}",
|
||||
static_cast<int>(result));
|
||||
LOG_ERROR("Failed to initialize loopback device");
|
||||
return -1;
|
||||
}
|
||||
impl_->initialized = true;
|
||||
|
||||
inited_ = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerWasapi::Start() {
|
||||
if (!impl_->initialized) return -1;
|
||||
if (IsRunning()) return 0;
|
||||
const ma_result result = ma_device_start(&impl_->device);
|
||||
ma_result result = ma_device_start(&device_);
|
||||
if (result != MA_SUCCESS) {
|
||||
LOG_ERROR("Failed to start WASAPI loopback device: {}",
|
||||
static_cast<int>(result));
|
||||
Destroy();
|
||||
ma_device_uninit(&device_);
|
||||
LOG_ERROR("Failed to start device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerWasapi::Stop() {
|
||||
if (!impl_->initialized || !IsRunning()) return 0;
|
||||
const ma_result result = ma_device_stop(&impl_->device);
|
||||
if (result != MA_SUCCESS) {
|
||||
LOG_WARN("Failed to stop WASAPI loopback device: {}",
|
||||
static_cast<int>(result));
|
||||
return -1;
|
||||
}
|
||||
ma_device_stop(&device_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpeakerCapturerWasapi::Destroy() {
|
||||
if (impl_->initialized) {
|
||||
// ma_device_uninit also stops and drains the device callback thread.
|
||||
ma_device_uninit(&impl_->device);
|
||||
impl_->initialized = false;
|
||||
}
|
||||
impl_->callback = nullptr;
|
||||
ma_device_uninit(&device_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SpeakerCapturerWasapi::IsRunning() const {
|
||||
return impl_->initialized && ma_device_is_started(&impl_->device);
|
||||
}
|
||||
int SpeakerCapturerWasapi::Pause() { return Stop(); }
|
||||
int SpeakerCapturerWasapi::Resume() { return Start(); }
|
||||
} // namespace crossdesk
|
||||
int SpeakerCapturerWasapi::Pause() { return 0; }
|
||||
} // namespace crossdesk
|
||||
@@ -14,21 +14,24 @@ namespace crossdesk {
|
||||
class SpeakerCapturerWasapi : public SpeakerCapturer {
|
||||
public:
|
||||
SpeakerCapturerWasapi();
|
||||
~SpeakerCapturerWasapi() override;
|
||||
~SpeakerCapturerWasapi();
|
||||
|
||||
public:
|
||||
int Init(speaker_data_cb cb) override;
|
||||
int Destroy() override;
|
||||
int Start() override;
|
||||
int Stop() override;
|
||||
bool IsRunning() const override;
|
||||
virtual int Init(speaker_data_cb cb);
|
||||
virtual int Destroy();
|
||||
virtual int Start();
|
||||
virtual int Stop();
|
||||
|
||||
int Pause();
|
||||
int Resume();
|
||||
|
||||
speaker_data_cb GetCallback();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
Impl* impl_ = nullptr;
|
||||
speaker_data_cb cb_ = nullptr;
|
||||
|
||||
private:
|
||||
bool inited_ = false;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -1,224 +0,0 @@
|
||||
#include "windows_installer.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <shobjidl.h>
|
||||
#include <winhttp.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kMaxInstallerSize = 1024ull * 1024 * 1024;
|
||||
// Stable identity used only for Windows Attachment Services policy/UI.
|
||||
constexpr GUID kAttachmentClient = {
|
||||
0x72d17a6b,
|
||||
0xeac1,
|
||||
0x4816,
|
||||
{0x91, 0x40, 0x33, 0x1b, 0xa7, 0x76, 0x22, 0xcd}};
|
||||
|
||||
void CheckWin32(BOOL success, const char* operation) {
|
||||
if (!success) {
|
||||
throw std::system_error(static_cast<int>(GetLastError()),
|
||||
std::system_category(), operation);
|
||||
}
|
||||
}
|
||||
|
||||
struct InternetCloser {
|
||||
void operator()(HINTERNET handle) const { WinHttpCloseHandle(handle); }
|
||||
};
|
||||
using InternetHandle = std::unique_ptr<void, InternetCloser>;
|
||||
|
||||
struct PartialFile {
|
||||
std::filesystem::path path;
|
||||
~PartialFile() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path, error);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsWindowsExecutable(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
std::array<unsigned char, 64> header{};
|
||||
input.read(reinterpret_cast<char*>(header.data()), header.size());
|
||||
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
|
||||
const uint32_t pe_offset =
|
||||
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
|
||||
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
|
||||
if (pe_offset < header.size()) return false;
|
||||
input.seekg(pe_offset);
|
||||
std::array<char, 4> signature{};
|
||||
input.read(signature.data(), signature.size());
|
||||
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
|
||||
}
|
||||
|
||||
uint64_t ContentLength(HINTERNET request) {
|
||||
std::array<wchar_t, 32> value{};
|
||||
DWORD bytes = static_cast<DWORD>(sizeof(value));
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, value.data(), &bytes,
|
||||
WINHTTP_NO_HEADER_INDEX)) {
|
||||
if (GetLastError() == ERROR_WINHTTP_HEADER_NOT_FOUND) return 0;
|
||||
CheckWin32(FALSE, "WinHttpQueryHeaders(Content-Length)");
|
||||
}
|
||||
const std::wstring length(value.data());
|
||||
if (length.empty() ||
|
||||
length.find_first_not_of(L"0123456789") != std::wstring::npos) {
|
||||
throw std::runtime_error("Invalid installer content length");
|
||||
}
|
||||
return std::stoull(length);
|
||||
}
|
||||
|
||||
class ComApartment {
|
||||
public:
|
||||
ComApartment() : status_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
|
||||
~ComApartment() {
|
||||
if (SUCCEEDED(status_)) CoUninitialize();
|
||||
}
|
||||
bool Ready() const {
|
||||
return SUCCEEDED(status_) || status_ == RPC_E_CHANGED_MODE;
|
||||
}
|
||||
|
||||
private:
|
||||
HRESULT status_;
|
||||
};
|
||||
|
||||
bool HandleAttachment(const std::filesystem::path& installer,
|
||||
const std::string& source, bool execute) {
|
||||
ComApartment apartment;
|
||||
if (!apartment.Ready()) {
|
||||
LOG_WARN("Cannot initialize Windows Attachment Services");
|
||||
return false;
|
||||
}
|
||||
Microsoft::WRL::ComPtr<IAttachmentExecute> attachment;
|
||||
HRESULT result =
|
||||
CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&attachment));
|
||||
const std::wstring source_url(source.begin(), source.end());
|
||||
if (SUCCEEDED(result)) result = attachment->SetClientGuid(kAttachmentClient);
|
||||
if (SUCCEEDED(result))
|
||||
result = attachment->SetClientTitle(L"CrossDesk Update");
|
||||
if (SUCCEEDED(result)) result = attachment->SetLocalPath(installer.c_str());
|
||||
if (SUCCEEDED(result)) result = attachment->SetSource(source_url.c_str());
|
||||
if (SUCCEEDED(result)) {
|
||||
if (execute) {
|
||||
HANDLE process = nullptr;
|
||||
// Execute applies attachment policy, scanning, and any required prompts.
|
||||
// The installer's manifest, rather than this process, requests elevation.
|
||||
result = attachment->Execute(GetActiveWindow(), L"open", &process);
|
||||
if (process) CloseHandle(process);
|
||||
} else {
|
||||
// Save records the source zone and invokes installed trust/AV services.
|
||||
result = attachment->Save();
|
||||
}
|
||||
}
|
||||
if (FAILED(result)) {
|
||||
LOG_WARN("Windows Attachment Services {} failed/blocked, HRESULT=0x{:08X}",
|
||||
execute ? "execute" : "save", static_cast<uint32_t>(result));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DownloadWindowsInstaller(
|
||||
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||
const std::function<bool(uint64_t, uint64_t)>& progress) {
|
||||
auto partial_path = destination;
|
||||
partial_path += ".part";
|
||||
PartialFile partial{partial_path};
|
||||
try {
|
||||
if (!progress(0, 0)) return false;
|
||||
const std::string agent = "CrossDesk/" CROSSDESK_VERSION " WindowsUpdater";
|
||||
const std::wstring user_agent(agent.begin(), agent.end());
|
||||
InternetHandle session(
|
||||
WinHttpOpen(user_agent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
CheckWin32(session != nullptr, "WinHttpOpen");
|
||||
CheckWin32(WinHttpSetTimeouts(session.get(), 5000, 5000, 5000, 5000),
|
||||
"WinHttpSetTimeouts");
|
||||
InternetHandle connection(WinHttpConnect(session.get(),
|
||||
L"downloads.crossdesk.cn",
|
||||
INTERNET_DEFAULT_HTTPS_PORT, 0));
|
||||
CheckWin32(connection != nullptr, "WinHttpConnect");
|
||||
const std::wstring path(download.path.begin(), download.path.end());
|
||||
InternetHandle request(WinHttpOpenRequest(
|
||||
connection.get(), L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
|
||||
CheckWin32(request != nullptr, "WinHttpOpenRequest");
|
||||
DWORD disabled = WINHTTP_DISABLE_REDIRECTS;
|
||||
CheckWin32(WinHttpSetOption(request.get(), WINHTTP_OPTION_DISABLE_FEATURE,
|
||||
&disabled, sizeof(disabled)),
|
||||
"Disable redirects");
|
||||
// Use Windows certificate validation and configured proxies as-is. Do not
|
||||
// ignore TLS errors, spoof a browser, or retry through alternative hosts.
|
||||
CheckWin32(WinHttpSendRequest(request.get(), WINHTTP_NO_ADDITIONAL_HEADERS,
|
||||
0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0),
|
||||
"WinHttpSendRequest");
|
||||
CheckWin32(WinHttpReceiveResponse(request.get(), nullptr),
|
||||
"WinHttpReceiveResponse");
|
||||
DWORD status = 0;
|
||||
DWORD status_size = sizeof(status);
|
||||
CheckWin32(WinHttpQueryHeaders(
|
||||
request.get(),
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size,
|
||||
WINHTTP_NO_HEADER_INDEX),
|
||||
"WinHttpQueryHeaders(status)");
|
||||
if (status != 200) {
|
||||
LOG_WARN("Windows installer download returned HTTP {}", status);
|
||||
return false;
|
||||
}
|
||||
const uint64_t total = ContentLength(request.get());
|
||||
if (total > kMaxInstallerSize || !progress(0, total)) return false;
|
||||
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
|
||||
if (!output)
|
||||
throw std::runtime_error("Cannot create installer download file");
|
||||
uint64_t received = 0;
|
||||
std::array<unsigned char, 64 * 1024> buffer{};
|
||||
while (progress(received, total)) {
|
||||
DWORD count = 0;
|
||||
CheckWin32(WinHttpReadData(request.get(), buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()), &count),
|
||||
"WinHttpReadData");
|
||||
if (count == 0) break;
|
||||
received += count;
|
||||
if (received > kMaxInstallerSize || (total > 0 && received > total))
|
||||
return false;
|
||||
output.write(reinterpret_cast<const char*>(buffer.data()), count);
|
||||
if (!output)
|
||||
throw std::runtime_error("Cannot write installer download file");
|
||||
}
|
||||
output.close();
|
||||
if (output.fail() || received == 0 || (total > 0 && received != total) ||
|
||||
!progress(received, total) || !IsWindowsExecutable(partial.path))
|
||||
return false;
|
||||
LOG_INFO("Windows installer downloaded, bytes={}", received);
|
||||
std::filesystem::rename(partial.path, destination);
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Windows installer download failed: {}", error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source) {
|
||||
return HandleAttachment(installer, source, false);
|
||||
}
|
||||
|
||||
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source) {
|
||||
return HandleAttachment(installer, source, true);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_INSTALLER_H_
|
||||
#define _WINDOWS_INSTALLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "installer_download.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool DownloadWindowsInstaller(
|
||||
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||
const std::function<bool(uint64_t, uint64_t)>& progress);
|
||||
|
||||
// Attachment Services records the Internet source and applies Windows policy
|
||||
// and installed antivirus checks. Failure must never fall back to direct
|
||||
// launch.
|
||||
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source);
|
||||
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,123 +0,0 @@
|
||||
#include "windows_updater.h"
|
||||
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "installer_download.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_installer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
WindowsUpdater::~WindowsUpdater() {
|
||||
Cancel();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
RemoveDownload();
|
||||
}
|
||||
|
||||
void WindowsUpdater::RemoveDownload() {
|
||||
// A running NSIS installer still needs its source file. Leave that one in
|
||||
// the user's update cache; clean up cancelled/failed attempts.
|
||||
if (installer_.empty() || installer_launched_) return;
|
||||
std::error_code error;
|
||||
std::filesystem::remove(installer_, error);
|
||||
std::filesystem::remove(installer_.parent_path(), error);
|
||||
}
|
||||
|
||||
void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
const State state = GetState();
|
||||
if (state == State::Downloading || state == State::Checking ||
|
||||
state == State::Launching || state == State::SecurityBlocked)
|
||||
return;
|
||||
if (state == State::Ready || state == State::LaunchFailed ||
|
||||
state == State::Launched) {
|
||||
cancelled_ = false;
|
||||
Launch();
|
||||
return;
|
||||
}
|
||||
if (worker_.joinable()) worker_.join();
|
||||
RemoveDownload();
|
||||
installer_.clear();
|
||||
installer_launched_ = false;
|
||||
cancelled_ = false;
|
||||
downloaded_ = 0;
|
||||
total_ = 0;
|
||||
const auto download = GetWindowsInstallerDownload(metadata);
|
||||
if (!download) {
|
||||
LOG_WARN("No valid Windows installer for the advertised version");
|
||||
state_ = State::Failed;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::random_device random;
|
||||
PWSTR local_app_data = nullptr;
|
||||
const HRESULT path_result = SHGetKnownFolderPath(
|
||||
FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &local_app_data);
|
||||
if (FAILED(path_result))
|
||||
throw std::runtime_error("Cannot locate LocalAppData");
|
||||
const std::filesystem::path data_path(local_app_data);
|
||||
CoTaskMemFree(local_app_data);
|
||||
const auto updates = data_path / L"CrossDesk" / L"Updates";
|
||||
std::filesystem::create_directories(updates);
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
const auto directory = updates / ("download-" + std::to_string(random()) +
|
||||
"-" + std::to_string(random()));
|
||||
if (std::filesystem::create_directory(directory)) {
|
||||
installer_ = directory / download->filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (installer_.empty())
|
||||
throw std::runtime_error("Cannot create update directory");
|
||||
source_url_ = download->origin + download->path;
|
||||
state_ = State::Downloading;
|
||||
worker_ = std::thread([this, download = *download] {
|
||||
const bool success = DownloadWindowsInstaller(
|
||||
download, installer_, [this](uint64_t received, uint64_t total) {
|
||||
downloaded_ = received;
|
||||
total_ = total;
|
||||
return !cancelled_.load();
|
||||
});
|
||||
if (success && !cancelled_) {
|
||||
state_ = State::Checking;
|
||||
state_ = CheckWindowsInstaller(installer_, source_url_)
|
||||
? State::Ready
|
||||
: State::SecurityBlocked;
|
||||
} else {
|
||||
state_ = State::Failed;
|
||||
}
|
||||
if (cancelled_) state_ = State::Idle;
|
||||
});
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Cannot start Windows update: {}", error.what());
|
||||
state_ = State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsUpdater::Cancel() {
|
||||
cancelled_ = true;
|
||||
if (GetState() == State::Ready) state_ = State::Idle;
|
||||
}
|
||||
|
||||
void WindowsUpdater::Launch() {
|
||||
if (cancelled_) return;
|
||||
const State state = GetState();
|
||||
if (state != State::Ready && state != State::LaunchFailed &&
|
||||
state != State::Launched)
|
||||
return;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
state_ = State::Launching;
|
||||
if (!LaunchWindowsInstaller(installer_, source_url_)) {
|
||||
state_ = State::LaunchFailed;
|
||||
return;
|
||||
}
|
||||
installer_launched_ = true;
|
||||
state_ = State::Launched;
|
||||
// NSIS already prompts to close CrossDesk and stops the installed service.
|
||||
// Keep this process alive if the user subsequently cancels installation.
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_UPDATER_H_
|
||||
#define _WINDOWS_UPDATER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class WindowsUpdater {
|
||||
public:
|
||||
enum class State {
|
||||
Idle,
|
||||
Downloading,
|
||||
Checking,
|
||||
Ready,
|
||||
Launching,
|
||||
Failed,
|
||||
SecurityBlocked,
|
||||
LaunchFailed,
|
||||
Launched
|
||||
};
|
||||
|
||||
~WindowsUpdater();
|
||||
void Start(const nlohmann::json& metadata);
|
||||
void Cancel();
|
||||
void Launch();
|
||||
State GetState() const { return state_.load(); }
|
||||
uint64_t Downloaded() const { return downloaded_.load(); }
|
||||
uint64_t Total() const { return total_.load(); }
|
||||
|
||||
private:
|
||||
void RemoveDownload();
|
||||
|
||||
std::atomic<State> state_{State::Idle};
|
||||
std::atomic<bool> cancelled_{false};
|
||||
std::atomic<uint64_t> downloaded_{0};
|
||||
std::atomic<uint64_t> total_{0};
|
||||
std::thread worker_;
|
||||
std::filesystem::path installer_;
|
||||
std::string source_url_;
|
||||
bool installer_launched_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "captured_nv12_frame.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
bool GetNv12Size(uint32_t width, uint32_t height, size_t* size) {
|
||||
if (!size || width == 0 || height == 0 || (width & 1U) != 0 ||
|
||||
(height & 1U) != 0 ||
|
||||
static_cast<size_t>(width) >
|
||||
std::numeric_limits<size_t>::max() / height) {
|
||||
return false;
|
||||
}
|
||||
const size_t pixels = static_cast<size_t>(width) * height;
|
||||
if (pixels > std::numeric_limits<size_t>::max() / 3U * 2U) {
|
||||
return false;
|
||||
}
|
||||
*size = pixels * 3U / 2U;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::shared_ptr<CapturedNv12FramePool> CapturedNv12FramePool::Create(
|
||||
size_t max_cached_buffers) {
|
||||
return std::shared_ptr<CapturedNv12FramePool>(
|
||||
new CapturedNv12FramePool(max_cached_buffers));
|
||||
}
|
||||
|
||||
CapturedNv12FramePool::CapturedNv12FramePool(size_t max_cached_buffers)
|
||||
: max_cached_buffers_(max_cached_buffers) {}
|
||||
|
||||
CapturedNv12Frame* CapturedNv12FramePool::CopyFrom(const uint8_t* data,
|
||||
size_t size,
|
||||
uint32_t width,
|
||||
uint32_t height) {
|
||||
size_t required_size = 0;
|
||||
if (!data || !GetNv12Size(width, height, &required_size) ||
|
||||
size < required_size) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> storage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto best = free_buffers_.end();
|
||||
for (auto it = free_buffers_.begin(); it != free_buffers_.end(); ++it) {
|
||||
if (it->capacity() < required_size) {
|
||||
continue;
|
||||
}
|
||||
if (best == free_buffers_.end() || it->capacity() < best->capacity()) {
|
||||
best = it;
|
||||
}
|
||||
}
|
||||
if (best == free_buffers_.end() && !free_buffers_.empty()) {
|
||||
best = std::max_element(
|
||||
free_buffers_.begin(), free_buffers_.end(),
|
||||
[](const auto& lhs, const auto& rhs) {
|
||||
return lhs.capacity() < rhs.capacity();
|
||||
});
|
||||
}
|
||||
if (best != free_buffers_.end()) {
|
||||
storage = std::move(*best);
|
||||
free_buffers_.erase(best);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
storage.resize(required_size);
|
||||
std::memcpy(storage.data(), data, required_size);
|
||||
return new CapturedNv12Frame(shared_from_this(), std::move(storage), width,
|
||||
height);
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12FramePool::Recycle(std::vector<uint8_t> storage) {
|
||||
if (max_cached_buffers_ == 0 || storage.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (free_buffers_.size() < max_cached_buffers_) {
|
||||
free_buffers_.push_back(std::move(storage));
|
||||
return;
|
||||
}
|
||||
|
||||
auto smallest = std::min_element(
|
||||
free_buffers_.begin(), free_buffers_.end(),
|
||||
[](const auto& lhs, const auto& rhs) {
|
||||
return lhs.capacity() < rhs.capacity();
|
||||
});
|
||||
if (smallest != free_buffers_.end() &&
|
||||
smallest->capacity() < storage.capacity()) {
|
||||
*smallest = std::move(storage);
|
||||
}
|
||||
}
|
||||
|
||||
CapturedNv12Frame::CapturedNv12Frame(
|
||||
std::shared_ptr<CapturedNv12FramePool> pool,
|
||||
std::vector<uint8_t> storage, uint32_t width, uint32_t height)
|
||||
: pool_(std::move(pool)),
|
||||
storage_(std::move(storage)),
|
||||
width_(width),
|
||||
height_(height) {
|
||||
descriptor_.struct_size = sizeof(descriptor_);
|
||||
descriptor_.type = MiniRtcNativeVideoFrameCpuNv12;
|
||||
descriptor_.width = width_;
|
||||
descriptor_.height = height_;
|
||||
descriptor_.payload.cpu_nv12 = {
|
||||
storage_.data(), storage_.data() + static_cast<size_t>(width_) * height_,
|
||||
width_, width_};
|
||||
descriptor_.owner = this;
|
||||
descriptor_.retain = &RetainOwner;
|
||||
descriptor_.release = &ReleaseOwner;
|
||||
descriptor_.copy_to_nv12 = &CopyToNv12;
|
||||
}
|
||||
|
||||
CapturedNv12Frame::~CapturedNv12Frame() {
|
||||
if (pool_) {
|
||||
pool_->Recycle(std::move(storage_));
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::AddRef() {
|
||||
references_.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::Release() {
|
||||
if (references_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::RetainOwner(void* owner) {
|
||||
static_cast<CapturedNv12Frame*>(owner)->AddRef();
|
||||
}
|
||||
|
||||
void CapturedNv12Frame::ReleaseOwner(void* owner) {
|
||||
static_cast<CapturedNv12Frame*>(owner)->Release();
|
||||
}
|
||||
|
||||
int CapturedNv12Frame::CopyToNv12(void* owner, uint8_t* destination,
|
||||
size_t destination_size) {
|
||||
auto* frame = static_cast<CapturedNv12Frame*>(owner);
|
||||
if (!frame || !destination || destination_size < frame->Size()) {
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(destination, frame->storage_.data(), frame->Size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-04
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CAPTURED_NV12_FRAME_H_
|
||||
#define _CAPTURED_NV12_FRAME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class CapturedNv12Frame;
|
||||
|
||||
// Owns the pixels behind captured native frames. A frame may outlive the
|
||||
// capture callback while MiniRTC waits for its asynchronous encoder, so capture
|
||||
// buffers cannot be borrowed directly. Released allocations are cached to keep
|
||||
// that ownership transfer inexpensive after the pipeline is warm.
|
||||
class CapturedNv12FramePool final
|
||||
: public std::enable_shared_from_this<CapturedNv12FramePool> {
|
||||
public:
|
||||
static std::shared_ptr<CapturedNv12FramePool> Create(
|
||||
size_t max_cached_buffers = 4);
|
||||
|
||||
CapturedNv12Frame* CopyFrom(const uint8_t* data, size_t size,
|
||||
uint32_t width, uint32_t height);
|
||||
|
||||
private:
|
||||
explicit CapturedNv12FramePool(size_t max_cached_buffers);
|
||||
void Recycle(std::vector<uint8_t> storage);
|
||||
|
||||
friend class CapturedNv12Frame;
|
||||
|
||||
const size_t max_cached_buffers_;
|
||||
std::mutex mutex_;
|
||||
std::vector<std::vector<uint8_t>> free_buffers_;
|
||||
};
|
||||
|
||||
class CapturedNv12Frame final {
|
||||
public:
|
||||
const MiniRtcNativeVideoFrame* Descriptor() const { return &descriptor_; }
|
||||
size_t Size() const { return storage_.size(); }
|
||||
|
||||
void AddRef();
|
||||
void Release();
|
||||
|
||||
private:
|
||||
friend class CapturedNv12FramePool;
|
||||
|
||||
CapturedNv12Frame(std::shared_ptr<CapturedNv12FramePool> pool,
|
||||
std::vector<uint8_t> storage, uint32_t width,
|
||||
uint32_t height);
|
||||
~CapturedNv12Frame();
|
||||
|
||||
static void RetainOwner(void* owner);
|
||||
static void ReleaseOwner(void* owner);
|
||||
static int CopyToNv12(void* owner, uint8_t* destination,
|
||||
size_t destination_size);
|
||||
|
||||
std::atomic<uint32_t> references_{1};
|
||||
std::shared_ptr<CapturedNv12FramePool> pool_;
|
||||
std::vector<uint8_t> storage_;
|
||||
uint32_t width_ = 0;
|
||||
uint32_t height_ = 0;
|
||||
MiniRtcNativeVideoFrame descriptor_{};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -10,18 +10,14 @@
|
||||
#include <functional>
|
||||
|
||||
#include "display_info.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ScreenCapturer {
|
||||
public:
|
||||
// |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
|
||||
// display name or physical handle. |native_frame| is borrowed for the
|
||||
// duration of the callback; retain its owner before using it asynchronously.
|
||||
typedef std::function<void(unsigned char* data, int size, int width,
|
||||
int height, const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame)>
|
||||
// The final callback argument is a logical MiniRTC stream ID (DisplayN),
|
||||
// not a platform display name or physical handle.
|
||||
typedef std::function<void(unsigned char*, int, int, int, const char*)>
|
||||
cb_desktop_data;
|
||||
|
||||
public:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user