[feat] remember remote display resolutions across connections

This commit is contained in:
dijunkun committed 2026-11-11 14:14:00 +08:00
1 parent 17b4db25fb
commit a862bf6b35
15 files changed
+414 -67

No files matched your search

@@ -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
+16 -1
View File
@@ -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;