From c9863ebee9deff21e0439b07170d5d8ed431c2e0 Mon Sep 17 00:00:00 2001 From: martona Date: Wed, 17 Jun 2026 11:28:07 -0400 Subject: [PATCH] fix(macos): wake sleeping display at the beginning of capture session (#5291) --- cmake/compile_definitions/macos.cmake | 1 + cmake/dependencies/macos.cmake | 1 + src/platform/macos/display.mm | 234 +++++++++++++++++++++++++- 3 files changed, 235 insertions(+), 1 deletion(-) diff --git a/cmake/compile_definitions/macos.cmake b/cmake/compile_definitions/macos.cmake index dbca9df90..1d184929a 100644 --- a/cmake/compile_definitions/macos.cmake +++ b/cmake/compile_definitions/macos.cmake @@ -35,6 +35,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${CORE_MEDIA_LIBRARY} ${CORE_VIDEO_LIBRARY} ${FOUNDATION_LIBRARY} + ${IOKIT_LIBRARY} ${VIDEO_TOOLBOX_LIBRARY}) set(APPLE_PLIST_TEMPLATE "${SUNSHINE_SOURCE_ASSETS_DIR}/macos/build/Info.plist.in") diff --git a/cmake/dependencies/macos.cmake b/cmake/dependencies/macos.cmake index 5e225fdac..4a027ef9d 100644 --- a/cmake/dependencies/macos.cmake +++ b/cmake/dependencies/macos.cmake @@ -9,6 +9,7 @@ FIND_LIBRARY(CORE_AUDIO_LIBRARY CoreAudio) FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia) FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo) FIND_LIBRARY(FOUNDATION_LIBRARY Foundation) +FIND_LIBRARY(IOKIT_LIBRARY IOKit) FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox) if(SUNSHINE_ENABLE_TRAY) diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index be124b2d3..5d6c9a736 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -2,6 +2,15 @@ * @file src/platform/macos/display.mm * @brief Definitions for display capture on macOS. */ + +// standard includes +#include +#include +#include + +// platform includes +#include + // local includes #include "src/config.h" #include "src/logging.h" @@ -21,12 +30,221 @@ namespace fs = std::filesystem; namespace platf { using namespace std::literals; + namespace { + const char *cg_error_name(CGError error) { + switch (error) { + case kCGErrorSuccess: + return "success"; + case kCGErrorFailure: + return "failure"; + case kCGErrorIllegalArgument: + return "illegal argument"; + case kCGErrorInvalidConnection: + return "invalid connection"; + case kCGErrorInvalidContext: + return "invalid context"; + case kCGErrorCannotComplete: + return "cannot complete"; + case kCGErrorNotImplemented: + return "not implemented"; + case kCGErrorRangeCheck: + return "range check"; + case kCGErrorTypeCheck: + return "type check"; + case kCGErrorInvalidOperation: + return "invalid operation"; + case kCGErrorNoneAvailable: + return "none available"; + default: + return "unknown"; + } + } + + std::string format_rect(CGRect rect) { + std::ostringstream formatted; + formatted << '(' << rect.origin.x << ',' << rect.origin.y << ") " + << rect.size.width << 'x' << rect.size.height; + return formatted.str(); + } + + std::string display_mode_summary(CGDisplayModeRef mode) { + if (!mode) { + return ""; + } + + std::ostringstream formatted; + formatted << CGDisplayModeGetWidth(mode) << 'x' << CGDisplayModeGetHeight(mode) + << " points, " << CGDisplayModeGetPixelWidth(mode) << 'x' + << CGDisplayModeGetPixelHeight(mode) << " pixels"; + + const auto refresh_rate = CGDisplayModeGetRefreshRate(mode); + if (refresh_rate > 0) { + formatted << ", " << refresh_rate << " Hz"; + } + + return formatted.str(); + } + + void log_display_diagnostic(CGDirectDisplayID display_id, const char *source) { + NSString *display_name = [AVVideo getDisplayName:display_id]; + const char *display_name_utf8 = display_name ? display_name.UTF8String : ""; + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display_id); + + BOOST_LOG(info) << "Display diagnostic ["sv << source << "]: id: "sv << display_id + << ", name: "sv << display_name_utf8 + << ", main: "sv << (display_id == CGMainDisplayID()) + << ", active: "sv << CGDisplayIsActive(display_id) + << ", online: "sv << CGDisplayIsOnline(display_id) + << ", asleep: "sv << CGDisplayIsAsleep(display_id) + << ", built-in: "sv << CGDisplayIsBuiltin(display_id) + << ", bounds: "sv << format_rect(CGDisplayBounds(display_id)) + << ", framebuffer pixels: "sv << CGDisplayPixelsWide(display_id) << 'x' << CGDisplayPixelsHigh(display_id) + << ", mode: "sv << display_mode_summary(mode); + + if (mode) { + CFRelease(mode); + } + } + + void log_nsscreen_diagnostics() { + NSArray *screens = [NSScreen screens]; + + BOOST_LOG(info) << "NSScreen diagnostics: count: "sv << [screens count]; + + for (NSScreen *screen in screens) { + NSNumber *display_id = screen.deviceDescription[@"NSScreenNumber"]; + NSString *screen_name = screen.localizedName; + + BOOST_LOG(info) << "NSScreen diagnostic: id: "sv << (display_id ? [display_id unsignedIntValue] : 0) + << ", name: "sv << (screen_name ? screen_name.UTF8String : "") + << ", frame: "sv << format_rect(screen.frame) + << ", backing scale: "sv << screen.backingScaleFactor; + } + } + + void log_display_list_diagnostics(const char *list_name, CGError error, const CGDirectDisplayID *displays, uint32_t count) { + BOOST_LOG(info) << list_name << ": status: "sv << cg_error_name(error) + << " ("sv << error << "), count: "sv << count; + + if (error != kCGErrorSuccess) { + return; + } + + for (uint32_t i = 0; i < count; ++i) { + log_display_diagnostic(displays[i], list_name); + } + } + + void log_display_environment_diagnostics() { + CGDirectDisplayID active_displays[kMaxDisplays]; + CGDirectDisplayID online_displays[kMaxDisplays]; + uint32_t active_display_count = 0; + uint32_t online_display_count = 0; + + const auto active_error = CGGetActiveDisplayList(kMaxDisplays, active_displays, &active_display_count); + const auto online_error = CGGetOnlineDisplayList(kMaxDisplays, online_displays, &online_display_count); + + BOOST_LOG(info) << "Main display diagnostic: id: "sv << CGMainDisplayID(); + log_display_list_diagnostics("CGGetActiveDisplayList", active_error, active_displays, active_display_count); + log_display_list_diagnostics("CGGetOnlineDisplayList", online_error, online_displays, online_display_count); + log_nsscreen_diagnostics(); + } + + bool has_required_active_display(const std::string &display_name) { + CGDirectDisplayID displays[kMaxDisplays]; + uint32_t display_count = 0; + + if (CGGetActiveDisplayList(kMaxDisplays, displays, &display_count) != kCGErrorSuccess) { + return false; + } + + if (display_name.empty()) { + return display_count > 0; + } + + char *end = nullptr; + const auto selected_display_id = std::strtoul(display_name.c_str(), &end, 10); + if (!end || *end != '\0') { + return display_count > 0; + } + + for (uint32_t i = 0; i < display_count; ++i) { + if (displays[i] == selected_display_id) { + return true; + } + } + + return false; + } + + void wake_displays_for_detection(const std::string &display_name) { + IOPMAssertionID wake_assertion = kIOPMNullAssertionID; + const auto result = IOPMAssertionDeclareUserActivity( + CFSTR("Sunshine display detection"), + kIOPMUserActiveRemote, + &wake_assertion + ); + + if (result != kIOReturnSuccess) { + BOOST_LOG(warning) << "Unable to declare remote user activity to wake displays, IOReturn: "sv << result; + return; + } + + BOOST_LOG(info) << "Declared remote user activity to wake displays, assertion id: "sv << wake_assertion; + + for (int attempt = 0; attempt < 10 && !has_required_active_display(display_name); ++attempt) { + std::this_thread::sleep_for(100ms); + } + + if (!has_required_active_display(display_name)) { + BOOST_LOG(warning) << "Display wake attempt did not expose the requested display ["sv + << display_name << "] in the active display list."sv; + } + + if (wake_assertion != kIOPMNullAssertionID) { + const auto release_result = IOPMAssertionRelease(wake_assertion); + if (release_result != kIOReturnSuccess) { + BOOST_LOG(warning) << "Unable to release display wake assertion, IOReturn: "sv << release_result; + } + } + } + } // namespace + struct av_display_t: public display_t { AVVideo *av_capture {}; CGDirectDisplayID display_id {}; + IOPMAssertionID display_sleep_assertion {kIOPMNullAssertionID}; ~av_display_t() override { [av_capture release]; + + if (display_sleep_assertion != kIOPMNullAssertionID) { + const auto result = IOPMAssertionRelease(display_sleep_assertion); + if (result != kIOReturnSuccess) { + BOOST_LOG(warning) << "Unable to release display sleep assertion, IOReturn: "sv << result; + } + } + } + + void prevent_display_sleep() { + if (display_sleep_assertion != kIOPMNullAssertionID) { + return; + } + + const auto result = IOPMAssertionCreateWithName( + kIOPMAssertPreventUserIdleDisplaySleep, + kIOPMAssertionLevelOn, + CFSTR("Sunshine display capture"), + &display_sleep_assertion + ); + + if (result == kIOReturnSuccess) { + BOOST_LOG(info) << "Created display sleep prevention assertion, assertion id: "sv << display_sleep_assertion; + return; + } + + display_sleep_assertion = kIOPMNullAssertionID; + BOOST_LOG(warning) << "Unable to create display sleep prevention assertion, IOReturn: "sv << result; } capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override { @@ -158,13 +376,18 @@ namespace platf { } auto display = std::make_shared(); + display->prevent_display_sleep(); + wake_displays_for_detection(display_name); // Default to main display display->display_id = CGMainDisplayID(); // Print all displays available with it's name and id - auto display_array = [AVVideo displayNames]; BOOST_LOG(info) << "Detecting displays"sv; + log_display_environment_diagnostics(); + + auto display_array = [AVVideo displayNames]; + bool matched_configured_display = display_name.empty(); for (NSDictionary *item in display_array) { NSNumber *display_id = item[@"id"]; // We need show display's product name and corresponding display number given by user @@ -173,8 +396,17 @@ namespace platf { BOOST_LOG(info) << "Detected display: "sv << name.UTF8String << " (id: "sv << [NSString stringWithFormat:@"%@", display_id].UTF8String << ") connected: true"sv; if (!display_name.empty() && std::atoi(display_name.c_str()) == [display_id unsignedIntValue]) { display->display_id = [display_id unsignedIntValue]; + matched_configured_display = true; } } + + if (!matched_configured_display) { + BOOST_LOG(warning) << "Configured display ["sv << display_name + << "] was not found in the active display list. Falling back to main display ["sv + << display->display_id << "]."sv; + } + + log_display_diagnostic(display->display_id, "selected for AVFoundation capture"); BOOST_LOG(info) << "Configuring selected display ("sv << display->display_id << ") to stream"sv; display->av_capture = [[AVVideo alloc] initWithDisplay:display->display_id frameRate:config.framerate];