diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index aaef6084..000f182d 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -1294,6 +1294,9 @@ void GuiApplication::InitializeSettings() { LOG_ERROR("Failed to clear saved remote previews"); } settings_.LoadRecentConnectionAliases(); + if (!remote_resolutions_.Load( + std::filesystem::path(cache_path_) / "remote_display_resolutions.json")) + LOG_WARN("Could not load remembered remote display resolutions"); localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_); localization_language_ = @@ -2411,6 +2414,17 @@ void GuiApplication::BindStreamCallbacks() { auto& state = props->display_resolution_; const DisplayResolutionChange selection{props->selected_display_, ui_->resolution_choices.display_key, ui_->resolution_choices.modes[index]}; + // Explicitly choosing the current mode also establishes a preference. + if (!state.pending && std::any_of(state.displays.begin(), state.displays.end(), + [&](const auto& display) { + return SupportsDisplayResolution(display, selection) && + display.current == selection.resolution; + })) { + if (!remote_resolutions_.Remember(props->resolution_preference_key_, + {selection.display_key, selection.resolution})) + LOG_WARN("Could not save remote display resolution"); + return; + } if (!state.Begin(selection, SDL_GetTicks())) return; action.resolution_change = state.requested; } 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 d2367b2c..ab928bac 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -253,6 +253,8 @@ int SessionDeviceManager::InitializeScreenCapturer() { uint64_t send_errors = 0; for (const std::string &remote_id : connected_remote_ids) { if (owner_.managed_host_frontend_ && remote_id.rfind("lan/", 0) != 0) continue; + // Warm/shared capture may already be running for another controller. + if (!owner_.DisplayResolutionsReadyForCapture(remote_id)) continue; if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time, stream_id)) continue; if (owner_.WithController( @@ -693,6 +695,9 @@ void SessionDeviceManager::PushAudio(const std::string& remote_id, } void SessionDeviceManager::UpdateInteractions() { + // Restore the previous session's modes and apply the new controller's saved + // selections before starting capture or allowing an early input return. + owner_.HandleDisplayResolutionChanges(); // Release even when pending remote-input cleanup makes this tick return early. UpdateScreenSharingPower(); #ifdef _WIN32 @@ -720,7 +725,8 @@ void SessionDeviceManager::UpdateInteractions() { if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) { const auto now = std::chrono::steady_clock::now(); - if (now >= next_capture_start_retry_) { + if (now >= next_capture_start_retry_ && + owner_.DisplayResolutionsReadyForCapture()) { next_capture_start_retry_ = now + std::chrono::seconds(1); if (StartScreenCapturer() == 0) { owner_.screen_capturer_is_started_ = true; @@ -799,7 +805,6 @@ void SessionDeviceManager::UpdateInteractions() { } owner_.keyboard_.CheckRemoteTimeouts(); - owner_.HandleDisplayResolutionChanges(); } void SessionDeviceManager::DrainCapturedKeyboardInput() { diff --git a/apps/desktop/src/gui/features/settings/remote_resolution_store.cpp b/apps/desktop/src/gui/features/settings/remote_resolution_store.cpp new file mode 100644 index 00000000..79bdd4fd --- /dev/null +++ b/apps/desktop/src/gui/features/settings/remote_resolution_store.cpp @@ -0,0 +1,77 @@ +#include "remote_resolution_store.h" + +#include + +#include + +#include "common/private_file.h" + +namespace crossdesk { +namespace { +constexpr std::uintmax_t kMaxStoreBytes = 8 * 1024 * 1024; +} + +bool RemoteResolutionStore::Load(const std::filesystem::path &path) { + std::lock_guard lock(mutex_); + path_ = path; + devices_.clear(); + try { + if (!std::filesystem::exists(path_)) return true; + if (std::filesystem::file_size(path_) > kMaxStoreBytes) return false; + std::ifstream input(path_, std::ios::binary); + const auto json = nlohmann::json::parse(input); + if (json.at("version") != 1 || !json.at("devices").is_object()) + return false; + for (const auto &[device, values] : json.at("devices").items()) { + std::vector preferences; + if (!device.empty() && ReadDisplayResolutionPreferences(values, preferences)) + devices_.emplace(device, std::move(preferences)); + } + return true; + } catch (const std::exception &) { + return false; + } +} + +std::vector +RemoteResolutionStore::Get(const std::string &device) const { + std::lock_guard lock(mutex_); + const auto found = devices_.find(device); + return found == devices_.end() ? std::vector{} + : found->second; +} + +bool RemoteResolutionStore::Remember( + const std::string &device, const DisplayResolutionPreference &preference) { + if (device.empty() || !ValidDisplayResolutionPreference(preference)) + return false; + std::lock_guard lock(mutex_); + if (path_.empty()) return false; + auto updated = devices_; + auto &preferences = updated[device]; + const auto found = std::find_if(preferences.begin(), preferences.end(), + [&](const auto &saved) { return saved.display_key == preference.display_key; }); + if (found != preferences.end()) { + if (found->resolution == preference.resolution) return true; + *found = preference; + } else { + if (preferences.size() >= kMaxResolutionDisplays) + preferences.erase(preferences.begin()); + preferences.push_back(preference); + } + try { + nlohmann::json json{{"version", 1}, {"devices", nlohmann::json::object()}}; + for (const auto &[id, saved] : updated) + json["devices"][id] = DisplayResolutionPreferencesJson(saved); + const auto text = json.dump(2); + if (text.size() > kMaxStoreBytes) return false; + private_file::EnsureDirectory(path_.parent_path()); + private_file::Write(path_, text); + devices_ = std::move(updated); + return true; + } catch (const std::exception &) { + return false; + } +} + +} // namespace crossdesk diff --git a/apps/desktop/src/gui/features/settings/remote_resolution_store.h b/apps/desktop/src/gui/features/settings/remote_resolution_store.h new file mode 100644 index 00000000..9d35eb4b --- /dev/null +++ b/apps/desktop/src/gui/features/settings/remote_resolution_store.h @@ -0,0 +1,33 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _REMOTE_RESOLUTION_STORE_H_ +#define _REMOTE_RESOLUTION_STORE_H_ + +#include +#include +#include + +#include + +namespace crossdesk { + +class RemoteResolutionStore { +public: + bool Load(const std::filesystem::path &path); + std::vector Get(const std::string &device) const; + bool Remember(const std::string &device, + const DisplayResolutionPreference &preference); + +private: + mutable std::mutex mutex_; + std::filesystem::path path_; + std::map> devices_; +}; + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 0fc1b44f..48624ec9 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -193,6 +193,10 @@ void GuiRuntime::HandleServerControllerDisconnected( if (auto events = ControllerEvents(remote_id)) transfers_.Cancel(events.get(), remote_id); preview_subscriptions_.Remove(remote_id); + { + std::lock_guard lock(display_resolution_commands_mutex_); + initial_display_resolutions_.erase(remote_id); + } bool has_connected_controller = false; std::string remaining_controller_id; diff --git a/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp b/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp index 823b6043..e5a672e0 100644 --- a/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp +++ b/apps/desktop/src/gui/runtime/display_resolution_runtime.cpp @@ -48,6 +48,71 @@ void GuiRuntime::QueueDisplayResolutionChange( display_resolution_commands_.push_back({id, events, change, epoch}); } +void GuiRuntime::BeginDisplayResolutionStartup(const std::string &id, + PeerEventHandler *source) { + const auto events = ControllerEvents(id); + if (events.get() != source) return; + std::lock_guard lock(display_resolution_commands_mutex_); + auto &initial = initial_display_resolutions_[id]; + if (initial.source.lock() != events) initial = {}; + initial.source = events; + if (!initial.state.connected) + initial.generation = ++next_display_resolution_startup_; + initial.state.Begin(SDL_GetTicks()); +} + +void GuiRuntime::ReceiveDisplayResolutionStartup( + const std::string &id, PeerEventHandler *source, + std::vector preferences) { + const auto events = ControllerEvents(id); + if (events.get() != source) return; + std::lock_guard lock(display_resolution_commands_mutex_); + auto &initial = initial_display_resolutions_[id]; + if (initial.source.lock() != events) initial = {}; + initial.source = events; + initial.state.Receive(std::move(preferences)); +} + +bool GuiRuntime::DisplayResolutionsReadyForCapture(const std::string &id) { + std::lock_guard lock(display_resolution_commands_mutex_); + const auto now = SDL_GetTicks(); + if (!id.empty()) { + const auto found = initial_display_resolutions_.find(id); + return found == initial_display_resolutions_.end() || found->second.state.Ready(now); + } + return std::all_of(initial_display_resolutions_.begin(), + initial_display_resolutions_.end(), [&](const auto &entry) { + return entry.second.state.Ready(now); + }); +} + +bool GuiRuntime::ApplyHostDisplayResolution(const DisplayResolutionChange &change) { + const auto available = QueryHostDisplayResolutions(); + const auto selected = std::find_if(available.begin(), available.end(), + [&](const auto &info) { return SupportsDisplayResolution(info, change); }); + // Unsupported or stale requests must never create a restoration record. + if (selected == available.end()) return false; + if (selected->current == change.resolution) return true; + const auto displays = devices_.display_info_list(); + std::lock_guard lock(native_display_resolution_mutex); +#ifdef _WIN32 + if (!IsCurrentSessionUserDesktopActive()) return false; + return display_resolution_restoration_.Apply(change.display_key, + [&] { return CaptureWindowsDisplayResolution(displays, change); }, + [&] { return ApplyWindowsDisplayResolution(displays, change); }); +#elif defined(__APPLE__) + return display_resolution_restoration_.Apply(change.display_key, + [&] { return CaptureMacDisplayResolution(displays, change); }, + [&] { return ApplyMacDisplayResolution(displays, change); }); +#elif defined(__linux__) && !defined(__OHOS__) + return display_resolution_restoration_.Apply(change.display_key, + [&] { return CaptureLinuxDisplayResolution(displays, change); }, + [&] { return ApplyLinuxDisplayResolution(displays, change); }); +#else + return false; +#endif +} + void GuiRuntime::RestoreHostDisplayResolutions(bool force) { const auto epoch = display_resolution_epoch_.load(); const bool disconnected = epoch != handled_display_resolution_epoch_; @@ -76,6 +141,57 @@ void GuiRuntime::RestoreHostDisplayResolutions(bool force) { void GuiRuntime::HandleDisplayResolutionChanges() { RestoreHostDisplayResolutions(); + std::vector> initial; + { + std::lock_guard lock(display_resolution_commands_mutex_); + for (auto &[id, startup] : initial_display_resolutions_) { + if (!startup.state.NeedsApply()) continue; + initial.emplace_back(id, startup); + } + } + for (const auto &[id, startup] : initial) { + const auto source = startup.source.lock(); + const auto active = [&] { + if (stopping_ || !source || !source->IsActive() || + source != ControllerEvents(id) || !IsAuthorizedController(id)) + return false; + std::lock_guard lock(display_resolution_commands_mutex_); + const auto found = initial_display_resolutions_.find(id); + return found != initial_display_resolutions_.end() && + found->second.generation == startup.generation && + found->second.source.lock() == source; + }; + // Connected and metadata callbacks can interleave with this tick. Keep + // early preferences pending until admission has actually been committed. + if (!active()) continue; + for (const auto &preference : startup.state.preferences) { + RestoreHostDisplayResolutions(); + if (!active()) break; + const auto available = QueryHostDisplayResolutions(); + const auto display = std::find_if(available.begin(), available.end(), + [&](const auto &info) { return info.display_key == preference.display_key; }); + if (display == available.end()) continue; + // Resolve a fresh slot from the native identity: hotplug can reorder the + // list, and another display must never inherit the remembered mode. + const DisplayResolutionChange change{display->display_id, + display->display_key, preference.resolution}; + if (!ApplyHostDisplayResolution(change)) + LOG_INFO("Remembered display resolution is unavailable; keeping current mode"); + } + RestoreHostDisplayResolutions(); + { + std::lock_guard lock(display_resolution_commands_mutex_); + const auto found = initial_display_resolutions_.find(id); + if (found != initial_display_resolutions_.end() && + found->second.source.lock() == source && + found->second.generation == startup.generation) { + found->second.state.applied = true; + found->second.state.preferences.clear(); + } + } + if (active() && !startup.state.preferences.empty()) + AnnounceHostCapabilities(id); + } std::deque commands; { std::lock_guard lock(display_resolution_commands_mutex_); @@ -92,62 +208,7 @@ void GuiRuntime::HandleDisplayResolutionChanges() { RemoteAction reply; reply.type = ControlType::display_resolution_status; reply.resolution_change = command.change; - reply.resolution_change.accepted = false; - const auto displays = devices_.display_info_list(); - const auto available = QueryHostDisplayResolutions(); - const auto selected = - std::find_if(available.begin(), available.end(), [&](const auto &info) { - return SupportsDisplayResolution(info, command.change); - }); - if (selected == available.end()) { - // Unsupported or stale requests must never create a restoration record. - } else if (selected->current == command.change.resolution) { - reply.resolution_change.accepted = true; - } else { -#ifdef _WIN32 - { - std::lock_guard lock(native_display_resolution_mutex); - if (IsCurrentSessionUserDesktopActive()) - reply.resolution_change.accepted = - display_resolution_restoration_.Apply( - command.change.display_key, - [&] { - return CaptureWindowsDisplayResolution(displays, - command.change); - }, - [&] { - return ApplyWindowsDisplayResolution(displays, - command.change); - }); - } -#elif defined(__APPLE__) - { - std::lock_guard lock(native_display_resolution_mutex); - reply.resolution_change.accepted = - display_resolution_restoration_.Apply( - command.change.display_key, - [&] { - return CaptureMacDisplayResolution(displays, command.change); - }, - [&] { - return ApplyMacDisplayResolution(displays, command.change); - }); - } -#elif defined(__linux__) && !defined(__OHOS__) - { - std::lock_guard lock(native_display_resolution_mutex); - reply.resolution_change.accepted = - display_resolution_restoration_.Apply( - command.change.display_key, - [&] { - return CaptureLinuxDisplayResolution(displays, command.change); - }, - [&] { - return ApplyLinuxDisplayResolution(displays, command.change); - }); - } -#endif - } + reply.resolution_change.accepted = ApplyHostDisplayResolution(command.change); reply.display_resolutions = QueryHostDisplayResolutions(); const auto message = reply.to_json(); SendControllerData(command.remote_id, message.data(), message.size(), diff --git a/apps/desktop/src/gui/runtime/display_resolution_state.h b/apps/desktop/src/gui/runtime/display_resolution_state.h index 822a870f..947a0bae 100644 --- a/apps/desktop/src/gui/runtime/display_resolution_state.h +++ b/apps/desktop/src/gui/runtime/display_resolution_state.h @@ -41,13 +41,13 @@ struct DisplayResolutionState { return false; } - void Complete(const DisplayResolutionChange &response, + bool Complete(const DisplayResolutionChange &response, std::vector actual) { if (response.request_id != requested.request_id || response.display_id != requested.display_id || response.display_key != requested.display_key || !(response.resolution == requested.resolution)) - return; + return false; displays = std::move(actual); pending = false; failed = !response.accepted; @@ -60,6 +60,7 @@ struct DisplayResolutionState { }); failed = found == displays.end(); } + return !failed; } void Expire(uint64_t now) { @@ -70,6 +71,35 @@ struct DisplayResolutionState { } }; +// Initial host information can arrive before Connected. Consume it only once +// per connection, and let legacy controllers without metadata start after a +// bounded wait. Late metadata can still apply the remembered selection. +struct DisplayResolutionStartup { + bool connected = false; + bool received = false; + bool applied = false; + uint64_t deadline = 0; + std::vector preferences; + + void Begin(uint64_t now) { + if (connected) return; + connected = true; + deadline = now + 2000; + } + + void Receive(std::vector values) { + if (received) return; + preferences = std::move(values); + received = true; + } + + bool NeedsApply() const { return connected && received && !applied; } + + bool Ready(uint64_t now) const { + return !connected || applied || (!received && now >= deadline); + } +}; + } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/gui/runtime/gui_runtime.h b/apps/desktop/src/gui/runtime/gui_runtime.h index 6db35648..dc5b5744 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.h +++ b/apps/desktop/src/gui/runtime/gui_runtime.h @@ -21,6 +21,7 @@ #include "features/input/keyboard_controller.h" #include "features/input/remote_input_dispatcher.h" #include "features/settings/settings_manager.h" +#include "features/settings/remote_resolution_store.h" #include "network/lan/lan_session_manager.h" #include "privacy_controller.h" #include "runtime/gui_state.h" @@ -159,6 +160,11 @@ class GuiRuntime : protected gui_detail::GuiState { const DisplayResolutionChange& change); void HandleDisplayResolutionChanges(); void RestoreHostDisplayResolutions(bool force = false); + bool ApplyHostDisplayResolution(const DisplayResolutionChange& change); + void BeginDisplayResolutionStartup(const std::string& id, PeerEventHandler* source); + void ReceiveDisplayResolutionStartup(const std::string& id, PeerEventHandler* source, + std::vector preferences); + bool DisplayResolutionsReadyForCapture(const std::string& id = {}); void CloseRemoteSession(std::shared_ptr props); void CloseAllRemoteSessions(); @@ -217,7 +223,8 @@ class GuiRuntime : protected gui_detail::GuiState { // Set before initializing callbacks; immutable for the process lifetime. bool unattended_host_ = false; bool managed_host_frontend_ = false; - std::string BuildHostInformation(bool advertise_trust = false); + std::string BuildHostInformation(bool advertise_trust = false, + const std::vector& preferences = {}); void AnnounceHostCapabilities(const std::string& id); #ifdef _WIN32 std::shared_ptr local_host_; @@ -262,6 +269,13 @@ class GuiRuntime : protected gui_detail::GuiState { }; std::mutex display_resolution_commands_mutex_; std::deque display_resolution_commands_; + struct InitialDisplayResolutions { + std::weak_ptr source; + DisplayResolutionStartup state; + uint64_t generation = 0; + }; + std::map initial_display_resolutions_; + uint64_t next_display_resolution_startup_ = 0; std::vector last_display_resolutions_; uint64_t next_display_resolution_poll_ = 0; // Incremented when the last incoming controller disconnects. Keep the @@ -277,6 +291,7 @@ class GuiRuntime : protected gui_detail::GuiState { SessionDeviceManager devices_; FileTransferManager transfers_; SettingsManager settings_; + RemoteResolutionStore remote_resolutions_; KeyboardController keyboard_; RemoteInputDispatcher remote_input_; std::shared_ptr peer_events_; diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index be82a954..c4969ae6 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -285,8 +285,12 @@ void PeerEventHandler::OnReceiveDataBuffer( props && source_id == props->control_data_label_ && !props->closing_ && props->connection_status_ == MiniRtcConnectionStatus::MiniRtcConnectionConnected) { std::lock_guard lock(props->display_resolution_mutex_); - props->display_resolution_.Complete(remote_action.resolution_change, - std::move(remote_action.display_resolutions)); + if (props->display_resolution_.Complete(remote_action.resolution_change, + std::move(remote_action.display_resolutions)) && + !runtime->remote_resolutions_.Remember(props->resolution_preference_key_, + {remote_action.resolution_change.display_key, + remote_action.resolution_change.resolution})) + LOG_WARN("Could not save remote display resolution"); } return; } @@ -552,6 +556,8 @@ void PeerEventHandler::OnReceiveDataBuffer( } LOG_DEBUG("Remote hostname: [{}]", host_name); if (source_id == runtime->control_data_label_) { + runtime->ReceiveDisplayResolutionStartup(remote_id, handler, + std::move(remote_action.preferred_display_resolutions)); runtime->SetControllerPrivacySupport( remote_id, remote_action.i.supports_privacy_screen); } diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index f74cd53c..662e6066 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -331,12 +331,14 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status, if (!props) { if (status == MiniRtcConnectionStatus::MiniRtcConnectionConnected) { if (!runtime->BeginControllerTrust(remote_id, handler)) return; + runtime->BeginDisplayResolutionStartup(remote_id, handler); } else { runtime->ForgetControllerTrust(remote_id, handler); std::lock_guard lock(runtime->display_resolution_commands_mutex_); std::erase_if(runtime->display_resolution_commands_, [&](const auto& request) { return request.remote_id == remote_id; }); + runtime->initial_display_resolutions_.erase(remote_id); } #ifdef _WIN32 if (runtime->unattended_host_) { @@ -370,7 +372,19 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status, props->cursor_presentation_ = {}; } { - const auto msg = runtime->BuildHostInformation(true); + // Snapshot the server namespace for this session. Direct connections + // use the authenticated device identity, not a random LAN session key. + const auto preference_key = props->lan_session_ + ? nlohmann::json::array({"lan", props->lan_session_->target.device_id}).dump() + : nlohmann::json::array({"signal", + runtime->config_center_->GetSignalServerHost(), + runtime->config_center_->GetSignalServerPort(), remote_id}).dump(); + { + std::lock_guard lock(props->display_resolution_mutex_); + props->resolution_preference_key_ = preference_key; + } + const auto msg = runtime->BuildHostInformation(true, + runtime->remote_resolutions_.Get(preference_key)); const int ret = MiniRtcSendReliableDataFrame(props->peer_, msg.data(), msg.size(), runtime->control_data_label_.c_str()); @@ -653,10 +667,12 @@ void PeerEventHandler::OnNetStatusReport( props->net_traffic_stats_.Store(*net_traffic_stats); } } -std::string GuiRuntime::BuildHostInformation(bool advertise_trust) { +std::string GuiRuntime::BuildHostInformation(bool advertise_trust, + const std::vector& preferences) { auto action = MakeHostInformation(GetHostName(), devices_.host_display_list(), UserSessionFeaturesAvailable(), CROSSDESK_VERSION, kClientPlatform); action.display_resolutions = QueryHostDisplayResolutions(); + action.preferred_display_resolutions = preferences; action.i.supports_video_settings = true; action.i.supports_video_codec_status = true; action.i.supports_text_input = devices_.SupportsTextInput(); diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index acae0985..31751d9c 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -129,6 +129,8 @@ struct RemoteSession { std::string clipboard_label_ = kClipboardStream; std::string local_id_; std::string remote_id_; + // Stable device/server identity; LAN's remote_id_ is a temporary session key. + std::string resolution_preference_key_; bool exit_ = false; bool signal_connected_ = false; MiniRtcSignalStatus signal_status_ = MiniRtcSignalStatus::MiniRtcSignalClosed; diff --git a/libs/wire/include/display_resolution.h b/libs/wire/include/display_resolution.h index 161a10b0..272039af 100644 --- a/libs/wire/include/display_resolution.h +++ b/libs/wire/include/display_resolution.h @@ -52,6 +52,20 @@ struct DisplayResolutionChange { bool accepted = false; }; +// Persist the native identity, never the display's position in a session. +struct DisplayResolutionPreference { + std::string display_key; + DisplayResolution resolution; +}; + +inline bool ValidDisplayResolutionPreference( + const DisplayResolutionPreference &preference) { + return !preference.display_key.empty() && + preference.display_key.size() <= 1024 && + preference.display_key.find('\0') == std::string::npos && + ValidDisplayResolution(preference.resolution); +} + inline bool SupportsDisplayResolution(const DisplayResolutionInfo &display, const DisplayResolutionChange &change) { return display.display_id == change.display_id && @@ -62,4 +76,4 @@ inline bool SupportsDisplayResolution(const DisplayResolutionInfo &display, } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/libs/wire/include/display_resolution_preferences.h b/libs/wire/include/display_resolution_preferences.h new file mode 100644 index 00000000..d601fc47 --- /dev/null +++ b/libs/wire/include/display_resolution_preferences.h @@ -0,0 +1,58 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-11-11 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _DISPLAY_RESOLUTION_PREFERENCES_H_ +#define _DISPLAY_RESOLUTION_PREFERENCES_H_ + +#include + +#include "display_resolution.h" + +namespace crossdesk { + +inline nlohmann::json DisplayResolutionPreferencesJson( + const std::vector &preferences) { + auto result = nlohmann::json::array(); + for (const auto &preference : preferences) + result.push_back({{"display_key", preference.display_key}, + {"width", preference.resolution.width}, + {"height", preference.resolution.height}}); + return result; +} + +inline bool ReadDisplayResolutionPreferences( + const nlohmann::json &values, + std::vector &result) { + std::vector parsed; + if (!values.is_array() || values.size() > kMaxResolutionDisplays) + return false; + try { + for (const auto &value : values) { + const auto &width = value.at("width"); + const auto &height = value.at("height"); + if (!width.is_number_integer() || !height.is_number_integer() || + width < 1 || width > 32768 || height < 1 || height > 32768) + return false; + DisplayResolutionPreference preference{ + value.at("display_key").get(), + {width.get(), height.get()}}; + if (!ValidDisplayResolutionPreference(preference) || + std::any_of(parsed.begin(), parsed.end(), [&](const auto &existing) { + return existing.display_key == preference.display_key; + })) + return false; + parsed.push_back(std::move(preference)); + } + } catch (const nlohmann::json::exception &) { + return false; + } + result = std::move(parsed); + return true; +} + +} // namespace crossdesk + +#endif diff --git a/libs/wire/include/remote_action.h b/libs/wire/include/remote_action.h index d8841f7b..ef303bc4 100644 --- a/libs/wire/include/remote_action.h +++ b/libs/wire/include/remote_action.h @@ -223,6 +223,8 @@ struct RemoteAction { // Optional host information extension, also returned with resolution status. std::vector display_resolutions; DisplayResolutionChange resolution_change; + // Controller preferences sent with its initial host information. + std::vector preferred_display_resolutions; std::string to_json() const; bool from_json(const std::string& json_string); diff --git a/libs/wire/src/remote_action.cpp b/libs/wire/src/remote_action.cpp index f5461c98..7256c485 100644 --- a/libs/wire/src/remote_action.cpp +++ b/libs/wire/src/remote_action.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -339,6 +340,9 @@ std::string RemoteAction::ToJson(const RemoteAction& action) { if (!action.display_resolutions.empty()) object["host_info"]["display_resolutions"] = DisplayResolutionsJson(action.display_resolutions); + if (!action.preferred_display_resolutions.empty()) + object["host_info"]["preferred_display_resolutions"] = + DisplayResolutionPreferencesJson(action.preferred_display_resolutions); break; } case ControlType::invalid: @@ -354,6 +358,7 @@ bool RemoteAction::FromJson(const std::string& json_string, try { const json object = json::parse(json_string); output.display_resolutions.clear(); + output.preferred_display_resolutions.clear(); output.type = static_cast(object.at("type").get()); switch (output.type) { case ControlType::display_resolution: @@ -549,6 +554,11 @@ bool RemoteAction::FromJson(const std::string& json_string, ResetHostInfo(output.i); owns_host_info = true; const auto& host_info_object = object.at("host_info"); + // A corrupt optional preference must not prevent a connection. + if (const auto preferences = host_info_object.find("preferred_display_resolutions"); + preferences != host_info_object.end()) + ReadDisplayResolutionPreferences(*preferences, + output.preferred_display_resolutions); if (const auto modes = host_info_object.find("display_resolutions"); modes != host_info_object.end() && !ReadDisplayResolutions(*modes, output.display_resolutions)) return false;