diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 989e1ecf..6550fc5a 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1225,6 +1225,9 @@ bool GuiApplication::InitializeSDL() { "windows while retaining Wayland portal capture and input"); } #endif + // SDL disables the screensaver by default. A background remote desktop app + // should leave automatic display sleep under the user's system settings. + SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { LOG_ERROR("SDL initialization failed: {}", SDL_GetError()); return false; diff --git a/apps/desktop/src/gui/features/devices/screen_sharing_power.cpp b/apps/desktop/src/gui/features/devices/screen_sharing_power.cpp new file mode 100644 index 00000000..333be90b --- /dev/null +++ b/apps/desktop/src/gui/features/devices/screen_sharing_power.cpp @@ -0,0 +1,245 @@ +#include "features/devices/screen_sharing_power.h" + +#include "rd_log.h" + +#ifdef _WIN32 +#include +#elif defined(__APPLE__) +#include +#elif defined(__linux__) && !defined(__OHOS__) +#include +#include +#else +#include +#endif + +namespace crossdesk { + +#if defined(__linux__) && !defined(__OHOS__) +namespace { + +constexpr const char* kApplication = "CrossDesk"; +constexpr const char* kReason = "Sharing the desktop with a remote controller"; +constexpr int kDbusTimeoutMs = 1000; + +struct ConnectionCloser { + void operator()(DBusConnection* connection) const { + dbus_connection_close(connection); + dbus_connection_unref(connection); + } +}; +using BusConnection = std::unique_ptr; +using BusMessage = std::unique_ptr; + +BusConnection OpenBus(DBusBusType type) { + DBusError error; + dbus_error_init(&error); + BusConnection connection(dbus_bus_get_private(type, &error)); + if (connection) { + dbus_connection_set_exit_on_disconnect(connection.get(), false); + } else { + LOG_WARN("Cannot connect to power management bus: {}", + error.message ? error.message : "unknown error"); + } + dbus_error_free(&error); + return connection; +} + +BusMessage Call(DBusConnection* connection, DBusMessage* message) { + return BusMessage(dbus_connection_send_with_reply_and_block( + connection, message, kDbusTimeoutMs, nullptr), + dbus_message_unref); +} + +} // namespace +#endif + +struct ScreenSharingPower::Impl { +#ifdef _WIN32 + HANDLE request = INVALID_HANDLE_VALUE; + bool display = false; + bool system = false; + + void Start() { + wchar_t reason_text[] = L"CrossDesk is sharing the desktop"; + REASON_CONTEXT reason{}; + reason.Version = POWER_REQUEST_CONTEXT_VERSION; + reason.Flags = POWER_REQUEST_CONTEXT_SIMPLE_STRING; + reason.Reason.SimpleReasonString = reason_text; + request = PowerCreateRequest(&reason); + if (request == INVALID_HANDLE_VALUE) { + LOG_WARN("Cannot create screen sharing power request: {}", GetLastError()); + return; + } + // Handle-based requests also work in the unattended host, which has no + // SDL video subsystem, and do not depend on the calling thread's lifetime. + system = PowerSetRequest(request, PowerRequestSystemRequired) != 0; + if (!system) + LOG_WARN("Cannot prevent system idle sleep: {}", GetLastError()); + display = PowerSetRequest(request, PowerRequestDisplayRequired) != 0; + if (!display) + LOG_WARN("Cannot prevent display idle sleep: {}", GetLastError()); + } + + void Stop() { + if (request == INVALID_HANDLE_VALUE) return; + if (display) PowerClearRequest(request, PowerRequestDisplayRequired); + if (system) PowerClearRequest(request, PowerRequestSystemRequired); + CloseHandle(request); + request = INVALID_HANDLE_VALUE; + display = system = false; + } +#elif defined(__APPLE__) + IOPMAssertionID display = kIOPMNullAssertionID; + IOPMAssertionID system = kIOPMNullAssertionID; + + void Start() { + const auto reason = CFSTR("CrossDesk is sharing the desktop"); + auto result = IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleSystemSleep, kIOPMAssertionLevelOn, + reason, &system); + if (result != kIOReturnSuccess) + LOG_WARN("Cannot prevent system idle sleep: {}", result); + result = IOPMAssertionCreateWithName( + kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, + reason, &display); + if (result != kIOReturnSuccess) + LOG_WARN("Cannot prevent display idle sleep: {}", result); + } + + void Stop() { + if (display != kIOPMNullAssertionID) IOPMAssertionRelease(display); + if (system != kIOPMNullAssertionID) IOPMAssertionRelease(system); + display = system = kIOPMNullAssertionID; + } +#elif defined(__linux__) && !defined(__OHOS__) + BusConnection session; + dbus_uint32_t cookie = 0; + bool desktop_inhibited = false; + bool gnome = false; + int sleep_fd = -1; + + bool InhibitDesktop(bool use_gnome) { + const char* service = use_gnome ? "org.gnome.SessionManager" + : "org.freedesktop.ScreenSaver"; + const char* path = use_gnome ? "/org/gnome/SessionManager" + : "/org/freedesktop/ScreenSaver"; + BusMessage message(dbus_message_new_method_call(service, path, service, + "Inhibit"), + dbus_message_unref); + if (!message) return false; + dbus_bool_t appended; + if (use_gnome) { + const dbus_uint32_t window = 0; + const dbus_uint32_t flags = 4 | 8; // Suspend and session idle. + appended = dbus_message_append_args( + message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_UINT32, + &window, DBUS_TYPE_STRING, &kReason, DBUS_TYPE_UINT32, &flags, + DBUS_TYPE_INVALID); + } else { + appended = dbus_message_append_args( + message.get(), DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING, + &kReason, DBUS_TYPE_INVALID); + } + if (!appended) return false; + auto reply = Call(session.get(), message.get()); + if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UINT32, + &cookie, DBUS_TYPE_INVALID)) + return false; + gnome = use_gnome; + desktop_inhibited = true; + return true; + } + + void Start() { + if (!dbus_threads_init_default()) { + LOG_WARN("Cannot initialize D-Bus for screen sharing power management"); + return; + } + // These session APIs work without an SDL window, including native Wayland + // and a Slint window hidden in the tray. GNOME's API inhibits both kinds of + // idle sleep; other desktops use ScreenSaver plus logind below. + session = OpenBus(DBUS_BUS_SESSION); + if (session && InhibitDesktop(true)) return; + if (!session || !InhibitDesktop(false)) + LOG_WARN("Cannot inhibit desktop idle while sharing the screen"); + + auto system_bus = OpenBus(DBUS_BUS_SYSTEM); + if (!system_bus) return; + BusMessage message(dbus_message_new_method_call( + "org.freedesktop.login1", "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", "Inhibit"), + dbus_message_unref); + const char* what = "sleep:idle"; + const char* mode = "block"; + if (!message || !dbus_message_append_args( + message.get(), DBUS_TYPE_STRING, &what, + DBUS_TYPE_STRING, &kApplication, DBUS_TYPE_STRING, + &kReason, DBUS_TYPE_STRING, &mode, DBUS_TYPE_INVALID)) { + LOG_WARN("Cannot create system sleep inhibition request"); + return; + } + auto reply = Call(system_bus.get(), message.get()); + // get_args duplicates the returned fd with CLOEXEC. Holding this fd keeps + // the inhibitor alive independently of the reply or D-Bus connection. + if (!reply || !dbus_message_get_args(reply.get(), nullptr, DBUS_TYPE_UNIX_FD, + &sleep_fd, DBUS_TYPE_INVALID)) + LOG_WARN("Cannot inhibit system sleep while sharing the screen"); + } + + void Stop() { + if (sleep_fd >= 0) { + close(sleep_fd); + sleep_fd = -1; + } + if (session && desktop_inhibited && + dbus_connection_get_is_connected(session.get())) { + const char* service = gnome ? "org.gnome.SessionManager" + : "org.freedesktop.ScreenSaver"; + const char* path = gnome ? "/org/gnome/SessionManager" + : "/org/freedesktop/ScreenSaver"; + BusMessage message(dbus_message_new_method_call( + service, path, service, + gnome ? "Uninhibit" : "UnInhibit"), + dbus_message_unref); + if (message && dbus_message_append_args( + message.get(), DBUS_TYPE_UINT32, &cookie, + DBUS_TYPE_INVALID)) { + dbus_message_set_no_reply(message.get(), true); + dbus_connection_send(session.get(), message.get(), nullptr); + dbus_connection_flush(session.get()); + } + } + // A private connection also revokes the desktop inhibitor if UnInhibit + // could not be sent. Nothing is retained by unrelated D-Bus clients. + session.reset(); + desktop_inhibited = false; + gnome = false; + cookie = 0; + } +#else + void Start() { + if (!SDL_DisableScreenSaver()) + LOG_WARN("Cannot prevent display idle sleep: {}", SDL_GetError()); + } + void Stop() { SDL_EnableScreenSaver(); } +#endif +}; + +ScreenSharingPower::ScreenSharingPower() : impl_(std::make_unique()) {} + +ScreenSharingPower::~ScreenSharingPower() { SetSharing(false); } + +void ScreenSharingPower::SetSharing(bool sharing) { + if (sharing_ == sharing) return; + sharing_ = sharing; + if (sharing) { + impl_->Start(); + LOG_INFO("Requested display and system idle-sleep inhibition for screen sharing"); + } else { + impl_->Stop(); + LOG_INFO("Released screen sharing power inhibitors"); + } +} + +} // namespace crossdesk diff --git a/apps/desktop/src/gui/features/devices/screen_sharing_power.h b/apps/desktop/src/gui/features/devices/screen_sharing_power.h new file mode 100644 index 00000000..13d4a322 --- /dev/null +++ b/apps/desktop/src/gui/features/devices/screen_sharing_power.h @@ -0,0 +1,33 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-24 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SCREEN_SHARING_POWER_H_ +#define _SCREEN_SHARING_POWER_H_ + +#include + +namespace crossdesk { + +// Owns temporary display/system idle-sleep inhibitors for the shared capture +// source. Call on the UI thread; repeated updates do not acquire extra requests. +class ScreenSharingPower { + public: + ScreenSharingPower(); + ~ScreenSharingPower(); + ScreenSharingPower(const ScreenSharingPower&) = delete; + ScreenSharingPower& operator=(const ScreenSharingPower&) = delete; + + void SetSharing(bool sharing); + + private: + struct Impl; + std::unique_ptr impl_; + bool sharing_ = false; +}; + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index 8fcb3aba..ad0831fc 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -298,6 +298,21 @@ bool SessionDeviceManager::ShouldCaptureCursor() const { }); } +bool SessionDeviceManager::HasConnectedController() const { + std::shared_lock lock(owner_.connection_status_mutex_); + return std::any_of(owner_.connection_status_.begin(), + owner_.connection_status_.end(), [](const auto& entry) { + return entry.second == ConnectionStatus::Connected; + }); +} + +void SessionDeviceManager::UpdateScreenSharingPower() { + // Wayland keeps capture warm after the last disconnect. Only live viewers + // justify keeping the host awake; an outgoing viewing session does not. + screen_sharing_power_.SetSharing(owner_.screen_capturer_is_started_ && + HasConnectedController()); +} + int SessionDeviceManager::StartScreenCapturer() { #ifdef __APPLE__ if (!owner_.EnsureMacScreenRecordingPermission()) { @@ -317,8 +332,10 @@ int SessionDeviceManager::StartScreenCapturer() { cursor_capture_dirty_.store(false, std::memory_order_relaxed); const bool show_cursor = ShouldCaptureCursor(); LOG_INFO("Start screen capturer, show cursor: {}", show_cursor); + screen_sharing_power_.SetSharing(HasConnectedController()); const int ret = screen_capturer_->Start(show_cursor); if (ret != 0) { + screen_sharing_power_.SetSharing(false); LOG_ERROR("Start screen capturer failed: {}", ret); } return ret; @@ -329,6 +346,8 @@ int SessionDeviceManager::StopScreenCapturer() { LOG_INFO("Stop screen capturer"); screen_capturer_->Stop(); } + owner_.screen_capturer_is_started_ = false; + screen_sharing_power_.SetSharing(false); return 0; } @@ -647,6 +666,8 @@ void SessionDeviceManager::PushAudio(const std::string& remote_id, } void SessionDeviceManager::UpdateInteractions() { + // Release even when pending remote-input cleanup makes this tick return early. + UpdateScreenSharingPower(); #ifdef _WIN32 if (owner_.screen_capturer_is_started_ && cursor_capture_dirty_.load(std::memory_order_relaxed)) { @@ -680,6 +701,8 @@ void SessionDeviceManager::UpdateInteractions() { owner_.screen_capturer_is_started_ = false; } + UpdateScreenSharingPower(); + if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) { if (StartMouseController() == 0) { owner_.mouse_controller_is_started_ = true; @@ -823,6 +846,8 @@ std::vector SessionDeviceManager::host_display_list() const { } void SessionDeviceManager::DestroyDevices() { + owner_.screen_capturer_is_started_ = false; + screen_sharing_power_.SetSharing(false); speaker_capture_.Shutdown(); StopMouseController(true); if (screen_capturer_) { diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.h b/apps/desktop/src/gui/features/devices/session_device_manager.h index becf58e2..0f24efbb 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -23,6 +23,7 @@ #include "display_info.h" #include "features/input/captured_keyboard_queue.h" #include "features/input/remote_mouse_buttons.h" +#include "screen_sharing_power.h" #include "speaker_capture_controller.h" struct SDL_AudioStream; @@ -105,6 +106,8 @@ private: bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, int fps); bool ShouldCaptureCursor() const; + bool HasConnectedController() const; + void UpdateScreenSharingPower(); bool SyncDisplayInfo(); void SetDisplayInfoList(std::vector displays); void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps, @@ -115,6 +118,7 @@ private: std::unordered_map output_streams_; ScreenCapturerFactory *screen_capturer_factory_ = nullptr; ScreenCapturer *screen_capturer_ = nullptr; + ScreenSharingPower screen_sharing_power_; std::atomic cursor_capture_dirty_{true}; SpeakerCaptureController speaker_capture_; DeviceControllerFactory *device_controller_factory_ = nullptr; diff --git a/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp b/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp index 58d8d53c..a4088f7d 100644 --- a/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp +++ b/apps/desktop/src/platform/windows/gui/slint_renderer_probe.cpp @@ -11,6 +11,8 @@ namespace crossdesk { int RunSlintRendererProbe() { SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + // Renderer probing should not inhibit automatic display sleep either. + SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); if (!SDL_Init(SDL_INIT_VIDEO)) { return 1; } diff --git a/apps/desktop/xmake/platform.lua b/apps/desktop/xmake/platform.lua index ee75a943..ffd51dc7 100644 --- a/apps/desktop/xmake/platform.lua +++ b/apps/desktop/xmake/platform.lua @@ -57,6 +57,16 @@ function setup_platform_settings() "/usr/local/include/freetype2" }, {system = true}) + -- Screen sharing power inhibitors also use D-Bus in X11-only builds. + add_links("dbus-1") + add_existing_include_dirs({ + "/usr/include/dbus-1.0", + "/usr/local/include/dbus-1.0" + }, {system = true}) + for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do + add_includedirs(include_dir, {system = true}) + end + if is_config("USE_DRM", true) then add_links("drm") add_defines("CROSSDESK_HAS_DRM=1") @@ -69,11 +79,8 @@ function setup_platform_settings() end if is_config("USE_WAYLAND", true) then - add_links("dbus-1") add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=1") add_existing_include_dirs({ - "/usr/include/dbus-1.0", - "/usr/local/include/dbus-1.0", "/usr/include/pipewire-0.3", "/usr/local/include/pipewire-0.3", "/usr/include/pipewire", @@ -85,9 +92,6 @@ function setup_platform_settings() "/usr/local/include/spa", "/opt/crossdesk-pipewire-sdk/include/spa-0.2" }, {system = true}) - for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do - add_includedirs(include_dir, {system = true}) - end else add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=0") end @@ -98,7 +102,7 @@ function setup_platform_settings() add_ldflags("-Wl,-ld_classic") end add_cxflags("-Wno-unused-variable") - add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore", + add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore", "IOKit", "IOSurface", "ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox")