mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-29 03:08:42 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d32974dc18 | |||
| efd8b7c0ff | |||
| 383452ed3b | |||
| 2570e57958 | |||
| eed90dad34 | |||
| c53ce2a581 | |||
| 708c39bb99 | |||
| cee249a25b | |||
| 7c3b13409c |
@@ -103,12 +103,29 @@ jobs:
|
||||
- name: Verify Linux build image
|
||||
run: verify-crossdesk-build-image
|
||||
|
||||
- name: Ensure Wayland build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
if pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
libdbus-1-dev \
|
||||
libdrm-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libspa-0.2-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2
|
||||
|
||||
- name: Build CrossDesk
|
||||
env:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
- name: Package
|
||||
@@ -136,19 +153,20 @@ jobs:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "14.0"
|
||||
EXPECTED_MACOS_SDK: "26.5"
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: x64
|
||||
binary_arch: x86_64
|
||||
runner: macos-15-intel
|
||||
cache-key: intel-macos14
|
||||
runner: macos-26-intel
|
||||
cache-key: intel-macos26-xcode26.6-min14
|
||||
out-dir: ./build/macosx/x86_64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_x64.sh
|
||||
- arch: arm64
|
||||
binary_arch: arm64
|
||||
runner: macos-14
|
||||
cache-key: arm-macos14
|
||||
runner: macos-26
|
||||
cache-key: arm-macos26-xcode26.6-min14
|
||||
out-dir: ./build/macosx/arm64/release/crossdesk
|
||||
package_script: ./scripts/macosx/pkg_arm64.sh
|
||||
|
||||
@@ -193,6 +211,17 @@ jobs:
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Select Xcode 26.6
|
||||
shell: bash
|
||||
run: |
|
||||
sudo xcode-select -s /Applications/Xcode_26.6.app/Contents/Developer
|
||||
xcodebuild -version
|
||||
SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)"
|
||||
if [[ "${SDK_VERSION}" != "${EXPECTED_MACOS_SDK}" ]]; then
|
||||
echo "Unexpected macOS SDK: expected ${EXPECTED_MACOS_SDK}, got ${SDK_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -237,7 +266,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}"
|
||||
./scripts/macosx/verify_app.sh "${APP_PATH}" "${{ matrix.binary_arch }}" "${EXPECTED_MACOS_SDK}"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -424,8 +453,7 @@ jobs:
|
||||
release:
|
||||
name: Publish Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
[build-linux, build-macos, build-windows-x64]
|
||||
needs: [build-linux, build-macos, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -52,9 +52,11 @@ RUN apt-get update \
|
||||
libfreetype6-dev \
|
||||
libgl1-mesa-dev \
|
||||
libmount-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libpulse-dev \
|
||||
libreadline-dev \
|
||||
libsndio-dev \
|
||||
libspa-0.2-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libx11-dev \
|
||||
@@ -134,7 +136,7 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
|
||||
# CrossDesk's exact dependency graph. Source files are intentionally excluded
|
||||
# from the Docker context; source-only changes therefore reuse this layer.
|
||||
RUN xmake repo -u --root \
|
||||
&& xmake f -m release --USE_CUDA=true --root -y \
|
||||
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true --root -y \
|
||||
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
|
||||
&& mkdir -p /workspace
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# CrossDesk Linux 构建镜像
|
||||
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
|
||||
Rust、Linux 开发库以及根据项目 `xmake.lua` 提前编译好的依赖。
|
||||
Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA)以及根据项目
|
||||
`xmake.lua` 提前编译好的依赖。
|
||||
|
||||
## 发布方式
|
||||
|
||||
|
||||
@@ -30,7 +30,17 @@ rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
|
||||
cargo --version >/dev/null
|
||||
cmake --version >/dev/null
|
||||
g++ --version >/dev/null
|
||||
pkg-config --exists xft
|
||||
for pkg_config_package in \
|
||||
dbus-1 \
|
||||
libdrm \
|
||||
libpipewire-0.3 \
|
||||
libspa-0.2 \
|
||||
xft; do
|
||||
if ! pkg-config --exists "${pkg_config_package}"; then
|
||||
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for package_path in \
|
||||
"s/slint" \
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
|
||||
if [[ $# -lt 2 || $# -gt 3 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
APP_BUNDLE="$1"
|
||||
EXPECTED_ARCH="$2"
|
||||
EXPECTED_SDK_VERSION="${3:-}"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
|
||||
@@ -94,6 +95,23 @@ if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
|
||||
verify_sdk_version() {
|
||||
local macho="$1"
|
||||
local actual_sdk=""
|
||||
actual_sdk="$(otool -l "$macho" | awk '
|
||||
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
|
||||
build_version && $1 == "sdk" { print $2; exit }')"
|
||||
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
|
||||
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
verify_sdk_version "$APP_EXECUTABLE"
|
||||
verify_sdk_version "$SLINT_LIBRARY"
|
||||
fi
|
||||
|
||||
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
|
||||
|
||||
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
@@ -10,6 +11,7 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
|
||||
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
|
||||
|
||||
static int VkCodeFromUnicode(UniChar ch) {
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
@@ -103,6 +105,10 @@ static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
|
||||
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
CGEventRef event, void* userInfo) {
|
||||
(void)proxy;
|
||||
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
|
||||
kCrossDeskInjectedKeyboardEvent) {
|
||||
return event;
|
||||
}
|
||||
if (!g_on_key_action) {
|
||||
return event;
|
||||
}
|
||||
@@ -302,6 +308,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
}
|
||||
|
||||
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
|
||||
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
|
||||
kCrossDeskInjectedKeyboardEvent);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
@@ -314,6 +322,8 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
|
||||
return -1;
|
||||
}
|
||||
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
|
||||
kCrossDeskInjectedKeyboardEvent);
|
||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||
CFRelease(fn_release_event);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if ((kbData->flags & LLKHF_INJECTED) != 0) {
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
|
||||
@@ -65,7 +65,7 @@ struct InteractionState {
|
||||
bool start_keyboard_capturer_ = false;
|
||||
bool show_cursor_ = false;
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool keyboard_capturer_uses_sdl_events_ = false;
|
||||
bool keyboard_capturer_uses_window_events_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool focus_on_stream_window_ = false;
|
||||
bool audio_capture_ = false;
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/tray/mac_tray.h"
|
||||
@@ -1726,6 +1728,8 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
bool control, bool alt, bool shift, bool meta) {
|
||||
SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
|
||||
});
|
||||
stream->on_keyboard_focus_changed(
|
||||
[this](bool focused) { SetStreamKeyboardFocus(focused); });
|
||||
}
|
||||
|
||||
void GuiApplication::BindServerCallbacks() {
|
||||
@@ -1829,8 +1833,13 @@ void GuiApplication::Tick() {
|
||||
}
|
||||
HandleConnectionStatusChange();
|
||||
HandlePendingPresenceProbe();
|
||||
HandleConnectionTimeouts();
|
||||
HandlePresenceProbeTimeout();
|
||||
HandleWindowsServiceIntegration();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
SyncXWaylandWindowActivation();
|
||||
#else
|
||||
SyncStreamKeyboardFocus();
|
||||
#endif
|
||||
devices_.UpdateInteractions();
|
||||
|
||||
UpdateLocalization();
|
||||
@@ -1839,9 +1848,6 @@ void GuiApplication::Tick() {
|
||||
SyncPlatformDialogs();
|
||||
SyncStreamWindow();
|
||||
SyncServerWindow();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
SyncXWaylandWindowActivation();
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuiApplication::UpdateLocalization() {
|
||||
@@ -2228,6 +2234,7 @@ void GuiApplication::SyncXWaylandWindowActivation() {
|
||||
Display* display = ui_->x11_focus_display;
|
||||
const ::Window active_window = X11GetActiveWindow(display);
|
||||
if (!active_window) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2237,19 +2244,49 @@ void GuiApplication::SyncXWaylandWindowActivation() {
|
||||
if (ui_->stream) {
|
||||
(*ui_->stream)->set_window_active(false);
|
||||
}
|
||||
SetStreamKeyboardFocus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ui_->main->set_window_active(
|
||||
X11WindowMatches(display, active_window, ui_->main->window()));
|
||||
if (ui_->stream) {
|
||||
(*ui_->stream)
|
||||
->set_window_active(
|
||||
X11WindowMatches(display, active_window, (*ui_->stream)->window()));
|
||||
const bool stream_active =
|
||||
X11WindowMatches(display, active_window, (*ui_->stream)->window());
|
||||
(*ui_->stream)->set_window_active(stream_active);
|
||||
SetStreamKeyboardFocus(stream_active);
|
||||
} else {
|
||||
SetStreamKeyboardFocus(false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
|
||||
if (focus_on_stream_window_ == focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
focus_on_stream_window_ = focused;
|
||||
if (!focused) {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiApplication::SyncStreamKeyboardFocus() {
|
||||
if (!ui_ || !ui_->stream || !(*ui_->stream)->window().is_visible()) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const HWND stream_hwnd = (*ui_->stream)->window().win32_hwnd();
|
||||
SetStreamKeyboardFocus(stream_hwnd != nullptr &&
|
||||
GetForegroundWindow() == stream_hwnd);
|
||||
#elif defined(__APPLE__)
|
||||
SetStreamKeyboardFocus(IsStreamWindowActive());
|
||||
#endif
|
||||
}
|
||||
|
||||
void GuiApplication::SyncStreamWindow() {
|
||||
bool has_sessions = false;
|
||||
{
|
||||
@@ -2306,6 +2343,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
}
|
||||
}
|
||||
if (!has_sessions) {
|
||||
SetStreamKeyboardFocus(false);
|
||||
#if defined(__APPLE__)
|
||||
SetStreamWindowFullscreen(false);
|
||||
#endif
|
||||
@@ -2787,14 +2825,25 @@ void GuiApplication::SelectStreamTab(int index) {
|
||||
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
|
||||
return;
|
||||
}
|
||||
focused_remote_id_ = ui_->tab_ids[index];
|
||||
controlled_remote_id_ = focused_remote_id_;
|
||||
const std::string selected_remote_id = ui_->tab_ids[index];
|
||||
if (!controlled_remote_id_.empty() &&
|
||||
controlled_remote_id_ != selected_remote_id) {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
}
|
||||
focused_remote_id_ = selected_remote_id;
|
||||
controlled_remote_id_ = selected_remote_id;
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto& [id, props] : remote_sessions_) {
|
||||
if (props) {
|
||||
props->tab_selected_ = id == focused_remote_id_;
|
||||
}
|
||||
}
|
||||
const auto selected = remote_sessions_.find(selected_remote_id);
|
||||
start_keyboard_capturer_ = selected != remote_sessions_.end() &&
|
||||
selected->second &&
|
||||
selected->second->control_mouse_ &&
|
||||
selected->second->connection_status_.load() ==
|
||||
ConnectionStatus::Connected;
|
||||
}
|
||||
|
||||
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
|
||||
@@ -2982,10 +3031,22 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
|
||||
(void)alt;
|
||||
(void)shift;
|
||||
(void)meta;
|
||||
if (auto props = SelectedSession()) {
|
||||
controlled_remote_id_ = props->remote_id_;
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->peer_ || !props->control_mouse_ ||
|
||||
props->connection_status_.load() != ConnectionStatus::Connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
controlled_remote_id_ = props->remote_id_;
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
// Native hooks see the same physical key before Slint does. When a native
|
||||
// hook is active, forwarding the FocusScope callback as well would duplicate
|
||||
// the event on platforms whose hook does not consume the local key.
|
||||
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int virtual_key = SlintKeyToWindowsVk(text);
|
||||
if (virtual_key >= 0) {
|
||||
keyboard_.SendKeyCommand(virtual_key, pressed);
|
||||
|
||||
@@ -35,6 +35,8 @@ private:
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
void SyncXWaylandWindowActivation();
|
||||
|
||||
@@ -200,7 +200,8 @@ void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
|
||||
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
|
||||
if (keyboard_capturer_is_started_ &&
|
||||
keyboard_capturer_uses_window_events_ &&
|
||||
focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.key.windowID) {
|
||||
ProcessKeyboardEvent(event);
|
||||
|
||||
@@ -228,27 +228,27 @@ int SessionDeviceManager::StopMouseController() {
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = false;
|
||||
owner_.keyboard_capturer_uses_window_events_ = false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacAccessibilityPermission()) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!keyboard_capturer_) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"keyboard capturer is nullptr, falling back to SDL keyboard events");
|
||||
"keyboard capturer is nullptr, falling back to Slint keyboard events");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -263,9 +263,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
},
|
||||
&owner_);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = true;
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to SDL keyboard events");
|
||||
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native hook");
|
||||
}
|
||||
@@ -273,9 +273,9 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
if (owner_.keyboard_capturer_uses_sdl_events_) {
|
||||
owner_.keyboard_capturer_uses_sdl_events_ = false;
|
||||
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
|
||||
if (owner_.keyboard_capturer_uses_window_events_) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = false;
|
||||
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ bool HideDisabledMainWindowZoomButton();
|
||||
// fullscreen action with an immediate, single-window fullscreen transition.
|
||||
bool ConfigureStreamWindowLiveResize();
|
||||
|
||||
// Returns whether the configured stream window is currently the active AppKit
|
||||
// key window.
|
||||
bool IsStreamWindowActive();
|
||||
|
||||
// Enters or leaves the stream window's animation-free fullscreen mode.
|
||||
bool SetStreamWindowFullscreen(bool fullscreen);
|
||||
bool IsStreamWindowFullscreen();
|
||||
|
||||
@@ -146,6 +146,13 @@ bool ConfigureStreamWindowLiveResize() {
|
||||
}
|
||||
}
|
||||
|
||||
bool IsStreamWindowActive() {
|
||||
@autoreleasepool {
|
||||
return stream_window != nil && [NSApp isActive] &&
|
||||
[stream_window isKeyWindow];
|
||||
}
|
||||
}
|
||||
|
||||
bool SetStreamWindowFullscreen(bool fullscreen) {
|
||||
@autoreleasepool {
|
||||
if (stream_window == nil || stream_fullscreen == fullscreen) {
|
||||
|
||||
@@ -17,11 +17,6 @@ namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
|
||||
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
|
||||
bool IsConnectionAttemptPending(ConnectionStatus status) {
|
||||
return status == ConnectionStatus::Connecting ||
|
||||
status == ConnectionStatus::Gathering;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GuiRuntime::HandleConnectionStatusChange() {
|
||||
@@ -95,7 +90,7 @@ void GuiRuntime::HandlePendingPresenceProbe() {
|
||||
show_offline_warning_window_ = true;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleConnectionTimeouts() {
|
||||
void GuiRuntime::HandlePresenceProbeTimeout() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
bool presence_probe_timed_out = false;
|
||||
@@ -121,44 +116,6 @@ void GuiRuntime::HandleConnectionTimeouts() {
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
|
||||
}
|
||||
|
||||
bool rejoin_state_changed = false;
|
||||
for (auto& [_, props] : remote_sessions_) {
|
||||
if (!props || !props->connection_attempt_active_.load()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ConnectionStatus status = props->connection_status_.load();
|
||||
if (!IsConnectionAttemptPending(status)) {
|
||||
props->connection_attempt_active_.store(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - props->connection_attempt_started_at_ <
|
||||
kConnectionAttemptTimeout) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
|
||||
static_cast<int>(status));
|
||||
props->connection_attempt_active_.store(false);
|
||||
props->connection_established_ = false;
|
||||
props->rejoin_ = false;
|
||||
props->connection_status_.store(ConnectionStatus::Failed);
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
show_connection_status_window_ = true;
|
||||
rejoin_state_changed = true;
|
||||
}
|
||||
|
||||
if (rejoin_state_changed) {
|
||||
need_to_rejoin_ = false;
|
||||
for (const auto& [_, props] : remote_sessions_) {
|
||||
if (props && props->rejoin_) {
|
||||
need_to_rejoin_ = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleServerControllerDisconnected(
|
||||
@@ -455,8 +412,6 @@ int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
auto props = remote_sessions_[remote_id];
|
||||
if (!props->connection_established_) {
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
props->connection_attempt_active_.store(true);
|
||||
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
props->remember_password_ = remember_password;
|
||||
|
||||
@@ -45,7 +45,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
void HandleRecentConnections();
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandleConnectionTimeouts();
|
||||
void HandlePresenceProbeTimeout();
|
||||
void HandleServerControllerDisconnected(const std::string& remote_id,
|
||||
const char* reason);
|
||||
void HandleWindowsServiceIntegration();
|
||||
|
||||
@@ -170,10 +170,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->is_client_mode_ = true;
|
||||
runtime->show_connection_status_window_ = true;
|
||||
props->connection_status_.store(status);
|
||||
if (status != ConnectionStatus::Connecting &&
|
||||
status != ConnectionStatus::Gathering) {
|
||||
props->connection_attempt_active_.store(false);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
|
||||
@@ -58,7 +58,7 @@ struct FileTransferState {
|
||||
// connection, media, input, window and transfer data that share one lifetime.
|
||||
struct RemoteSession {
|
||||
Params params_;
|
||||
PeerPtr *peer_ = nullptr;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string mouse_label_ = "mouse";
|
||||
@@ -74,8 +74,6 @@ struct RemoteSession {
|
||||
SignalStatus signal_status_ = SignalStatus::SignalClosed;
|
||||
bool connection_established_ = false;
|
||||
bool rejoin_ = false;
|
||||
std::atomic<bool> connection_attempt_active_ = false;
|
||||
std::chrono::steady_clock::time_point connection_attempt_started_at_;
|
||||
bool net_traffic_stats_button_pressed_ = false;
|
||||
bool enable_mouse_control_ = true;
|
||||
bool mouse_controller_is_started_ = false;
|
||||
@@ -160,6 +158,6 @@ struct RemoteSession {
|
||||
|
||||
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
|
||||
@@ -163,6 +163,7 @@ export component StreamWindow inherits Window {
|
||||
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
|
||||
callback scroll-input(length, length, length, length);
|
||||
callback key-input(string, bool, bool, bool, bool, bool);
|
||||
callback keyboard-focus-changed(bool);
|
||||
|
||||
private property <bool> control-expanded: true;
|
||||
private property <bool> display-menu-open: false;
|
||||
@@ -486,6 +487,8 @@ export component StreamWindow inherits Window {
|
||||
|
||||
input-focus := FocusScope {
|
||||
focus-on-click: true;
|
||||
focus-gained => { root.keyboard-focus-changed(true); }
|
||||
focus-lost => { root.keyboard-focus-changed(false); }
|
||||
key-pressed(event) => {
|
||||
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
accept
|
||||
@@ -500,6 +503,9 @@ export component StreamWindow inherits Window {
|
||||
&& self.mouse-x <= control.x + control.width
|
||||
&& self.mouse-y >= control.y
|
||||
&& self.mouse-y <= control.y + control.height;
|
||||
if event.kind == PointerEventKind.down && !over-control {
|
||||
input-focus.focus();
|
||||
}
|
||||
if event.kind == PointerEventKind.down
|
||||
&& event.button == PointerEventButton.left
|
||||
&& over-control {
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: e6d796d7b7...00aad6aa37
@@ -104,16 +104,22 @@ int main() {
|
||||
"return localization::device_offline[language];");
|
||||
ok &= ExpectContains("runtime_state.h", runtime_state_h,
|
||||
"pending_presence_probe_started_at_");
|
||||
ok &= ExpectContains("remote_session.h", remote_session_h,
|
||||
"connection_attempt_started_at_");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"HandleConnectionTimeouts");
|
||||
"HandlePresenceProbeTimeout");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"kPresenceProbeTimeout");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"kConnectionAttemptTimeout");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"connection_attempt_started_at_");
|
||||
ok &= ExpectNotContains("remote_session.h", remote_session_h,
|
||||
"connection_attempt_started_at_");
|
||||
ok &= ExpectNotContains("remote_session.h", remote_session_h,
|
||||
"connection_attempt_active_");
|
||||
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"kConnectionAttemptTimeout");
|
||||
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"ConnectionStatus::Failed");
|
||||
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
|
||||
"props->connection_status_.store(status);");
|
||||
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
|
||||
"case ConnectionStatus::Failed:");
|
||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||
"HandleServerControllerDisconnected");
|
||||
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
|
||||
|
||||
@@ -187,6 +187,26 @@ int main() {
|
||||
stream->invoke_toggle_maximize_stream_window();
|
||||
assert(maximize_requested);
|
||||
|
||||
bool keyboard_focus_changed = false;
|
||||
bool keyboard_input_received = false;
|
||||
stream->on_keyboard_focus_changed(
|
||||
[&](bool focused) { keyboard_focus_changed = focused; });
|
||||
stream->on_key_input(
|
||||
[&](slint::SharedString text, bool pressed, bool, bool, bool, bool) {
|
||||
keyboard_input_received = std::string(text) == "a" && pressed;
|
||||
});
|
||||
stream->window().set_size(
|
||||
slint::LogicalSize(slint::Size<float>{1280.0f, 720.0f}));
|
||||
stream->window().dispatch_pointer_press_event(
|
||||
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
|
||||
slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(
|
||||
slint::LogicalPosition(slint::Point<float>{640.0f, 360.0f}),
|
||||
slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_key_press_event("a");
|
||||
assert(keyboard_focus_changed);
|
||||
assert(keyboard_input_received);
|
||||
|
||||
crossdesk::ui::FileTransferEntry transfer;
|
||||
transfer.name = "archive.zip";
|
||||
transfer.status = "Sending";
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "submodules/minirtc/src/transport/ice_transport_controller.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string ExtractSection(const std::string& source, const std::string& begin,
|
||||
const std::string& end) {
|
||||
const size_t begin_pos = source.find(begin);
|
||||
if (begin_pos == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
const size_t end_pos = source.find(end, begin_pos + begin.size());
|
||||
if (end_pos == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
return source.substr(begin_pos, end_pos - begin_pos);
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unsafe text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string controller = ReadFile(
|
||||
repo_root /
|
||||
"submodules/minirtc/src/transport/ice_transport_controller.cpp");
|
||||
const std::string destroy = ExtractSection(
|
||||
controller, "void IceTransportController::Destroy()",
|
||||
"uint32_t IceTransportController::AddVideoSendChannel");
|
||||
const std::string send_video = ExtractSection(
|
||||
controller, "int IceTransportController::SendVideo(",
|
||||
"void IceTransportController::MaybeDegradeResolutionOnEncodeTime");
|
||||
const std::string on_video_encoded = ExtractSection(
|
||||
controller, "int IceTransportController::OnVideoEncoded(",
|
||||
"int IceTransportController::SendAudio(");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("Destroy", destroy,
|
||||
"std::unique_lock lock(stream_senders_mutex_);");
|
||||
ok &= ExpectContains("Destroy", destroy,
|
||||
"senders.swap(stream_senders_);");
|
||||
ok &= ExpectContains("Destroy", destroy,
|
||||
"codecs.push_back(std::move(context->codec));");
|
||||
ok &= ExpectContains("Destroy", destroy, "senders.clear();");
|
||||
ok &= ExpectContains("Destroy", destroy, "codecs.clear();");
|
||||
ok &= ExpectNotContains("Destroy", destroy,
|
||||
"std::shared_lock lock(stream_senders_mutex_);");
|
||||
|
||||
ok &= ExpectContains("SendVideo", send_video,
|
||||
"std::weak_ptr<IceTransportController> weak_self");
|
||||
ok &= ExpectContains("SendVideo", send_video,
|
||||
"std::weak_ptr<StreamContext> weak_context");
|
||||
ok &= ExpectContains(
|
||||
"SendVideo", send_video,
|
||||
"[weak_self, weak_context, channel_name, queue_delay_ms");
|
||||
ok &= ExpectContains("SendVideo", send_video,
|
||||
"return self->OnVideoEncoded(");
|
||||
ok &= ExpectNotContains("SendVideo", send_video,
|
||||
"[this, channel_name, context");
|
||||
|
||||
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
|
||||
"if (!is_running_.load())");
|
||||
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
|
||||
"std::shared_lock lock(stream_senders_mutex_);");
|
||||
ok &= ExpectContains("OnVideoEncoded", on_video_encoded,
|
||||
"it->second != context");
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -73,6 +73,11 @@ function setup_targets()
|
||||
set_default(false)
|
||||
add_files("tests/connection_status_protocol_test.cpp")
|
||||
|
||||
target("video_callback_lifetime_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/video_callback_lifetime_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
|
||||
Reference in New Issue
Block a user