Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 121 additions & 7 deletions src/display_device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
// header include
#include "display_device.h"

// standard includes
#include <algorithm>
#include <cctype>
#include <mutex>
#include <regex>
#include <string_view>

// lib includes
#include <boost/algorithm/string.hpp>
#include <display_device/audio_context_interface.h>
#include <display_device/file_settings_persistence.h>
#include <display_device/json.h>
#include <display_device/retry_scheduler.h>
#include <display_device/settings_manager_interface.h>
#include <mutex>
#include <regex>

// local includes
#include "audio.h"
Expand All @@ -27,6 +32,13 @@
#include <display_device/windows/win_display_device.h>
#endif

#ifdef __APPLE__
#include <display_device/macos/display_power.h>
#include <display_device/macos/mac_api_layer.h>
#include <display_device/macos/mac_display_device.h>
#include <display_device/macos/settings_manager.h>
#endif
Comment thread
ReenigneArcher marked this conversation as resolved.

namespace display_device {
namespace {
constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL {5000};
Expand Down Expand Up @@ -129,6 +141,35 @@
return (int) result;
}

bool is_unsigned_integer(std::string_view value) {

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Linux / AppImage

‘bool display_device::{anonymous}::is_unsigned_integer(std::string_view)’ defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Windows / Windows-AMD64

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Windows / Windows-ARM64

unused function 'is_unsigned_integer' [-Werror,-Wunused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Linux Flatpak / aarch64

‘bool display_device::{anonymous}::is_unsigned_integer(std::string_view)’ defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Homebrew / ubuntu-24.04

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Homebrew / ubuntu-24.04

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Linux Flatpak / x86_64

‘bool display_device::{anonymous}::is_unsigned_integer(std::string_view)’ defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Docker / Docker-ubuntu-26.04

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Docker / Docker-debian-trixie

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Docker / Docker-ubuntu-24.04

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]

Check failure on line 144 in src/display_device.cpp

View workflow job for this annotation

GitHub Actions / Docker / Docker-ubuntu-22.04

'bool display_device::{anonymous}::is_unsigned_integer(std::string_view)' defined but not used [-Werror=unused-function]
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.
Expand Down Expand Up @@ -624,6 +665,23 @@
.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<MacSettingsManager>(
std::make_shared<MacDisplayDevice>(std::make_shared<MacApiLayer>()),
std::make_shared<sunshine_audio_context_t>(),
std::make_unique<MacPersistentState>(
std::make_shared<FileSettingsPersistence>(persistence_filepath)
),
MacWorkarounds {}
);
#else
return nullptr;
#endif
}

std::unique_ptr<DisplayPowerInterface> make_display_power() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't this added for Windows as well?

https://github.com/LizardByte/libdisplaydevice/blob/65616076ba881046085438c80fddead9beedf74f/src/windows/display_power.cpp#L43

Also, found this which should maybe replaced with the new library code?

// Keep the display awake during capture. If the display goes to sleep during
// capture, best case is that capture stops until it powers back on. However,
// worst case it will trigger us to reinit DD, waking the display back up in
// a neverending cycle of waking and sleeping the display of an idle machine.
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
auto clear_display_required = util::fail_guard([]() {
SetThreadExecutionState(ES_CONTINUOUS);
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's what I meant in part when I said I could make a power management pass after the interface-factories. Technically it could have been a part of this PR, but I did not want it out of control, touching multiple OSes. A note regarding it is also in the body text of the PR: it's definitely not forgotten about, it just felt out of scope.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously, I have a reading problem today. Thanks for the clarification!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just throw a couple TODO comments in? (Sonar will complain about the TODOs, but I'll bypass it)

#ifdef __APPLE__
return std::make_unique<MacDisplayPower>(std::make_shared<MacApiLayer>());
#else
return nullptr;
#endif
Expand Down Expand Up @@ -745,16 +803,42 @@
return std::make_unique<deinit_t>();
}

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<DisplayPowerGuardInterface> 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) {
// Fallback to giving back the output name if the platform is not supported.
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) {
Expand All @@ -765,11 +849,13 @@
}

if (const auto *disabled {std::get_if<configuration_disabled_tag_t>(&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
}

Expand All @@ -780,10 +866,24 @@
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();
}
},
Expand Down Expand Up @@ -831,7 +931,21 @@
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
Expand Down
18 changes: 18 additions & 0 deletions src/display_device.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
#pragma once

// standard includes
#include <chrono>
#include <filesystem>
#include <memory>
#include <string>

// lib includes
#include <display_device/display_power_interface.h>
#include <display_device/types.h>

// forward declarations
Expand Down Expand Up @@ -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<DisplayPowerGuardInterface> 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.
Expand Down
18 changes: 1 addition & 17 deletions src/platform/macos/av_video.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
#pragma once

// platform includes
#import <AppKit/AppKit.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreGraphics/CoreGraphics.h>

/**
* @brief macOS capture session and video output handles.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -66,20 +64,6 @@ typedef bool (^FrameCallbackBlock)(CMSampleBufferRef);
*/
@property (nonatomic, assign) NSMapTable<AVCaptureConnection *, dispatch_semaphore_t> *captureSignals;

/**
* @brief List display names accepted by the selected capture backend.
*
* @return Display names accepted by the selected capture backend.
*/
+ (NSArray<NSDictionary *> *)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.
*
Expand Down
38 changes: 4 additions & 34 deletions src/platform/macos/av_video.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSDictionary *> *)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;
Expand Down
Loading
Loading