diff --git a/src/display_device.cpp b/src/display_device.cpp index 256997d9c4e..bad003307f0 100644 --- a/src/display_device.cpp +++ b/src/display_device.cpp @@ -5,6 +5,13 @@ // header include #include "display_device.h" +// standard includes +#include +#include +#include +#include +#include + // lib includes #include #include @@ -12,8 +19,6 @@ #include #include #include -#include -#include // local includes #include "audio.h" @@ -27,6 +32,13 @@ #include #endif +#ifdef __APPLE__ + #include + #include + #include + #include +#endif + namespace display_device { namespace { constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL {5000}; @@ -129,6 +141,35 @@ namespace display_device { return (int) result; } + bool is_unsigned_integer(std::string_view value) { + return !value.empty() && std::ranges::all_of(value, [](unsigned char character) { + return std::isdigit(character); + }); + } + + std::string_view apply_result_name(SettingsManagerInterface::ApplyResult result) { + using enum SettingsManagerInterface::ApplyResult; + + switch (result) { + case Ok: + return "Ok"; + case ApiTemporarilyUnavailable: + return "ApiTemporarilyUnavailable"; + case DevicePrepFailed: + return "DevicePrepFailed"; + case PrimaryDevicePrepFailed: + return "PrimaryDevicePrepFailed"; + case DisplayModePrepFailed: + return "DisplayModePrepFailed"; + case HdrStatePrepFailed: + return "HdrStatePrepFailed"; + case PersistenceSaveFailed: + return "PersistenceSaveFailed"; + } + + return "Unknown"; + } + /** * @brief Parse resolution value from the string. * @param input String to be parsed. @@ -624,6 +665,23 @@ namespace display_device { .m_hdr_blank_delay = video_config.dd.wa.hdr_toggle_delay != std::chrono::milliseconds::zero() ? std::make_optional(video_config.dd.wa.hdr_toggle_delay) : std::nullopt } ); +#elif defined(__APPLE__) + return std::make_unique( + std::make_shared(std::make_shared()), + std::make_shared(), + std::make_unique( + std::make_shared(persistence_filepath) + ), + MacWorkarounds {} + ); +#else + return nullptr; +#endif + } + + std::unique_ptr make_display_power() { +#ifdef __APPLE__ + return std::make_unique(std::make_shared()); #else return nullptr; #endif @@ -745,6 +803,24 @@ namespace display_device { return std::make_unique(); } + bool wake_display(const std::string &display_name, std::chrono::milliseconds timeout) { + const auto display_power {make_display_power()}; + if (!display_power) { + return false; + } + + return display_power->wakeDisplay(display_name, timeout); + } + + std::unique_ptr keep_display_awake(const std::string &reason) { + const auto display_power {make_display_power()}; + if (!display_power) { + return nullptr; + } + + return display_power->keepDisplayAwake(reason); + } + std::string map_output_name(const std::string &output_name) { std::lock_guard lock {DD_DATA.mutex}; if (!DD_DATA.sm_instance) { @@ -752,9 +828,17 @@ namespace display_device { return output_name; } - return DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { + const auto mapped_name {DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); - }); + })}; + +#ifdef __APPLE__ + if (mapped_name.empty() && is_unsigned_integer(output_name)) { + return output_name; + } +#endif + + return mapped_name; } void configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { @@ -765,11 +849,13 @@ namespace display_device { } if (const auto *disabled {std::get_if(&result)}; disabled) { + BOOST_LOG(info) << "Display device configuration is disabled. Reverting any active display device configuration."; revert_configuration(); return; } - // Error already logged for failed_to_parse_tag_t case, and we also don't + BOOST_LOG(error) << "Failed to parse display device configuration. Display settings will not be changed."; + // Error details should already be logged for failed_to_parse_tag_t case, and we also don't // want to revert active configuration in case we have any } @@ -780,10 +866,24 @@ namespace display_device { return; } + BOOST_LOG(info) << "Scheduling display device configuration:\n" + << toJson(config); + DD_DATA.sm_instance->schedule([config](auto &settings_iface, auto &stop_token) { + using enum SettingsManagerInterface::ApplyResult; + // We only want to keep retrying in case of a transient errors. // In other cases, when we either fail or succeed we just want to stop... - if (settings_iface.applySettings(config) != SettingsManagerInterface::ApplyResult::ApiTemporarilyUnavailable) { + const auto result {settings_iface.applySettings(config)}; + if (result == Ok) { + BOOST_LOG(info) << "Display device configuration applied successfully."; + } else if (result == ApiTemporarilyUnavailable) { + BOOST_LOG(warning) << "Display device configuration API is temporarily unavailable. Will retry."; + } else { + BOOST_LOG(error) << "Display device configuration failed with result: " << apply_result_name(result); + } + + if (result != ApiTemporarilyUnavailable) { stop_token.requestStop(); } }, @@ -831,7 +931,21 @@ namespace display_device { SingleDisplayConfiguration config; config.m_device_id = video_config.output_name; config.m_device_prep = *device_prep; - config.m_hdr_state = parse_hdr_option(video_config, session); + + const auto hdr_state {parse_hdr_option(video_config, session)}; +#ifdef __APPLE__ + if (hdr_state) { + BOOST_LOG(info) << "Ignoring HDR display device request on macOS because macOS HDR changes are not supported by libdisplaydevice."; + } +#else + config.m_hdr_state = hdr_state; +#endif + +#ifdef __APPLE__ + if (config.m_device_prep != SingleDisplayConfiguration::DevicePreparation::VerifyOnly) { + BOOST_LOG(warning) << "macOS libdisplaydevice currently supports only VerifyOnly display preparation. The requested preparation mode will fail."; + } +#endif if (!parse_resolution_option(video_config, session, config)) { // Error already logged diff --git a/src/display_device.h b/src/display_device.h index e59334c832b..f2020e6682b 100644 --- a/src/display_device.h +++ b/src/display_device.h @@ -5,10 +5,13 @@ #pragma once // standard includes +#include #include #include +#include // lib includes +#include #include // forward declarations @@ -50,6 +53,21 @@ namespace display_device { */ [[nodiscard]] std::string map_output_name(const std::string &output_name); + /** + * @brief Ask the platform to wake displays before detection or capture. + * @param display_name Platform capture selector. + * @param timeout Maximum time to wait for platform-specific wake detection. + * @returns True if the display was already available or the wake request succeeded. + */ + [[nodiscard]] bool wake_display(const std::string &display_name, std::chrono::milliseconds timeout); + + /** + * @brief Keep displays awake until the returned guard is destroyed. + * @param reason Short human-readable reason for the power assertion. + * @returns A guard owning the platform assertion, or nullptr if unsupported or unavailable. + */ + [[nodiscard]] std::unique_ptr keep_display_awake(const std::string &reason); + /** * @brief Configure the display device based on the user configuration and the session information. * @note This is a convenience method for calling similar method of a different signature. diff --git a/src/platform/macos/av_video.h b/src/platform/macos/av_video.h index 7ba1522e25f..8832f32649e 100644 --- a/src/platform/macos/av_video.h +++ b/src/platform/macos/av_video.h @@ -5,8 +5,8 @@ #pragma once // platform includes -#import #import +#import /** * @brief macOS capture session and video output handles. @@ -16,8 +16,6 @@ struct CaptureSession { NSCondition *captureStopped; ///< Capture stopped. }; -static const int kMaxDisplays = 32; - /** * @brief AVFoundation video capture controller used by the macOS backend. */ @@ -66,20 +64,6 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef); */ @property (nonatomic, assign) NSMapTable *captureSignals; -/** - * @brief List display names accepted by the selected capture backend. - * - * @return Display names accepted by the selected capture backend. - */ -+ (NSArray *)displayNames; -/** - * @brief Return the user-visible name for a CoreGraphics display. - * - * @param displayID Display ID. - * @return Display name for the supplied CoreGraphics display ID. - */ -+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID; - /** * @brief Initialize AVFoundation capture for a display and frame rate. * diff --git a/src/platform/macos/av_video.m b/src/platform/macos/av_video.m index 630d7101598..5ad931c5a63 100644 --- a/src/platform/macos/av_video.m +++ b/src/platform/macos/av_video.m @@ -7,44 +7,14 @@ @implementation AVVideo -// XXX: Currently, this function only returns the screen IDs as names, -// which is not very helpful to the user. The API to retrieve names -// was deprecated with 10.9+. -// However, there is a solution with little external code that can be used: -// https://stackoverflow.com/questions/20025868/cgdisplayioserviceport-is-deprecated-in-os-x-10-9-how-to-replace -+ (NSArray *)displayNames { - CGDirectDisplayID displays[kMaxDisplays]; - uint32_t count; - if (CGGetActiveDisplayList(kMaxDisplays, displays, &count) != kCGErrorSuccess) { - return [NSArray array]; - } - - NSMutableArray *result = [NSMutableArray array]; - - for (uint32_t i = 0; i < count; i++) { - [result addObject:@{ - @"id": [NSNumber numberWithUnsignedInt:displays[i]], - @"name": [NSString stringWithFormat:@"%d", displays[i]], - @"displayName": [self getDisplayName:displays[i]], - }]; - } - - return [NSArray arrayWithArray:result]; -} - -+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID { - for (NSScreen *screen in [NSScreen screens]) { - if ([screen.deviceDescription[@"NSScreenNumber"] isEqualToNumber:[NSNumber numberWithUnsignedInt:displayID]]) { - return screen.localizedName; - } - } - return nil; -} - - (id)initWithDisplay:(CGDirectDisplayID)displayID frameRate:(int)frameRate { self = [super init]; CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayID); + if (!mode) { + [self release]; + return nil; + } self.displayID = displayID; self.pixelFormat = kCVPixelFormatType_32BGRA; diff --git a/src/platform/macos/display.mm b/src/platform/macos/display.mm index 1801696d405..d5fe3d024b2 100644 --- a/src/platform/macos/display.mm +++ b/src/platform/macos/display.mm @@ -4,15 +4,14 @@ */ // standard includes -#include -#include -#include - -// platform includes -#include +#include +#include +#include +#include // local includes #include "src/config.h" +#include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/platform/macos/av_img_t.h" @@ -29,188 +28,29 @@ #include "src/video.h" #undef AVMediaType -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; - } - + std::optional parse_display_id(std::string_view display_name) { if (display_name.empty()) { - return display_count > 0; + return std::nullopt; } - 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; - } + CGDirectDisplayID display_id {}; + const auto *const begin {display_name.data()}; + const auto *const end {display_name.data() + display_name.size()}; + const auto [ptr, ec] {std::from_chars(begin, end, display_id)}; + if (ec != std::errc {} || ptr != end) { + return std::nullopt; } - return false; + return display_id; } - 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; - } - } + OSType videotoolbox_pixel_format(const video::config_t &config) { + const auto colorspace {video::colorspace_from_client_config(config, false)}; + return colorspace.bit_depth == 10 ? kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; } } // namespace @@ -220,41 +60,10 @@ void wake_displays_for_detection(const std::string &display_name) { struct av_display_t: public display_t { AVVideo *av_capture {}; ///< AV capture. CGDirectDisplayID display_id {}; ///< Display ID. - IOPMAssertionID display_sleep_assertion {kIOPMNullAssertionID}; ///< Display sleep assertion. + std::unique_ptr display_power_guard; ///< Display power guard. ~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; - } - } - } - - /** - * @brief Prevent macOS from sleeping the captured display while streaming. - */ - 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 { @@ -412,37 +221,41 @@ static void setPixelFormat(void *display, OSType pixelFormat) { } auto display = std::make_shared(); - display->prevent_display_sleep(); - wake_displays_for_detection(display_name); - // Default to main display - display->display_id = CGMainDisplayID(); + BOOST_LOG(debug) << "Waking display for capture selector ["sv << display_name << ']'; + if (!display_device::wake_display(display_name, 1s)) { + BOOST_LOG(debug) << "Display wake attempt did not expose the requested display ["sv << display_name << ']'; + } - // Print all displays available with it's name and id - 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 - NSString *name = item[@"displayName"]; - // We are using CGGetActiveDisplayList that only returns active displays so hardcoded connected value in log to true - 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; - } + display->display_power_guard = display_device::keep_display_awake("Sunshine display capture"); + if (display->display_power_guard) { + BOOST_LOG(debug) << "Keeping display awake for capture"sv; + } else { + BOOST_LOG(debug) << "Unable to create display sleep prevention assertion"sv; } - if (!matched_configured_display) { + // Default to main display + display->display_id = CGMainDisplayID(); + + if (const auto configured_display_id {parse_display_id(display_name)}) { + display->display_id = *configured_display_id; + } else if (!display_name.empty()) { BOOST_LOG(warning) << "Configured display ["sv << display_name - << "] was not found in the active display list. Falling back to main display ["sv + << "] is not a valid macOS capture display id. Falling back to main display ["sv << display->display_id << "]."sv; } - log_display_diagnostic(display->display_id, "selected for AVFoundation capture"); + // Print all displays available with their names and ids + BOOST_LOG(debug) << "Detecting displays"sv; + for (const auto &device : display_device::enumerate_devices()) { + if (device.m_display_name.empty()) { + continue; + } + + BOOST_LOG(debug) << "Detected display: "sv << device.m_friendly_name + << " (id: "sv << device.m_display_name << ") connected: true"sv; + } + 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]; @@ -458,19 +271,28 @@ static void setPixelFormat(void *display, OSType pixelFormat) { display->env_width = display->width; display->env_height = display->height; + if (hwdevice_type == platf::mem_type_e::videotoolbox) { + const auto pixel_format {videotoolbox_pixel_format(config)}; + [display->av_capture setFrameWidth:config.width frameHeight:config.height]; + display->av_capture.pixelFormat = pixel_format; + } + return display; } std::vector display_names(mem_type_e hwdevice_type) { - __block std::vector display_names; - - auto display_array = [AVVideo displayNames]; + std::vector display_names; + if (hwdevice_type != platf::mem_type_e::system && hwdevice_type != platf::mem_type_e::videotoolbox) { + return display_names; + } - display_names.reserve([display_array count]); - [display_array enumerateObjectsUsingBlock:^(NSDictionary *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - NSString *name = obj[@"name"]; - display_names.emplace_back(name.UTF8String); - }]; + const auto devices {display_device::enumerate_devices()}; + display_names.reserve(devices.size()); + for (const auto &device : devices) { + if (!device.m_display_name.empty()) { + display_names.emplace_back(device.m_display_name); + } + } return display_names; } diff --git a/third-party/libdisplaydevice b/third-party/libdisplaydevice index 1b24e6ad910..65616076ba8 160000 --- a/third-party/libdisplaydevice +++ b/third-party/libdisplaydevice @@ -1 +1 @@ -Subproject commit 1b24e6ad910253109f4191e0a2d0fa21edd51c3a +Subproject commit 65616076ba881046085438c80fddead9beedf74f