[feat] add remote display resolution selection to video settings

This commit is contained in:
dijunkun committed 2026-11-11 05:30:25 +08:00
1 parent 14fa95beeb
commit a3159b198e
15 files changed
+740 -43

No files matched your search

@@ -0,0 +1,89 @@
#include "runtime/gui_runtime.h"
#include <SDL3/SDL_timer.h>
#ifdef _WIN32
#include "interactive_desktop.h"
#include "platform/windows/screen_capturer/display_resolution_win.h"
#endif
namespace crossdesk {
namespace {
// EnumDisplaySettings caches its enumeration. Serialize queries on transport
// threads with mode changes on the application thread.
std::mutex native_display_resolution_mutex;
} // namespace
std::vector<DisplayResolutionInfo> GuiRuntime::QueryHostDisplayResolutions() {
#ifdef _WIN32
std::lock_guard lock(native_display_resolution_mutex);
return QueryWindowsDisplayResolutions(devices_.display_info_list());
#else
return {};
#endif
}
void GuiRuntime::QueueDisplayResolutionChange(
const std::string &id, PeerEventHandler *source,
const DisplayResolutionChange &change) {
const auto events = ControllerEvents(id);
if (events.get() != source || !IsAuthorizedController(id))
return;
std::lock_guard lock(display_resolution_commands_mutex_);
if (display_resolution_commands_.size() >= 32)
return;
display_resolution_commands_.push_back({id, events, change});
}
void GuiRuntime::HandleDisplayResolutionChanges() {
std::deque<PendingDisplayResolution> commands;
{
std::lock_guard lock(display_resolution_commands_mutex_);
commands.swap(display_resolution_commands_);
}
for (const auto &command : commands) {
const auto source = command.source.lock();
if (!source || !source->IsActive() ||
source != ControllerEvents(command.remote_id) ||
!IsAuthorizedController(command.remote_id))
continue;
RemoteAction reply;
reply.type = ControlType::display_resolution_status;
reply.resolution_change = command.change;
reply.resolution_change.accepted = false;
#ifdef _WIN32
{
std::lock_guard lock(native_display_resolution_mutex);
if (IsCurrentSessionUserDesktopActive())
reply.resolution_change.accepted = ApplyWindowsDisplayResolution(
devices_.display_info_list(), command.change);
}
#endif
reply.display_resolutions = QueryHostDisplayResolutions();
const auto message = reply.to_json();
SendControllerData(command.remote_id, message.data(), message.size(),
control_data_label_.c_str());
next_display_resolution_poll_ = 0;
}
const auto now = SDL_GetTicks();
if (now < next_display_resolution_poll_)
return;
next_display_resolution_poll_ = now + 2000;
const auto controllers = ConnectedControllers();
if (controllers.empty())
return;
auto current = QueryHostDisplayResolutions();
if (current == last_display_resolutions_)
return;
last_display_resolutions_ = std::move(current);
// Re-advertise after local changes/hotplug as well as remote requests. The
// optional host-info extension is harmless to pre-feature controllers.
const auto message = BuildHostInformation();
for (const auto &id : controllers) {
if (IsAuthorizedController(id))
SendControllerData(id, message.data(), message.size(),
control_data_label_.c_str());
}
}
} // namespace crossdesk
@@ -0,0 +1,75 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-11
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISPLAY_RESOLUTION_STATE_H_
#define _DISPLAY_RESOLUTION_STATE_H_
#include <display_resolution.h>
namespace crossdesk {
// Guarded by RemoteSession::display_resolution_mutex_. The current selection
// always comes from the host, never from an unacknowledged request.
struct DisplayResolutionState {
std::vector<DisplayResolutionInfo> displays;
DisplayResolutionChange requested;
bool pending = false;
bool failed = false;
uint64_t request_tick = 0;
bool Begin(const DisplayResolutionChange &selection, uint64_t now) {
if (pending)
return false;
for (const auto &display : displays) {
if (!SupportsDisplayResolution(display, selection))
continue;
if (display.current == selection.resolution)
return false;
uint32_t next = requested.request_id + 1;
if (next == 0)
next = 1;
requested = {display.display_id, display.display_key,
selection.resolution, next, false};
pending = true;
failed = false;
request_tick = now;
return true;
}
return false;
}
void 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;
displays = std::move(actual);
pending = false;
failed = !response.accepted;
if (response.accepted) {
const auto found = std::find_if(
displays.begin(), displays.end(), [&](const auto &display) {
return display.display_id == response.display_id &&
display.display_key == response.display_key &&
display.current == response.resolution;
});
failed = found == displays.end();
}
}
void Expire(uint64_t now) {
if (pending && now - request_tick >= 10000) {
pending = false;
failed = true;
}
}
};
} // namespace crossdesk
#endif
@@ -152,6 +152,11 @@ class GuiRuntime : protected gui_detail::GuiState {
// any connected controller lacks support, or on for the first automatic one.
void ApplyPrivacyAdmission(bool automatic_enable);
void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
std::vector<DisplayResolutionInfo> QueryHostDisplayResolutions();
void QueueDisplayResolutionChange(const std::string& id,
PeerEventHandler* source,
const DisplayResolutionChange& change);
void HandleDisplayResolutionChanges();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
@@ -247,6 +252,15 @@ class GuiRuntime : protected gui_detail::GuiState {
DisconnectLockPolicy disconnect_lock_;
std::mutex privacy_commands_mutex_;
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
struct PendingDisplayResolution {
std::string remote_id;
std::weak_ptr<PeerEventHandler> source;
DisplayResolutionChange change;
};
std::mutex display_resolution_commands_mutex_;
std::deque<PendingDisplayResolution> display_resolution_commands_;
std::vector<DisplayResolutionInfo> last_display_resolutions_;
uint64_t next_display_resolution_poll_ = 0;
bool privacy_settings_changed_ = false;
uint32_t last_privacy_revision_ = 0;
uint64_t last_privacy_status_tick_ = 0;
@@ -280,6 +280,21 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
}
if (remote_action.type == ControlType::display_resolution_status) {
if (auto props = runtime->FindRemoteSession(remote_id);
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));
}
return;
}
if (remote_action.type == ControlType::display_resolution) {
if (source_id == runtime->control_data_label_ || source_id == kWebControlStream)
runtime->QueueDisplayResolutionChange(remote_id, handler, remote_action.resolution_change);
return;
}
if (remote_action.type == ControlType::video_codec_status) {
if (auto props = runtime->FindRemoteSession(remote_id);
props && source_id == props->control_data_label_ &&
@@ -460,6 +475,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
if (is_client_mode) {
if (props && source_id == props->control_data_label_) {
std::lock_guard lock(props->display_resolution_mutex_);
props->display_resolution_.displays = std::move(remote_action.display_resolutions);
}
if (props && source_id == props->control_data_label_) {
std::lock_guard lock(props->remote_version_mutex_);
props->remote_host_info_received_ = true;
@@ -502,7 +521,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_DEBUG("Remote hostname: [{}]", props->remote_host_name_);
}
// Preserve legacy hosts' initial display list on their original data
// channel; live topology updates use the reliable control channel.
if (source_id == props->control_data_label_ || props->display_info_list_.empty()) {
props->display_info_list_.clear();
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
@@ -333,6 +333,10 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
if (!runtime->BeginControllerTrust(remote_id, handler)) return;
} 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;
});
}
#ifdef _WIN32
if (runtime->unattended_host_) {
@@ -422,6 +426,10 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
props->video_settings_pending_ = false;
props->video_settings_failed_ = false;
}
{
std::lock_guard lock(props->display_resolution_mutex_);
props->display_resolution_ = {};
}
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->remote_cursor_state_ = {};
@@ -648,6 +656,7 @@ void PeerEventHandler::OnNetStatusReport(
std::string GuiRuntime::BuildHostInformation(bool advertise_trust) {
auto action = MakeHostInformation(GetHostName(), devices_.host_display_list(),
UserSessionFeaturesAvailable(), CROSSDESK_VERSION, kClientPlatform);
action.display_resolutions = QueryHostDisplayResolutions();
action.i.supports_video_settings = true;
action.i.supports_video_codec_status = true;
action.i.supports_text_input = devices_.SupportsTextInput();
@@ -24,6 +24,7 @@
#include <stream_names.h>
#include "display_info.h"
#include "features/gallery/preview_resolution.h"
#include "runtime/display_resolution_state.h"
#include "cursor_presentation.h"
#include "minirtc.h"
#include "rendering/video_latency.h"
@@ -203,6 +204,8 @@ struct RemoteSession {
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
std::mutex display_resolution_mutex_;
DisplayResolutionState display_resolution_;
// Cursor snapshots arrive on the transport callback thread and are applied
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
// and shape cannot briefly come from different protocol messages.