mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-11 19:07:33 +08:00
[feat] remember remote display resolutions across connections
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "remote_resolution_store.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <display_resolution_preferences.h>
|
||||
|
||||
#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<DisplayResolutionPreference> preferences;
|
||||
if (!device.empty() && ReadDisplayResolutionPreferences(values, preferences))
|
||||
devices_.emplace(device, std::move(preferences));
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<DisplayResolutionPreference>
|
||||
RemoteResolutionStore::Get(const std::string &device) const {
|
||||
std::lock_guard lock(mutex_);
|
||||
const auto found = devices_.find(device);
|
||||
return found == devices_.end() ? std::vector<DisplayResolutionPreference>{}
|
||||
: 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
|
||||
@@ -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 <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include <display_resolution.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class RemoteResolutionStore {
|
||||
public:
|
||||
bool Load(const std::filesystem::path &path);
|
||||
std::vector<DisplayResolutionPreference> 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<std::string, std::vector<DisplayResolutionPreference>> devices_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
|
||||
@@ -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<DisplayResolutionPreference> 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<std::pair<std::string, InitialDisplayResolutions>> 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<PendingDisplayResolution> 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(),
|
||||
|
||||
@@ -41,13 +41,13 @@ struct DisplayResolutionState {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Complete(const DisplayResolutionChange &response,
|
||||
bool Complete(const DisplayResolutionChange &response,
|
||||
std::vector<DisplayResolutionInfo> 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<DisplayResolutionPreference> preferences;
|
||||
|
||||
void Begin(uint64_t now) {
|
||||
if (connected) return;
|
||||
connected = true;
|
||||
deadline = now + 2000;
|
||||
}
|
||||
|
||||
void Receive(std::vector<DisplayResolutionPreference> 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
|
||||
#endif
|
||||
|
||||
@@ -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<DisplayResolutionPreference> preferences);
|
||||
bool DisplayResolutionsReadyForCapture(const std::string& id = {});
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> 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<DisplayResolutionPreference>& preferences = {});
|
||||
void AnnounceHostCapabilities(const std::string& id);
|
||||
#ifdef _WIN32
|
||||
std::shared_ptr<LocalHostChannel> local_host_;
|
||||
@@ -262,6 +269,13 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
};
|
||||
std::mutex display_resolution_commands_mutex_;
|
||||
std::deque<PendingDisplayResolution> display_resolution_commands_;
|
||||
struct InitialDisplayResolutions {
|
||||
std::weak_ptr<PeerEventHandler> source;
|
||||
DisplayResolutionStartup state;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
std::map<std::string, InitialDisplayResolutions> initial_display_resolutions_;
|
||||
uint64_t next_display_resolution_startup_ = 0;
|
||||
std::vector<DisplayResolutionInfo> 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<PeerEventHandler> peer_events_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<DisplayResolutionPreference>& 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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
#endif
|
||||
|
||||
@@ -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 <nlohmann/json.hpp>
|
||||
|
||||
#include "display_resolution.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline nlohmann::json DisplayResolutionPreferencesJson(
|
||||
const std::vector<DisplayResolutionPreference> &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<DisplayResolutionPreference> &result) {
|
||||
std::vector<DisplayResolutionPreference> 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<std::string>(),
|
||||
{width.get<int>(), height.get<int>()}};
|
||||
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
|
||||
@@ -223,6 +223,8 @@ struct RemoteAction {
|
||||
// Optional host information extension, also returned with resolution status.
|
||||
std::vector<DisplayResolutionInfo> display_resolutions;
|
||||
DisplayResolutionChange resolution_change;
|
||||
// Controller preferences sent with its initial host information.
|
||||
std::vector<DisplayResolutionPreference> preferred_display_resolutions;
|
||||
|
||||
std::string to_json() const;
|
||||
bool from_json(const std::string& json_string);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <remote_action.h>
|
||||
#include <display_resolution_preferences.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
@@ -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<ControlType>(object.at("type").get<int>());
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user