[feat] remember remote display resolutions across connections

This commit is contained in:
dijunkun
2026-11-11 14:14:00 +08:00
parent 17b4db25fb
commit a862bf6b35
15 changed files with 414 additions and 67 deletions
@@ -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
+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;