mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-12 03:28:20 +08:00
[feat] add macOS and mobile resolution controls with disconnect restore
This commit is contained in:
1 parent
a3159b198e
commit
34b59de15d
50 files changed
+1086
-36
No files matched your search
@@ -220,6 +220,7 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
||||
windows_rdp_handoff_connected_->store(has_connected_controller,
|
||||
std::memory_order_relaxed);
|
||||
#endif
|
||||
if (!has_connected_controller) ++display_resolution_epoch_;
|
||||
}
|
||||
clipboard_.ForgetWeb(remote_id);
|
||||
if (has_connected_controller) {
|
||||
@@ -269,6 +270,9 @@ void GuiRuntime::HandleScreenLock() {
|
||||
std::unique_lock lock(connection_status_mutex_);
|
||||
if (!disconnect_lock_.TakeRequest(config_center_->IsLockOnDisconnect()))
|
||||
return;
|
||||
// Restore while the interactive desktop is still available. This forced
|
||||
// path does not reacquire connection_status_mutex_.
|
||||
RestoreHostDisplayResolutions(true);
|
||||
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
|
||||
if (LockScreen()) LOG_INFO("Requested screen lock after last controller disconnected");
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DISPLAY_RESOLUTION_RESTORATION_H_
|
||||
#define _DISPLAY_RESOLUTION_RESTORATION_H_
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <display_resolution.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Application-thread state. Each callback owns the original native display
|
||||
// mode, so restoration does not depend on capture still running or slot order.
|
||||
class DisplayResolutionRestoration {
|
||||
public:
|
||||
using Restore = std::function<bool()>;
|
||||
|
||||
template <typename Capture, typename Change>
|
||||
bool Apply(const std::string &key, Capture capture, Change change) {
|
||||
if (originals_.count(key))
|
||||
return change();
|
||||
if (originals_.size() >= kMaxResolutionDisplays)
|
||||
return false;
|
||||
auto restore = capture();
|
||||
if (!restore)
|
||||
return false;
|
||||
// Save before changing anything, including a failed change whose native
|
||||
// rollback might fail. Subsequent requests never replace this baseline.
|
||||
originals_.emplace(key, std::move(restore));
|
||||
const bool accepted = change();
|
||||
if (!accepted && originals_.at(key)())
|
||||
originals_.erase(key);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
bool RestoreAll() {
|
||||
for (auto it = originals_.begin(); it != originals_.end();) {
|
||||
if (it->second())
|
||||
it = originals_.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
return originals_.empty();
|
||||
}
|
||||
|
||||
bool Empty() const { return originals_.empty(); }
|
||||
|
||||
private:
|
||||
std::map<std::string, Restore> originals_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include <SDL3/SDL_timer.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "interactive_desktop.h"
|
||||
#include "platform/windows/screen_capturer/display_resolution_win.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/macos/screen_capturer/display_resolution_mac.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
// EnumDisplaySettings caches its enumeration. Serialize queries on transport
|
||||
// threads with mode changes on the application thread.
|
||||
// Serialize native queries on transport threads with mode changes on the
|
||||
// application thread (EnumDisplaySettings also caches its enumeration).
|
||||
std::mutex native_display_resolution_mutex;
|
||||
} // namespace
|
||||
|
||||
@@ -18,6 +22,9 @@ std::vector<DisplayResolutionInfo> GuiRuntime::QueryHostDisplayResolutions() {
|
||||
#ifdef _WIN32
|
||||
std::lock_guard lock(native_display_resolution_mutex);
|
||||
return QueryWindowsDisplayResolutions(devices_.display_info_list());
|
||||
#elif defined(__APPLE__)
|
||||
std::lock_guard lock(native_display_resolution_mutex);
|
||||
return QueryMacDisplayResolutions(devices_.display_info_list());
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
@@ -26,24 +33,54 @@ std::vector<DisplayResolutionInfo> GuiRuntime::QueryHostDisplayResolutions() {
|
||||
void GuiRuntime::QueueDisplayResolutionChange(
|
||||
const std::string &id, PeerEventHandler *source,
|
||||
const DisplayResolutionChange &change) {
|
||||
const auto epoch = display_resolution_epoch_.load();
|
||||
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});
|
||||
display_resolution_commands_.push_back({id, events, change, epoch});
|
||||
}
|
||||
|
||||
void GuiRuntime::RestoreHostDisplayResolutions(bool force) {
|
||||
const auto epoch = display_resolution_epoch_.load();
|
||||
const bool disconnected = epoch != handled_display_resolution_epoch_;
|
||||
handled_display_resolution_epoch_ = epoch;
|
||||
if (display_resolution_restoration_.Empty())
|
||||
return;
|
||||
const auto now = SDL_GetTicks();
|
||||
// A disconnect boundary must survive a rapid reconnect. Otherwise retry
|
||||
// failed restoration only while idle, without disrupting an active session.
|
||||
if (!disconnected && !force &&
|
||||
(now < next_display_resolution_restore_ ||
|
||||
!ConnectedControllers().empty()))
|
||||
return;
|
||||
next_display_resolution_restore_ = now + 2000;
|
||||
std::lock_guard lock(native_display_resolution_mutex);
|
||||
#ifdef _WIN32
|
||||
if (!IsCurrentSessionUserDesktopActive())
|
||||
return;
|
||||
#endif
|
||||
if (display_resolution_restoration_.RestoreAll())
|
||||
LOG_INFO("Restored original display modes after remote control");
|
||||
else if (disconnected || force)
|
||||
LOG_WARN("Could not restore every display mode; will retry while idle");
|
||||
next_display_resolution_poll_ = 0;
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleDisplayResolutionChanges() {
|
||||
RestoreHostDisplayResolutions();
|
||||
std::deque<PendingDisplayResolution> commands;
|
||||
{
|
||||
std::lock_guard lock(display_resolution_commands_mutex_);
|
||||
commands.swap(display_resolution_commands_);
|
||||
}
|
||||
for (const auto &command : commands) {
|
||||
RestoreHostDisplayResolutions();
|
||||
const auto source = command.source.lock();
|
||||
if (!source || !source->IsActive() ||
|
||||
if (stopping_ || command.epoch != display_resolution_epoch_.load() ||
|
||||
!source || !source->IsActive() ||
|
||||
source != ControllerEvents(command.remote_id) ||
|
||||
!IsAuthorizedController(command.remote_id))
|
||||
continue;
|
||||
@@ -51,20 +88,57 @@ void GuiRuntime::HandleDisplayResolutionChanges() {
|
||||
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 = ApplyWindowsDisplayResolution(
|
||||
devices_.display_info_list(), command.change);
|
||||
}
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
#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;
|
||||
}
|
||||
// The transport can disconnect while a synchronous native mode switch is
|
||||
// in progress. Restore that change on this tick as well.
|
||||
RestoreHostDisplayResolutions();
|
||||
const auto now = SDL_GetTicks();
|
||||
if (now < next_display_resolution_poll_)
|
||||
return;
|
||||
|
||||
@@ -82,6 +82,7 @@ GuiRuntime::~GuiRuntime() {
|
||||
lan_.reset();
|
||||
transfers_.Shutdown();
|
||||
WaitForSessionCleanup();
|
||||
RestoreHostDisplayResolutions(true);
|
||||
}
|
||||
|
||||
void GuiRuntime::SendRemoteInput(const RemoteAction& action) {
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "privacy_controller.h"
|
||||
#include "runtime/gui_state.h"
|
||||
#include "runtime/disconnect_lock_policy.h"
|
||||
#include "runtime/display_resolution_restoration.h"
|
||||
#include "runtime/announcement_inbox.h"
|
||||
#include "runtime/peer_event_handler.h"
|
||||
#include "runtime/peer_transport.h"
|
||||
@@ -157,6 +158,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
PeerEventHandler* source,
|
||||
const DisplayResolutionChange& change);
|
||||
void HandleDisplayResolutionChanges();
|
||||
void RestoreHostDisplayResolutions(bool force = false);
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
@@ -256,11 +258,18 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
std::string remote_id;
|
||||
std::weak_ptr<PeerEventHandler> source;
|
||||
DisplayResolutionChange change;
|
||||
uint64_t epoch;
|
||||
};
|
||||
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;
|
||||
// Incremented when the last incoming controller disconnects. Keep the
|
||||
// boundary even if a new connection arrives before the application tick.
|
||||
std::atomic<uint64_t> display_resolution_epoch_{0};
|
||||
uint64_t handled_display_resolution_epoch_ = 0;
|
||||
uint64_t next_display_resolution_restore_ = 0;
|
||||
DisplayResolutionRestoration display_resolution_restoration_;
|
||||
bool privacy_settings_changed_ = false;
|
||||
uint32_t last_privacy_revision_ = 0;
|
||||
uint64_t last_privacy_status_tick_ = 0;
|
||||
|
||||
@@ -218,6 +218,7 @@ void GuiRuntime::BeginShutdown() {
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
if (lan_) lan_->Stop();
|
||||
CloseAllRemoteSessions();
|
||||
RestoreHostDisplayResolutions(true);
|
||||
}
|
||||
bool GuiRuntime::ShutdownDrained() {
|
||||
TickLan();
|
||||
@@ -225,6 +226,7 @@ bool GuiRuntime::ShutdownDrained() {
|
||||
const bool drained =
|
||||
(!lan_ || lan_->Quiescent()) && !connection_peer_cleanup_.valid() &&
|
||||
session_cleanup_tasks_.empty() && controller_cleanup_tasks_.empty();
|
||||
if (drained) RestoreHostDisplayResolutions(true);
|
||||
if (!drained && std::chrono::steady_clock::now() >= shutdown_report_at_) {
|
||||
LOG_WARN(
|
||||
"Shutdown waiting: LAN [{}], cloud peer={}, viewers={}, controllers={}",
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DISPLAY_RESOLUTION_MAC_H_
|
||||
#define _DISPLAY_RESOLUTION_MAC_H_
|
||||
|
||||
#include <ColorSync/ColorSync.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <IOKit/graphics/IOGraphicsTypes.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
|
||||
#include "display_info.h"
|
||||
#include <display_resolution.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline DisplayResolution MacDisplayResolution(CGDisplayModeRef mode) {
|
||||
if (!mode)
|
||||
return {};
|
||||
// Desktop dimensions, matching macOS scaled resolutions. Pixel dimensions
|
||||
// can be twice these values on Retina displays and belong to capture only.
|
||||
const auto width = CGDisplayModeGetWidth(mode);
|
||||
const auto height = CGDisplayModeGetHeight(mode);
|
||||
if (width > 32768 || height > 32768)
|
||||
return {};
|
||||
return {int(width), int(height)};
|
||||
}
|
||||
|
||||
inline bool ResolveMacResolutionDisplay(const DisplayInfo &display,
|
||||
CGDirectDisplayID &id,
|
||||
std::string &key) {
|
||||
id = static_cast<CGDirectDisplayID>(
|
||||
reinterpret_cast<uintptr_t>(display.handle));
|
||||
if (!id || !CGDisplayIsOnline(id) || !CGDisplayIsActive(id))
|
||||
return false;
|
||||
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(id);
|
||||
if (!uuid)
|
||||
return false;
|
||||
CFStringRef uuid_string = CFUUIDCreateString(nullptr, uuid);
|
||||
CFRelease(uuid);
|
||||
if (!uuid_string)
|
||||
return false;
|
||||
char value[128]{};
|
||||
const bool valid = CFStringGetCString(uuid_string, value, sizeof(value),
|
||||
kCFStringEncodingUTF8);
|
||||
CFRelease(uuid_string);
|
||||
if (!valid)
|
||||
return false;
|
||||
key = std::to_string(id) + "|" + value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The array owns the borrowed mode references returned by Choices().
|
||||
class MacResolutionModes {
|
||||
public:
|
||||
explicit MacResolutionModes(CGDirectDisplayID display)
|
||||
: current(CGDisplayCopyDisplayMode(display)),
|
||||
modes_(CGDisplayCopyAllDisplayModes(display, nullptr)) {}
|
||||
~MacResolutionModes() {
|
||||
if (modes_)
|
||||
CFRelease(modes_);
|
||||
if (current)
|
||||
CGDisplayModeRelease(current);
|
||||
}
|
||||
MacResolutionModes(const MacResolutionModes &) = delete;
|
||||
MacResolutionModes &operator=(const MacResolutionModes &) = delete;
|
||||
|
||||
std::vector<CGDisplayModeRef> Choices() const {
|
||||
std::vector<CGDisplayModeRef> result;
|
||||
if (!current || !modes_ ||
|
||||
!ValidDisplayResolution(MacDisplayResolution(current)))
|
||||
return result;
|
||||
const double scale = double(CGDisplayModeGetPixelWidth(current)) /
|
||||
CGDisplayModeGetWidth(current);
|
||||
const double refresh = CGDisplayModeGetRefreshRate(current);
|
||||
const auto rank = [&](CGDisplayModeRef mode) {
|
||||
return std::make_tuple(
|
||||
std::abs(double(CGDisplayModeGetPixelWidth(mode)) /
|
||||
CGDisplayModeGetWidth(mode) -
|
||||
scale),
|
||||
std::abs(CGDisplayModeGetRefreshRate(mode) - refresh),
|
||||
CGDisplayModeGetIODisplayModeID(mode) !=
|
||||
CGDisplayModeGetIODisplayModeID(current));
|
||||
};
|
||||
std::map<std::pair<int, int>, CGDisplayModeRef> unique;
|
||||
for (CFIndex index = 0; index < CFArrayGetCount(modes_); ++index) {
|
||||
auto mode = static_cast<CGDisplayModeRef>(
|
||||
const_cast<void *>(CFArrayGetValueAtIndex(modes_, index)));
|
||||
const auto size = MacDisplayResolution(mode);
|
||||
const auto flags = CGDisplayModeGetIOFlags(mode);
|
||||
if (!ValidDisplayResolution(size) ||
|
||||
!CGDisplayModeIsUsableForDesktopGUI(mode) ||
|
||||
(flags & (kDisplayModeInterlacedFlag | kDisplayModeStretchedFlag)))
|
||||
continue;
|
||||
const auto key = std::make_pair(size.width, size.height);
|
||||
auto found = unique.find(key);
|
||||
// Prefer the current Retina density and then the closest refresh rate.
|
||||
if (found == unique.end() || rank(mode) < rank(found->second))
|
||||
unique[key] = mode;
|
||||
}
|
||||
for (auto it = unique.rbegin(); it != unique.rend(); ++it) {
|
||||
result.push_back(it->second);
|
||||
if (result.size() == kMaxDisplayResolutionModes)
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CGDisplayModeRef current;
|
||||
|
||||
private:
|
||||
CFArrayRef modes_;
|
||||
};
|
||||
|
||||
inline std::vector<DisplayResolutionInfo>
|
||||
QueryMacDisplayResolutions(const std::vector<DisplayInfo> &displays) {
|
||||
std::vector<DisplayResolutionInfo> result;
|
||||
for (int index = 0;
|
||||
index < int(displays.size()) && index < kMaxResolutionDisplays;
|
||||
++index) {
|
||||
DisplayResolutionInfo info;
|
||||
info.display_id = index;
|
||||
CGDirectDisplayID id = 0;
|
||||
if (!ResolveMacResolutionDisplay(displays[index], id, info.display_key))
|
||||
continue;
|
||||
MacResolutionModes modes(id);
|
||||
info.current = MacDisplayResolution(modes.current);
|
||||
if (!ValidDisplayResolution(info.current))
|
||||
continue;
|
||||
for (auto mode : modes.Choices())
|
||||
info.modes.push_back(MacDisplayResolution(mode));
|
||||
result.push_back(std::move(info));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
inline bool ApplyMacDisplayResolution(const std::vector<DisplayInfo> &displays,
|
||||
const DisplayResolutionChange &change) {
|
||||
if (change.display_id < 0 || change.display_id >= int(displays.size()) ||
|
||||
!ValidDisplayResolution(change.resolution))
|
||||
return false;
|
||||
CGDirectDisplayID id = 0;
|
||||
std::string key;
|
||||
if (!ResolveMacResolutionDisplay(displays[change.display_id], id, key) ||
|
||||
key != change.display_key)
|
||||
return false;
|
||||
MacResolutionModes modes(id);
|
||||
for (auto mode : modes.Choices()) {
|
||||
if (!(MacDisplayResolution(mode) == change.resolution))
|
||||
continue;
|
||||
if (MacDisplayResolution(modes.current) == change.resolution)
|
||||
return true;
|
||||
// A synchronous, application-lifetime change; never persist remote choices
|
||||
// in the user's permanent display configuration.
|
||||
if (CGDisplaySetDisplayMode(id, mode, nullptr) != kCGErrorSuccess)
|
||||
return false;
|
||||
CGDisplayModeRef applied = CGDisplayCopyDisplayMode(id);
|
||||
const bool accepted = MacDisplayResolution(applied) == change.resolution;
|
||||
if (applied)
|
||||
CGDisplayModeRelease(applied);
|
||||
if (!accepted)
|
||||
CGDisplaySetDisplayMode(id, modes.current, nullptr);
|
||||
return accepted;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool SameMacResolutionMode(CGDisplayModeRef mode,
|
||||
CGDisplayModeRef original) {
|
||||
return mode && original &&
|
||||
MacDisplayResolution(mode) == MacDisplayResolution(original) &&
|
||||
CGDisplayModeGetPixelWidth(mode) ==
|
||||
CGDisplayModeGetPixelWidth(original) &&
|
||||
CGDisplayModeGetPixelHeight(mode) ==
|
||||
CGDisplayModeGetPixelHeight(original) &&
|
||||
CGDisplayModeGetRefreshRate(mode) ==
|
||||
CGDisplayModeGetRefreshRate(original);
|
||||
}
|
||||
|
||||
inline std::function<bool()>
|
||||
CaptureMacDisplayResolution(const std::vector<DisplayInfo> &displays,
|
||||
const DisplayResolutionChange &change) {
|
||||
if (change.display_id < 0 || change.display_id >= int(displays.size()))
|
||||
return {};
|
||||
const auto display = displays[change.display_id];
|
||||
CGDirectDisplayID id = 0;
|
||||
std::string key;
|
||||
if (!ResolveMacResolutionDisplay(display, id, key) ||
|
||||
key != change.display_key)
|
||||
return {};
|
||||
// Retain the exact mode, including Retina pixel density and refresh rate.
|
||||
std::shared_ptr<CGDisplayMode> original(CGDisplayCopyDisplayMode(id),
|
||||
[](CGDisplayModeRef mode) {
|
||||
if (mode)
|
||||
CGDisplayModeRelease(mode);
|
||||
});
|
||||
if (!original)
|
||||
return {};
|
||||
return [display, key, original] {
|
||||
CGDirectDisplayID id = 0;
|
||||
std::string current_key;
|
||||
if (!ResolveMacResolutionDisplay(display, id, current_key) ||
|
||||
current_key != key)
|
||||
return false;
|
||||
CGDisplayModeRef current = CGDisplayCopyDisplayMode(id);
|
||||
const bool unchanged = SameMacResolutionMode(current, original.get());
|
||||
if (current)
|
||||
CGDisplayModeRelease(current);
|
||||
if (unchanged)
|
||||
return true;
|
||||
if (CGDisplaySetDisplayMode(id, original.get(), nullptr) != kCGErrorSuccess)
|
||||
return false;
|
||||
current = CGDisplayCopyDisplayMode(id);
|
||||
const bool restored = SameMacResolutionMode(current, original.get());
|
||||
if (current)
|
||||
CGDisplayModeRelease(current);
|
||||
return restored;
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -180,7 +180,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
bool complete, SCStreamConfiguration* configuration,
|
||||
NSError* error);
|
||||
void RetryPrivacyFilter();
|
||||
void PrivacyCoversChanged() { StartOrReconfigureCapturer(); }
|
||||
void CaptureConfigurationChanged() { StartOrReconfigureCapturer(); }
|
||||
void CaptureStopped(SCStream* source, NSError* error) {
|
||||
{
|
||||
std::lock_guard lock(lock_);
|
||||
@@ -914,13 +914,23 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
DISPATCH_QUEUE_SERIAL);
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(privacyCoversChanged:)
|
||||
name:@"CrossDeskPrivacyCoversChanged" object:nil];
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(screenParametersChanged:)
|
||||
name:NSApplicationDidChangeScreenParametersNotification object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)privacyCoversChanged:(NSNotification*)notification {
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
if (_capturer) _capturer->PrivacyCoversChanged();
|
||||
if (_capturer) _capturer->CaptureConfigurationChanged();
|
||||
}
|
||||
- (void)screenParametersChanged:(NSNotification*)notification {
|
||||
// Defer until the mode switch has completed, then fetch fresh SCDisplay
|
||||
// geometry and update the stream's pixel dimensions and input bounds.
|
||||
dispatch_async(_completion_queue, ^{
|
||||
std::lock_guard<std::mutex> lock(self->_capturer_lock);
|
||||
if (self->_capturer) self->_capturer->CaptureConfigurationChanged();
|
||||
});
|
||||
}
|
||||
- (void)configurationFinished:(SCStream*)stream generation:(uint64_t)generation
|
||||
revision:(uint64_t)revision complete:(BOOL)complete
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -105,6 +106,18 @@ inline bool ApplyWindowsResolutionMode(const wchar_t *device,
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool WindowsResolutionDeviceKey(const std::wstring &device,
|
||||
std::string &key) {
|
||||
DISPLAY_DEVICEW target{};
|
||||
target.cb = sizeof(target);
|
||||
if (!EnumDisplayDevicesW(device.c_str(), 0, &target,
|
||||
EDD_GET_DEVICE_INTERFACE_NAME) ||
|
||||
!target.DeviceID[0])
|
||||
return false;
|
||||
key = WideToUtf8(device) + "|" + WideToUtf8(target.DeviceID);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ResolveWindowsResolutionDisplay(const DisplayInfo &display,
|
||||
std::wstring &device,
|
||||
std::string &key) {
|
||||
@@ -135,14 +148,55 @@ inline bool ResolveWindowsResolutionDisplay(const DisplayInfo &display,
|
||||
if (device.empty())
|
||||
return false;
|
||||
}
|
||||
DISPLAY_DEVICEW target{};
|
||||
target.cb = sizeof(target);
|
||||
if (!EnumDisplayDevicesW(device.c_str(), 0, &target,
|
||||
EDD_GET_DEVICE_INTERFACE_NAME) ||
|
||||
!target.DeviceID[0])
|
||||
return false;
|
||||
key = WideToUtf8(device) + "|" + WideToUtf8(target.DeviceID);
|
||||
return true;
|
||||
return WindowsResolutionDeviceKey(device, key);
|
||||
}
|
||||
|
||||
inline bool SameWindowsResolutionMode(const DEVMODEW &mode,
|
||||
const DEVMODEW &original) {
|
||||
return mode.dmPelsWidth == original.dmPelsWidth &&
|
||||
mode.dmPelsHeight == original.dmPelsHeight &&
|
||||
mode.dmBitsPerPel == original.dmBitsPerPel &&
|
||||
mode.dmDisplayFrequency == original.dmDisplayFrequency &&
|
||||
mode.dmDisplayFlags == original.dmDisplayFlags &&
|
||||
(!(original.dmFields & DM_DISPLAYORIENTATION) ||
|
||||
mode.dmDisplayOrientation == original.dmDisplayOrientation) &&
|
||||
(!(original.dmFields & DM_DISPLAYFIXEDOUTPUT) ||
|
||||
mode.dmDisplayFixedOutput == original.dmDisplayFixedOutput);
|
||||
}
|
||||
|
||||
inline std::function<bool()> CaptureWindowsDisplayResolution(
|
||||
const std::vector<DisplayInfo> &displays,
|
||||
const DisplayResolutionChange &change,
|
||||
const WindowsResolutionApi &api = {}) {
|
||||
if (change.display_id < 0 || change.display_id >= int(displays.size()))
|
||||
return {};
|
||||
std::wstring device;
|
||||
std::string key;
|
||||
if (!ResolveWindowsResolutionDisplay(displays[change.display_id], device, key) ||
|
||||
key != change.display_key)
|
||||
return {};
|
||||
DEVMODEW original{};
|
||||
original.dmSize = sizeof(original);
|
||||
if (!api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, &original)) return {};
|
||||
return [device, key, original, api] {
|
||||
std::string current_key;
|
||||
// Resolve the native output, not a capture slot or stale HMONITOR. Never
|
||||
// apply a saved mode to a different monitor plugged into the same output.
|
||||
if (!WindowsResolutionDeviceKey(device, current_key) || current_key != key)
|
||||
return false;
|
||||
DEVMODEW current{};
|
||||
current.dmSize = sizeof(current);
|
||||
if (!api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, ¤t)) return false;
|
||||
if (SameWindowsResolutionMode(current, original)) return true;
|
||||
auto mode = original;
|
||||
if (api.change(device.c_str(), &mode, nullptr, CDS_TEST, nullptr) !=
|
||||
DISP_CHANGE_SUCCESSFUL ||
|
||||
api.change(device.c_str(), &mode, nullptr, 0, nullptr) !=
|
||||
DISP_CHANGE_SUCCESSFUL)
|
||||
return false;
|
||||
return api.enumerate(device.c_str(), ENUM_CURRENT_SETTINGS, ¤t) &&
|
||||
SameWindowsResolutionMode(current, original);
|
||||
};
|
||||
}
|
||||
|
||||
inline std::vector<DisplayResolutionInfo>
|
||||
@@ -187,4 +241,4 @@ ApplyWindowsDisplayResolution(const std::vector<DisplayInfo> &displays,
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -106,7 +106,7 @@ function setup_platform_settings()
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
elseif is_os("macosx") then
|
||||
add_cxflags("-Wno-unused-variable")
|
||||
add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore", "IOKit",
|
||||
add_frameworks("Cocoa", "CoreGraphics", "ColorSync", "Metal", "QuartzCore", "IOKit",
|
||||
"IOSurface",
|
||||
"ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo",
|
||||
"CoreAudio", "AudioToolbox")
|
||||
|
||||
Reference in new issue
Block a user