From 2215927a025e25f64a4643b95ef4195fae8c80a5 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Sat, 20 Jul 2024 12:07:16 +0300 Subject: [PATCH 1/5] feat: Configure display device based on user config --- docs/configuration.md | 204 ++++++ src/audio.cpp | 34 +- src/audio.h | 44 ++ src/config.cpp | 79 ++- src/config.h | 43 +- src/confighttp.cpp | 18 + src/display_device.cpp | 586 +++++++++++++++++- src/display_device.h | 126 +++- src/main.cpp | 2 +- src/nvhttp.cpp | 59 +- src/platform/common.h | 8 + src/platform/linux/audio.cpp | 6 + src/platform/macos/microphone.mm | 6 + src/platform/windows/audio.cpp | 7 + src/process.cpp | 8 +- src/stream.cpp | 9 +- src_assets/common/assets/web/config.html | 8 + .../assets/web/configs/tabs/AudioVideo.vue | 5 + .../tabs/audiovideo/DisplayDeviceOptions.vue | 144 ++++- .../assets/web/public/assets/locale/en.json | 35 +- .../common/assets/web/troubleshooting.html | 40 ++ tests/unit/test_display_device.cpp | 276 +++++++++ 22 files changed, 1650 insertions(+), 97 deletions(-) create mode 100644 tests/unit/test_display_device.cpp diff --git a/docs/configuration.md b/docs/configuration.md index 659e3d29d5c..a31de13553c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -942,6 +942,210 @@ editing the `conf` file in a text editor. Use the examples as reference. +### dd_configuration_option + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + Perform additional configuration for the display device: +
    +
  • `disabled` - perform no additional configuration (disables all `dd_` configuration options).
  • +
  • `verify_only` - verify that display is active only (required for changing display mode and other options).
  • +
  • `ensure_active` - activate the display if it's currently inactive.
  • +
  • `ensure_primary` - activate the display if it's currently inactive and make it primary.
  • +
  • `ensure_only_display` - activate the display if it's currently inactive and disable all others.
  • +
+
Default@code{}verify_only@endcode
Example@code{} + dd_configuration_option = ensure_only_display + @endcode
+ +### dd_resolution_option + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + @note{"Optimize game settings" must be enabled for this option to work.} + Perform additional resolution configuration for the display device: +
    +
  • `disabled` - perform no additional configuration.
  • +
  • `automatic` - change resolution to the requested resolution from the client.
  • +
  • `manual` - change resolution to the user specified one (set via `dd_manual_resolution`).
  • +
+
Default@code{}automatic@endcode
Example@code{} + dd_resolution_option = manual + @endcode
+ +### dd_manual_resolution + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + @note{`dd_resolution_option` must be set to `manual`} + Specify manual resolution to be used. +
DefaultNo value
Example@code{} + dd_manual_resolution = 1920x1080 + @endcode
+ +### dd_refresh_rate_option + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + Perform additional refresh rate configuration for the display device: +
    +
  • `disabled` - perform no additional configuration.
  • +
  • `automatic` - change refresh rate to the requested FPS value from the client.
  • +
  • `manual` - change refresh rate to the user specified one (set via `dd_manual_refresh_rate`).
  • +
+
Default@code{}automatic@endcode
Example@code{} + dd_refresh_rate_option = manual + @endcode
+ +### dd_manual_refresh_rate + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + @note{`dd_refresh_rate_option` must be set to `manual`} + Specify manual refresh rate to be used. +
DefaultNo value
Example@code{} + dd_manual_resolution = 120 + dd_manual_resolution = 59.95 + @endcode
+ +### dd_hdr_option + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + Perform additional HDR configuration for the display device: +
    +
  • `disabled` - perform no additional configuration.
  • +
  • `automatic` - change HDR to the requested state from the client if the display supports it.
  • +
+
Default@code{}automatic@endcode
Example@code{} + dd_hdr_option = disabled + @endcode
+ +### dd_wa_hdr_toggle + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + @note{This option works independently of `dd_hdr_option`} + When using virtual display device as for streaming, it might display incorrect (high-contrast) color. + With this option enabled, Sunshine will try to mitigate this issue. +
Default@code{}false@endcode
Example@code{} + dd_wa_hdr_toggle = true + @endcode
+ +### dd_config_revert_delay + + + + + + + + + + + + + + +
Description + @warning{Windows only!} + Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. + Main purpose is to provide a smoother transition when quickly switching between apps. +
Default@code{}3000@endcode
Example@code{} + dd_config_revert_delay = 1500 + @endcode
+ ### min_fps_factor diff --git a/src/audio.cpp b/src/audio.cpp index b24ae61350f..82b1ec37351 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -20,16 +20,6 @@ namespace audio { using opus_t = util::safe_ptr; using sample_queue_t = std::shared_ptr>>; - struct audio_ctx_t { - // We want to change the sink for the first stream only - std::unique_ptr sink_flag; - - std::unique_ptr control; - - bool restore_sink; - platf::sink_t sink; - }; - static int start_audio_control(audio_ctx_t &ctx); static void @@ -95,8 +85,6 @@ namespace audio { }, }; - auto control_shared = safe::make_shared(start_audio_control, stop_audio_control); - void encodeThread(sample_queue_t samples, config_t config, void *channel_data) { auto packets = mail::man->queue(mail::audio_packets); @@ -149,7 +137,7 @@ namespace audio { apply_surround_params(stream, config.customStreamParams); } - auto ref = control_shared.ref(); + auto ref = get_audio_ctx_ref(); if (!ref) { return; } @@ -255,6 +243,26 @@ namespace audio { } } + audio_ctx_ref_t + get_audio_ctx_ref() { + static auto control_shared { safe::make_shared(start_audio_control, stop_audio_control) }; + return control_shared.ref(); + } + + bool + is_audio_ctx_sink_available(const audio_ctx_t &ctx) { + if (!ctx.control) { + return false; + } + + const std::string &sink = ctx.sink.host.empty() ? config::audio.sink : ctx.sink.host; + if (sink.empty()) { + return false; + } + + return ctx.control->is_sink_available(sink); + } + int map_stream(int channels, bool quality) { int shift = quality ? 1 : 0; diff --git a/src/audio.h b/src/audio.h index 208a5775871..927dfdef20b 100644 --- a/src/audio.h +++ b/src/audio.h @@ -4,6 +4,8 @@ */ #pragma once +// local includes +#include "platform/common.h" #include "thread_safe.h" #include "utility.h" @@ -55,8 +57,50 @@ namespace audio { std::bitset flags; }; + struct audio_ctx_t { + // We want to change the sink for the first stream only + std::unique_ptr sink_flag; + + std::unique_ptr control; + + bool restore_sink; + platf::sink_t sink; + }; + using buffer_t = util::buffer_t; using packet_t = std::pair; + using audio_ctx_ref_t = safe::shared_t::ptr_t; + void capture(safe::mail_t mail, config_t config, void *channel_data); + + /** + * @brief Get the reference to the audio context. + * @returns A shared pointer reference to audio context. + * @note Aside from the configuration purposes, it can be used to extend the + * audio sink lifetime to capture sink earlier and restore it later. + * + * @examples + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * @examples_end + */ + audio_ctx_ref_t + get_audio_ctx_ref(); + + /** + * @brief Check if the audio sink held by audio context is available. + * @returns True if available (and can probably be restored), false otherwise. + * @note Useful for delaying the release of audio context shared pointer (which + * tries to restore original sink). + * + * @examples + * audio_ctx_ref_t audio = get_audio_ctx_ref() + * if (audio.get()) { + * return is_audio_ctx_sink_available(*audio.get()); + * } + * return false; + * @examples_end + */ + bool + is_audio_ctx_sink_available(const audio_ctx_t &ctx); } // namespace audio diff --git a/src/config.cpp b/src/config.cpp index 77531cc06b7..3fe330b6e78 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -328,13 +328,59 @@ namespace config { } } // namespace sw + namespace dd { + video_t::dd_t::config_option_e + config_option_from_view(const std::string_view value) { +#define _CONVERT_(x) \ + if (value == #x##sv) return video_t::dd_t::config_option_e::x + _CONVERT_(disabled); + _CONVERT_(verify_only); + _CONVERT_(ensure_active); + _CONVERT_(ensure_primary); + _CONVERT_(ensure_only_display); +#undef _CONVERT_ + return video_t::dd_t::config_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::resolution_option_e + resolution_option_from_view(const std::string_view value) { +#define _CONVERT_(x) \ + if (value == #x##sv) return video_t::dd_t::resolution_option_e::x + _CONVERT_(disabled); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return video_t::dd_t::resolution_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::refresh_rate_option_e + refresh_rate_option_from_view(const std::string_view value) { +#define _CONVERT_(x) \ + if (value == #x##sv) return video_t::dd_t::refresh_rate_option_e::x + _CONVERT_(disabled); + _CONVERT_(automatic); + _CONVERT_(manual); +#undef _CONVERT_ + return video_t::dd_t::refresh_rate_option_e::disabled; // Default to this if value is invalid + } + + video_t::dd_t::hdr_option_e + hdr_option_from_view(const std::string_view value) { +#define _CONVERT_(x) \ + if (value == #x##sv) return video_t::dd_t::hdr_option_e::x + _CONVERT_(disabled); + _CONVERT_(automatic); +#undef _CONVERT_ + return video_t::dd_t::hdr_option_e::disabled; // Default to this if value is invalid + } + } // namespace dd + video_t video { 28, // qp 0, // hevc_mode 0, // av1_mode - 1, // min_fps_factor 2, // min_threads { "superfast"s, // preset @@ -385,6 +431,19 @@ namespace config { {}, // encoder {}, // adapter_name {}, // output_name + + { + video_t::dd_t::config_option_e::verify_only, // configuration_option + video_t::dd_t::resolution_option_e::automatic, // resolution_option + {}, // manual_resolution + video_t::dd_t::refresh_rate_option_e::automatic, // refresh_rate_option + {}, // manual_refresh_rate + video_t::dd_t::hdr_option_e::automatic, // hdr_option + 3s, // config_revert_delay + {} // wa + }, // display_device + + 1 // min_fps_factor }; audio_t audio { @@ -952,9 +1011,9 @@ namespace config { } int_f(vars, "qp", video.qp); - int_f(vars, "min_threads", video.min_threads); int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 }); + int_f(vars, "min_threads", video.min_threads); string_f(vars, "sw_preset", video.sw.sw_preset); if (!video.sw.sw_preset.empty()) { video.sw.svtav1_preset = sw::svtav1_preset_from_view(video.sw.sw_preset); @@ -1024,6 +1083,22 @@ namespace config { string_f(vars, "encoder", video.encoder); string_f(vars, "adapter_name", video.adapter_name); string_f(vars, "output_name", video.output_name); + + generic_f(vars, "dd_configuration_option", video.dd.configuration_option, dd::config_option_from_view); + generic_f(vars, "dd_resolution_option", video.dd.resolution_option, dd::resolution_option_from_view); + string_f(vars, "dd_manual_resolution", video.dd.manual_resolution); + generic_f(vars, "dd_refresh_rate_option", video.dd.refresh_rate_option, dd::refresh_rate_option_from_view); + string_f(vars, "dd_manual_refresh_rate", video.dd.manual_refresh_rate); + generic_f(vars, "dd_hdr_option", video.dd.hdr_option, dd::hdr_option_from_view); + { + int value = -1; + int_between_f(vars, "dd_config_revert_delay", value, { 0, std::numeric_limits::max() }); + if (value >= 0) { + video.dd.config_revert_delay = std::chrono::milliseconds { value }; + } + } + bool_f(vars, "dd_wa_hdr_toggle", video.dd.wa.hdr_toggle); + int_between_f(vars, "min_fps_factor", video.min_fps_factor, { 1, 3 }); path_f(vars, "pkey", nvhttp.pkey); diff --git a/src/config.h b/src/config.h index 891a4079772..e481a1e74d1 100644 --- a/src/config.h +++ b/src/config.h @@ -21,7 +21,6 @@ namespace config { int hevc_mode; int av1_mode; - int min_fps_factor; // Minimum fps target, determines minimum frame time int min_threads; // Minimum number of threads/slices for CPU encoding struct { std::string sw_preset; @@ -79,6 +78,48 @@ namespace config { std::string encoder; std::string adapter_name; std::string output_name; + + struct dd_t { + struct workarounds_t { + bool hdr_toggle; ///< Specify whether to apply HDR high-contrast color workaround. + }; + + enum class config_option_e { + disabled, ///< Disable the configuration for the device. + verify_only, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_active, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_primary, ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + ensure_only_display ///< @seealso{display_device::SingleDisplayConfiguration::DevicePreparation} + }; + + enum class resolution_option_e { + disabled, ///< Do not change resolution. + automatic, ///< Change resolution and use the one received from Moonlight. + manual ///< Change resolution and use the manually provided one. + }; + + enum class refresh_rate_option_e { + disabled, ///< Do not change refresh rate. + automatic, ///< Change refresh rate and use the one received from Moonlight. + manual ///< Change refresh rate and use the manually provided one. + }; + + enum class hdr_option_e { + disabled, ///< Do not change HDR settings. + automatic ///< Change HDR settings and use the state requested by Moonlight. + }; + + config_option_e configuration_option; + resolution_option_e resolution_option; + std::string manual_resolution; ///< Manual resolution in case `resolution_option == resolution_option_e::manual`. + refresh_rate_option_e refresh_rate_option; + std::string manual_refresh_rate; ///< Manual refresh rate in case `refresh_rate_option == refresh_rate_option_e::manual`. + hdr_option_e hdr_option; + std::chrono::milliseconds config_revert_delay; ///< Time to wait until settings are reverted (after stream ends/app exists). + workarounds_t wa; + } dd; + + int min_fps_factor; // Minimum fps target, determines minimum frame time }; struct audio_t { diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 756a4688259..9b8570e0817 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -28,6 +28,7 @@ #include "config.h" #include "confighttp.h" #include "crypto.h" +#include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -734,6 +735,22 @@ namespace confighttp { * } * @endcode */ + void + resetDisplayDevicePersistence(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) return; + + print_req(request); + + pt::ptree outputTree; + auto g = util::fail_guard([&outputTree, &response]() { + std::ostringstream data; + pt::write_json(data, outputTree); + response->write(data.str()); + }); + + outputTree.put("status", display_device::reset_persistence()); + } + void savePassword(resp_https_t response, req_https_t request) { if (!config::sunshine.username.empty() && !authenticate(response, request)) return; @@ -976,6 +993,7 @@ namespace confighttp { server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/configLocale$"]["GET"] = getLocale; server.resource["^/api/restart$"]["POST"] = restart; + server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence; server.resource["^/api/password$"]["POST"] = savePassword; server.resource["^/api/apps/([0-9]+)$"]["DELETE"] = deleteApp; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; diff --git a/src/display_device.cpp b/src/display_device.cpp index f273104bd34..a9a21b6ad1b 100644 --- a/src/display_device.cpp +++ b/src/display_device.cpp @@ -6,12 +6,19 @@ #include "display_device.h" // lib includes +#include +#include +#include #include #include #include +#include +#include // local includes +#include "audio.h" #include "platform/common.h" +#include "rtsp.h" // platform-specific includes #ifdef _WIN32 @@ -22,52 +29,508 @@ namespace display_device { namespace { + constexpr std::chrono::milliseconds DEFAULT_RETRY_INTERVAL { 5000 }; + + /** + * @brief A global for the settings manager interface and other settings whose lifetime is managed by `display_device::init(...)`. + */ + struct { + std::mutex mutex {}; + std::chrono::milliseconds config_revert_delay { 0 }; + std::unique_ptr> sm_instance { nullptr }; + } DD_DATA; + + /** + * @brief Helper class for capturing audio context when the API demands it. + * + * The capture is needed to be done in case some of the displays are going + * to be deactivated before the stream starts. In this case the audio context + * will be captured for this display and can be restored once it is turned back. + */ + class sunshine_audio_context_t: public AudioContextInterface { + public: + [[nodiscard]] bool + capture() override { + return context_scheduler.execute([](auto &audio_context) { + // Explicitly releasing the context first in case it was not release yet so that it can be potentially cleaned up. + audio_context = boost::none; + audio_context = audio_context_t {}; + + // Always say that we have captured it successfully as otherwise the settings change procedure will be aborted. + return true; + }); + } + + [[nodiscard]] bool + isCaptured() const override { + return context_scheduler.execute([](const auto &audio_context) { + if (audio_context) { + // In case we still have context we need to check whether it was released or not. + // If it was released we can pretend that we no longer have it as it will be immediately cleaned up in `capture` method before we acquire new context. + return !audio_context->released; + } + + return false; + }); + } + + void + release() override { + context_scheduler.schedule([](auto &audio_context, auto &stop_token) { + if (audio_context) { + audio_context->released = true; + + const auto *audio_ctx_ptr = audio_context->audio_ctx_ref.get(); + if (audio_ctx_ptr && !audio::is_audio_ctx_sink_available(*audio_ctx_ptr) && audio_context->retry_counter > 0) { + // It is possible that the audio sink is not immediately available after the display is turned on. + // Therefore, we will hold on to the audio context a little longer, until it is either available + // or we time out. + --audio_context->retry_counter; + return; + } + } + + audio_context = boost::none; + stop_token.requestStop(); + }, + SchedulerOptions { .m_sleep_durations = { 2s } }); + } + + private: + struct audio_context_t { + /** + * @brief A reference to the audio context that will automatically extend the audio session. + * @note It is auto-initialized here for convenience. + */ + decltype(audio::get_audio_ctx_ref()) audio_ctx_ref { audio::get_audio_ctx_ref() }; + + /** + * @brief Will be set to true if the capture was released, but we still have to keep the context around, because the device is not available. + */ + bool released { false }; + + /** + * @brief How many times to check if the audio sink is available before giving up. + */ + int retry_counter { 15 }; + }; + + RetryScheduler> context_scheduler { std::make_unique>(boost::none) }; + }; + + /** + * @breif Convert string to unsigned int. + * @note For random reason there is std::stoi, but not std::stou... + * @param value String to be converted + * @return Parsed unsigned integer. + */ + unsigned int + stou(const std::string &value) { + unsigned long result { std::stoul(value) }; + if (result > std::numeric_limits::max()) { + throw std::out_of_range("stou"); + } + return result; + } + + /** + * @brief Parse resolution value from the string. + * @param input String to be parsed. + * @param output Reference to output variable to fill in. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * @examples + * std::optional resolution; + * if (parse_resolution_string("1920x1080", resolution)) { + * if (resolution) { + * BOOST_LOG(info) << "Value was specified"; + * } + * else { + * BOOST_LOG(info) << "Value was empty"; + * } + * } + * @examples_end + */ + bool + parse_resolution_string(const std::string &input, std::optional &output) { + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + const std::regex resolution_regex { R"(^(\d+)x(\d+)$)" }; + + if (std::smatch match; std::regex_match(trimmed_input, match, resolution_regex)) { + try { + output = Resolution { + stou(match[1].str()), + stou(match[2].str()) + }; + return true; + } + catch (const std::out_of_range &) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << " (number out of range)."; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << ":\n" + << err.what(); + } + } + else { + if (trimmed_input.empty()) { + output = std::nullopt; + return true; + } + + BOOST_LOG(error) << "Failed to parse resolution string " << trimmed_input << R"(. It must match a "1920x1080" pattern!)"; + } + + return false; + } + + /** + * @brief Parse refresh rate value from the string. + * @param input String to be parsed. + * @param output Reference to output variable to fill in. + * @returns True on successful parsing (empty string allowed), false otherwise. + * + * @examples + * std::optional refresh_rate; + * if (parse_refresh_rate_string("59.95", refresh_rate)) { + * if (refresh_rate) { + * BOOST_LOG(info) << "Value was specified"; + * } + * else { + * BOOST_LOG(info) << "Value was empty"; + * } + * } + * @examples_end + */ + bool + parse_refresh_rate_string(const std::string &input, std::optional &output) { + static const auto is_zero { [](const auto &character) { return character == '0'; } }; + const std::string trimmed_input { boost::algorithm::trim_copy(input) }; + const std::regex refresh_rate_regex { R"(^(\d+)(?:\.(\d+))?$)" }; + + if (std::smatch match; std::regex_match(trimmed_input, match, refresh_rate_regex)) { + try { + // Here we are trimming zeros from the string to possibly reduce out of bounds case + std::string trimmed_match_1 { boost::algorithm::trim_left_copy_if(match[1].str(), is_zero) }; + if (trimmed_match_1.empty()) { + trimmed_match_1 = "0"s; // Just in case ALL the string is full of zeros, we want to leave one + } + + std::string trimmed_match_2; + if (match[2].matched) { + trimmed_match_2 = boost::algorithm::trim_right_copy_if(match[2].str(), is_zero); + } + + if (!trimmed_match_2.empty()) { + // We have a decimal point and will have to split it into numerator and denominator. + // For example: + // 59.995: + // numerator = 59995 + // denominator = 1000 + + // We are essentially removing the decimal point here: 59.995 -> 59995 + const std::string numerator_str { trimmed_match_1 + trimmed_match_2 }; + const auto numerator { stou(numerator_str) }; + + // Here we are counting decimal places and calculating denominator: 10^decimal_places + const auto denominator { static_cast(std::pow(10, trimmed_match_2.size())) }; + + output = Rational { numerator, denominator }; + } + else { + // We do not have a decimal point, just a valid number. + // For example: + // 60: + // numerator = 60 + // denominator = 1 + output = Rational { stou(trimmed_match_1), 1 }; + } + return true; + } + catch (const std::out_of_range &) { + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << " (number out of range)."; + } + catch (const std::exception &err) { + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << ":\n" + << err.what(); + } + } + else { + if (trimmed_input.empty()) { + output = std::nullopt; + return true; + } + + BOOST_LOG(error) << "Failed to parse refresh rate string " << trimmed_input << R"(. Must have a pattern of "123" or "123.456"!)"; + } + + return false; + } + + /** + * @brief Parse device preparation option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @returns Parsed device preparation value we need to use. + * Empty optional if no preparation nor configuration shall take place. + * + * @examples + * const config::video_t &video_config { config::video }; + * const auto device_prep_option = parse_device_prep_option(video_config); + * @examples_end + */ + std::optional + parse_device_prep_option(const config::video_t &video_config) { + using enum config::video_t::dd_t::config_option_e; + using enum SingleDisplayConfiguration::DevicePreparation; + + switch (video_config.dd.configuration_option) { + case verify_only: + return VerifyOnly; + case ensure_active: + return EnsureActive; + case ensure_primary: + return EnsurePrimary; + case ensure_only_display: + return EnsureOnlyDisplay; + case disabled: + break; + } + + return std::nullopt; + } + + /** + * @brief Parse resolution option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @param config A reference to a display config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * SingleDisplayConfiguration config; + * const bool success = parse_resolution_option(video_config, *launch_session, config); + * @examples_end + */ + bool + parse_resolution_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { + using resolution_option_e = config::video_t::dd_t::resolution_option_e; + + switch (video_config.dd.resolution_option) { + case resolution_option_e::automatic: { + if (!session.enable_sops) { + BOOST_LOG(warning) << R"(Sunshine is configured to change resolution automatically, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; + } + else if (session.width >= 0 && session.height >= 0) { + config.m_resolution = Resolution { + static_cast(session.width), + static_cast(session.height) + }; + } + else { + BOOST_LOG(error) << "Resolution provided by client session config is invalid: " << session.width << "x" << session.height; + return false; + } + break; + } + case resolution_option_e::manual: { + if (!session.enable_sops) { + BOOST_LOG(warning) << R"(Sunshine is configured to change resolution manually, but the "Optimize game settings" is not set in the client! Resolution will not be changed.)"; + } + else { + if (!parse_resolution_string(video_config.dd.manual_resolution, config.m_resolution)) { + BOOST_LOG(error) << "Failed to parse manual resolution string!"; + return false; + } + + if (!config.m_resolution) { + BOOST_LOG(error) << "Manual resolution must be specified!"; + return false; + } + } + break; + } + case resolution_option_e::disabled: + break; + } + + return true; + } + /** - * @brief A global for the settings manager interface whose lifetime is managed by `display_device::init()`. + * @brief Parse refresh rate option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @param config A reference to a config object that will be modified on success. + * @returns True on successful parsing, false otherwise. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * SingleDisplayConfiguration config; + * const bool success = parse_refresh_rate_option(video_config, *launch_session, config); + * @examples_end */ - std::unique_ptr> SM_INSTANCE; + bool + parse_refresh_rate_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) { + using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; + + switch (video_config.dd.refresh_rate_option) { + case refresh_rate_option_e::automatic: { + if (session.fps >= 0) { + config.m_refresh_rate = Rational { static_cast(session.fps), 1 }; + } + else { + BOOST_LOG(error) << "FPS value provided by client session config is invalid: " << session.fps; + return false; + } + break; + } + case refresh_rate_option_e::manual: { + if (!parse_refresh_rate_string(video_config.dd.manual_refresh_rate, config.m_refresh_rate)) { + BOOST_LOG(error) << "Failed to parse manual refresh rate string!"; + return false; + } + + if (!config.m_refresh_rate) { + BOOST_LOG(error) << "Manual refresh rate must be specified!"; + return false; + } + break; + } + case refresh_rate_option_e::disabled: + break; + } + + return true; + } + + /** + * @brief Parse HDR option from the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @returns Parsed HDR state value we need to switch to. + * Empty optional if no action is required. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * const auto hdr_option = parse_hdr_option(video_config, *launch_session); + * @examples_end + */ + std::optional + parse_hdr_option(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + using hdr_option_e = config::video_t::dd_t::hdr_option_e; + + switch (video_config.dd.hdr_option) { + case hdr_option_e::automatic: + return session.enable_hdr ? HdrState::Enabled : HdrState::Disabled; + case hdr_option_e::disabled: + break; + } + + return std::nullopt; + } /** * @brief Construct a settings manager interface to manage display device settings. + * @param persistence_filepath File location for saving persistent state. + * @param video_config User's video related configuration. * @return An interface or nullptr if the OS does not support the interface. */ std::unique_ptr - make_settings_manager() { + make_settings_manager([[maybe_unused]] const std::filesystem::path &persistence_filepath, [[maybe_unused]] const config::video_t &video_config) { #ifdef _WIN32 - // TODO: In the upcoming PR, add audio context capture and settings persistence return std::make_unique( std::make_shared(std::make_shared()), - nullptr, - std::make_unique(nullptr), - WinWorkarounds {}); + std::make_shared(), + std::make_unique( + std::make_shared(persistence_filepath)), + WinWorkarounds { + .m_hdr_blank_delay = video_config.dd.wa.hdr_toggle ? std::make_optional(500ms) : std::nullopt }); #else return nullptr; #endif } + + /** + * @brief Defines the "revert config" algorithms. + */ + enum class revert_option_e { + try_once, ///< Try reverting once and then abort. + try_indefinitely, ///< Keep trying to revert indefinitely. + try_indefinitely_with_delay ///< Keep trying to revert indefinitely, but delay the first try by some amount of time. + }; + + /** + * @brief Reverts the configuration based on the provided option. + * @note This is function does not lock mutex. + */ + void + revert_configuration_unlocked(const revert_option_e option) { + if (!DD_DATA.sm_instance) { + // Platform is not supported, nothing to do. + return; + } + + // Note: by default the executor function is immediately executed in the calling thread. With delay, we want to avoid that. + SchedulerOptions scheduler_option { .m_sleep_durations = { DEFAULT_RETRY_INTERVAL } }; + if (option == revert_option_e::try_indefinitely_with_delay && DD_DATA.config_revert_delay > std::chrono::milliseconds::zero()) { + scheduler_option.m_sleep_durations = { DD_DATA.config_revert_delay, DEFAULT_RETRY_INTERVAL }; + scheduler_option.m_execution = SchedulerOptions::Execution::ScheduledOnly; + } + + DD_DATA.sm_instance->schedule([try_once = (option == revert_option_e::try_once)](auto &settings_iface, auto &stop_token) { + // Here we want to keep retrying indefinitely until we succeed. + if (settings_iface.revertSettings() || try_once) { + stop_token.requestStop(); + } + }, + scheduler_option); + } } // namespace std::unique_ptr - init() { - // We can support re-init without any issues, however we should make sure to cleanup first! - SM_INSTANCE = nullptr; + init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config) { + std::lock_guard lock { DD_DATA.mutex }; + // We can support re-init without any issues, however we should make sure to clean up first! + revert_configuration_unlocked(revert_option_e::try_once); + DD_DATA.config_revert_delay = video_config.dd.config_revert_delay; + DD_DATA.sm_instance = nullptr; - // If we fail to create settings manager, this means platform is not supported and - // we will need to provided error-free passtrough in other methods - if (auto settings_manager { make_settings_manager() }) { - SM_INSTANCE = std::make_unique>(std::move(settings_manager)); + // If we fail to create settings manager, this means platform is not supported, and + // we will need to provided error-free pass-trough in other methods + if (auto settings_manager { make_settings_manager(persistence_filepath, video_config) }) { + DD_DATA.sm_instance = std::make_unique>(std::move(settings_manager)); - const auto available_devices { SM_INSTANCE->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }) }; + const auto available_devices { DD_DATA.sm_instance->execute([](auto &settings_iface) { return settings_iface.enumAvailableDevices(); }) }; BOOST_LOG(info) << "Currently available display devices:\n" << toJson(available_devices); - // TODO: In the upcoming PR, schedule recovery here + // In case we have failed to revert configuration before shutting down, we should + // do it now. + revert_configuration_unlocked(revert_option_e::try_indefinitely); } class deinit_t: public platf::deinit_t { public: ~deinit_t() override { - // TODO: In the upcoming PR, execute recovery once here - SM_INSTANCE = nullptr; + std::lock_guard lock { DD_DATA.mutex }; + try { + // This may throw if used incorrectly. At the moment this will not happen, however + // in case some unforeseen changes are made that could raise an exception, + // we definitely don't want this to happen in destructor. Especially in the + // deinit_t where the outcome does not really matter. + revert_configuration_unlocked(revert_option_e::try_once); + } + catch (std::exception &err) { + BOOST_LOG(fatal) << err.what(); + } + + DD_DATA.sm_instance = nullptr; } }; return std::make_unique(); @@ -75,11 +538,94 @@ namespace display_device { std::string map_output_name(const std::string &output_name) { - if (!SM_INSTANCE) { + 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 SM_INSTANCE->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); }); + return DD_DATA.sm_instance->execute([&output_name](auto &settings_iface) { return settings_iface.getDisplayName(output_name); }); + } + + void + configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + const auto result { parse_configuration(video_config, session) }; + if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { + configure_display(*parsed_config); + return; + } + + if (const auto *disabled { std::get_if(&result) }; disabled) { + revert_configuration(); + return; + } + + // Error already logged for failed_to_parse_tag_t case, and we also don't + // want to revert active configuration in case we have any + } + + void + configure_display(const SingleDisplayConfiguration &config) { + std::lock_guard lock { DD_DATA.mutex }; + if (!DD_DATA.sm_instance) { + // Platform is not supported, nothing to do. + return; + } + + DD_DATA.sm_instance->schedule([config](auto &settings_iface, auto &stop_token) { + // 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) { + stop_token.requestStop(); + } + }, + { .m_sleep_durations = { DEFAULT_RETRY_INTERVAL } }); + } + + void + revert_configuration() { + std::lock_guard lock { DD_DATA.mutex }; + revert_configuration_unlocked(revert_option_e::try_indefinitely_with_delay); + } + + bool + reset_persistence() { + std::lock_guard lock { DD_DATA.mutex }; + if (!DD_DATA.sm_instance) { + // Platform is not supported, assume success. + return true; + } + + return DD_DATA.sm_instance->execute([](auto &settings_iface, auto &stop_token) { + // Whatever the outcome is we want to stop interfering with the user, + // so any schedulers need to be stopped. + stop_token.requestStop(); + return settings_iface.resetPersistence(); + }); + } + + std::variant + parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session) { + const auto device_prep { parse_device_prep_option(video_config) }; + if (!device_prep) { + return configuration_disabled_tag_t {}; + } + + 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); + + if (!parse_resolution_option(video_config, session, config)) { + // Error already logged + return failed_to_parse_tag_t {}; + } + + if (!parse_refresh_rate_option(video_config, session, config)) { + // Error already logged + return failed_to_parse_tag_t {}; + } + + return config; } } // namespace display_device diff --git a/src/display_device.h b/src/display_device.h index 6562f5a3dcc..e17c408fedb 100644 --- a/src/display_device.h +++ b/src/display_device.h @@ -5,24 +5,35 @@ #pragma once // lib includes +#include +#include #include // forward declarations namespace platf { class deinit_t; -} // namespace platf +} +namespace config { + struct video_t; +} +namespace rtsp_stream { + struct launch_session_t; +} namespace display_device { /** * @brief Initialize the implementation and perform the initial state recovery (if needed). + * @param persistence_filepath File location for reading/saving persistent state. + * @param video_config User's video related configuration. * @returns A deinit_t instance that performs cleanup when destroyed. * * @examples - * const auto init_guard { display_device::init() }; + * const config::video_t &video_config { config::video }; + * const auto init_guard { init("/my/persitence/file.state", video_config) }; * @examples_end */ - std::unique_ptr - init(); + [[nodiscard]] std::unique_ptr + init(const std::filesystem::path &persistence_filepath, const config::video_t &video_config); /** * @brief Map the output name to a specific display. @@ -34,6 +45,111 @@ namespace display_device { * const auto mapped_name_custom { map_output_name("{some-device-id}") }; * @examples_end */ - std::string + [[nodiscard]] std::string map_output_name(const std::string &output_name); + + /** + * @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. + * + * @param video_config User's video related configuration. + * @param session Session information. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * configure_display(video_config, *launch_session); + * @examples_end + */ + void + configure_display(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); + + /** + * @brief Configure the display device using the provided configuration. + * + * In some cases configuring display can fail due to transient issues and + * we will keep trying every 5 seconds, even if the stream has already started as there was + * no possibility to apply settings before the stream start. + * + * Therefore, there is no return value as we still want to continue with the stream, so that + * the users can do something about it once they are connected. Otherwise, we might + * prevent users from logging in at all if we keep failing to apply configuration. + * + * @param config Configuration for the display. + * + * @examples + * const SingleDisplayConfiguration valid_config { }; + * configure_display(valid_config); + * @examples_end + */ + void + configure_display(const SingleDisplayConfiguration &config); + + /** + * @brief Revert the display configuration and restore the previous state. + * + * In case the state could not be restored, by default it will be retried again in 5 seconds + * (repeating indefinitely until success or until persistence is reset). + * + * @examples + * revert_configuration(); + * @examples_end + */ + void + revert_configuration(); + + /** + * @brief Reset the persistence and currently held initial display state. + * + * This is normally used to get out of the "broken" state where the algorithm wants + * to restore the initial display state, but it is no longer possible. + * + * This could happen if the display is no longer available or the hardware was changed + * and the device ids no longer match. + * + * The user then accepts that Sunshine is not able to restore the state and "agrees" to + * do it manually. + * + * @return + * @note Whether the function succeeds or fails, the any of the scheduled "retries" from + * other methods will be stopped to not interfere with the user actions. + * + * @examples + * const auto result = reset_persistence(); + * @examples_end + */ + [[nodiscard]] bool + reset_persistence(); + + /** + * @brief A tag structure indicating that configuration parsing has failed. + */ + struct failed_to_parse_tag_t {}; + + /** + * @brief A tag structure indicating that configuration is disabled. + */ + struct configuration_disabled_tag_t {}; + + /** + * @brief Parse the user configuration and the session information. + * @param video_config User's video related configuration. + * @param session Session information. + * @return Parsed single display configuration or + * a tag indicating that the parsing has failed or + * a tag indicating that the user does not want to perform any configuration. + * + * @examples + * const std::shared_ptr launch_session; + * const config::video_t &video_config { config::video }; + * + * const auto config { parse_configuration(video_config, *launch_session) }; + * if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { + * configure_display(*config); + * } + * @examples_end + */ + [[nodiscard]] std::variant + parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session); } // namespace display_device diff --git a/src/main.cpp b/src/main.cpp index b9ffc049128..04ab7d13231 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -137,7 +137,7 @@ main(int argc, char *argv[]) { // Adding guard here first as it also performs recovery after crash, // otherwise people could theoretically end up without display output. // It also should be destroyed before forced shutdown to expedite the cleanup. - auto display_device_deinit_guard = display_device::init(); + auto display_device_deinit_guard = display_device::init(platf::appdata() / "display_device.state", config::video); if (!display_device_deinit_guard) { BOOST_LOG(error) << "Display device session failed to initialize"sv; } diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index ea9248e715a..98b3c58e31c 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -22,6 +22,7 @@ // local includes #include "config.h" #include "crypto.h" +#include "display_device.h" #include "file_handler.h" #include "globals.h" #include "httpcommon.h" @@ -812,12 +813,17 @@ namespace nvhttp { print_req(request); pt::ptree tree; + bool revert_display_configuration { false }; auto g = util::fail_guard([&]() { std::ostringstream data; pt::write_xml(data, tree); response->write(data.str()); response->close_connection_after_response = true; + + if (revert_display_configuration) { + display_device::revert_configuration(); + } }); auto args = request->parse_query_string(); @@ -844,11 +850,22 @@ namespace nvhttp { return; } - // Probe encoders again before streaming to ensure our chosen - // encoder matches the active GPU (which could have changed - // due to hotplugging, driver crash, primary monitor change, - // or any number of other factors). + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + auto launch_session = make_launch_session(host_audio, args); + if (rtsp_stream::session_count() == 0) { + // We want to prepare display only if there are no active sessions at + // the moment. This should be done before probing encoders as it could + // change the active displays. + display_device::configure_display(config::video, *launch_session); + + // The display should be restored in case something fails as there are no other sessions. + revert_display_configuration = true; + + // Probe encoders again before streaming to ensure our chosen + // encoder matches the active GPU (which could have changed + // due to hotplugging, driver crash, primary monitor change, + // or any number of other factors). if (video::probe_encoders()) { tree.put("root..status_code", 503); tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); @@ -858,9 +875,6 @@ namespace nvhttp { } } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -890,6 +904,9 @@ namespace nvhttp { tree.put("root.gamesession", 1); rtsp_stream::launch_session_raise(launch_session); + + // Stream was started successfully, we will revert the config when the app or session terminates + revert_display_configuration = false; } void @@ -925,7 +942,21 @@ namespace nvhttp { return; } - if (rtsp_stream::session_count() == 0) { + // Newer Moonlight clients send localAudioPlayMode on /resume too, + // so we should use it if it's present in the args and there are + // no active sessions we could be interfering with. + const bool no_active_sessions { rtsp_stream::session_count() == 0 }; + if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) { + host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); + } + const auto launch_session = make_launch_session(host_audio, args); + + if (no_active_sessions) { + // We want to prepare display only if there are no active sessions at + // the moment. This should be done before probing encoders as it could + // change the active displays. + display_device::configure_display(config::video, *launch_session); + // Probe encoders again before streaming to ensure our chosen // encoder matches the active GPU (which could have changed // due to hotplugging, driver crash, primary monitor change, @@ -937,17 +968,8 @@ namespace nvhttp { return; } - - // Newer Moonlight clients send localAudioPlayMode on /resume too, - // so we should use it if it's present in the args and there are - // no active sessions we could be interfering with. - if (args.find("localAudioPlayMode"s) != std::end(args)) { - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); - } } - auto launch_session = make_launch_session(host_audio, args); - auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { BOOST_LOG(error) << "Rejecting client that cannot comply with mandatory encryption requirement"sv; @@ -989,6 +1011,9 @@ namespace nvhttp { if (proc::proc.running() > 0) { proc::proc.terminate(); } + + // The config needs to be reverted regardless of whether "proc::proc.terminate()" was called or not. + display_device::revert_configuration(); } void diff --git a/src/platform/common.h b/src/platform/common.h index 4b2ca66a06b..abcbefc82d8 100644 --- a/src/platform/common.h +++ b/src/platform/common.h @@ -550,6 +550,14 @@ namespace platf { virtual std::unique_ptr microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size) = 0; + /** + * @brief Check if the audio sink is available in the system. + * @param sink Sink to be checked. + * @returns True if available, false otherwise. + */ + virtual bool + is_sink_available(const std::string &sink) = 0; + virtual std::optional sink_info() = 0; diff --git a/src/platform/linux/audio.cpp b/src/platform/linux/audio.cpp index ff231707e61..a48ee2f028d 100644 --- a/src/platform/linux/audio.cpp +++ b/src/platform/linux/audio.cpp @@ -473,6 +473,12 @@ namespace platf { return ::platf::microphone(mapping, channels, sample_rate, frame_size, get_monitor_name(sink_name)); } + bool + is_sink_available(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; + return true; + } + int set_sink(const std::string &sink) override { auto alarm = safe::make_alarm(); diff --git a/src/platform/macos/microphone.mm b/src/platform/macos/microphone.mm index 1e3a4cd65ed..8d2129f28b3 100644 --- a/src/platform/macos/microphone.mm +++ b/src/platform/macos/microphone.mm @@ -81,6 +81,12 @@ return mic; } + bool + is_sink_available(const std::string &sink) override { + BOOST_LOG(warning) << "audio_control_t::is_sink_available() unimplemented: "sv << sink; + return true; + } + std::optional sink_info() override { sink_t sink; diff --git a/src/platform/windows/audio.cpp b/src/platform/windows/audio.cpp index 3335eeb0b0f..3c401976afc 100644 --- a/src/platform/windows/audio.cpp +++ b/src/platform/windows/audio.cpp @@ -722,6 +722,13 @@ namespace platf::audio { return sink; } + bool + is_sink_available(const std::string &sink) override { + const auto match_list = match_all_fields(from_utf8(sink)); + const auto matched = find_device_id(match_list); + return static_cast(matched); + } + /** * @brief Extract virtual audio sink information possibly encoded in the sink name. * @param sink The sink name diff --git a/src/process.cpp b/src/process.cpp index 1c78ff4d9c8..3ee9d6b9c6f 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -23,6 +23,7 @@ #include "config.h" #include "crypto.h" +#include "display_device.h" #include "logging.h" #include "platform/common.h" #include "system_tray.h" @@ -341,16 +342,19 @@ namespace proc { } _pipe.reset(); -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + bool has_run = _app_id > 0; // Only show the Stopped notification if we actually have an app to stop // Since terminate() is always run when a new app has started if (proc::proc.get_last_run_app_name().length() > 0 && has_run) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_stopped(proc::proc.get_last_run_app_name()); - } #endif + display_device::revert_configuration(); + } + _app_id = -1; } diff --git a/src/stream.cpp b/src/stream.cpp index e6729a2197e..f26a8b762e9 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -20,6 +20,7 @@ extern "C" { } #include "config.h" +#include "display_device.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -1948,11 +1949,15 @@ namespace stream { // If this is the last session, invoke the platform callbacks if (--running_sessions == 0) { -#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 if (proc::proc.running()) { +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 system_tray::update_tray_pausing(proc::proc.get_last_run_app_name()); - } #endif + } + else { + display_device::revert_configuration(); + } + platf::streaming_will_stop(); } diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index a7681fdde02..3c5ec35304a 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -170,6 +170,14 @@

{{ $t('config.configuration') }}

"install_steam_audio_drivers": "enabled", "adapter_name": "", "output_name": "", + "dd_configuration_option": "verify_only", + "dd_resolution_option": "automatic", + "dd_manual_resolution": "", + "dd_refresh_rate_option": "automatic", + "dd_manual_refresh_rate": "", + "dd_hdr_option": "automatic", + "dd_hdr_workaround": false, + "dd_config_revert_delay": 3000, "min_fps_factor": 1, }, }, diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 0f18f9a14dc..1f025c3732a 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -75,6 +75,11 @@ const config = ref(props.config) :config="config" /> + + diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index b277c8af306..163711b9bc2 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -150,6 +150,31 @@ "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Sunshine's state file.", + "dd_config_disabled": "Disabled", + "dd_config_ensure_active": "Activate the display automatically", + "dd_config_ensure_only_display": "Deactivate other displays and activate only the specified display", + "dd_config_ensure_primary": "Activate the display automatically and make it a primary display", + "dd_config_label": "Device configuration", + "dd_config_revert_delay": "Config revert delay", + "dd_config_revert_delay_desc": "Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps.", + "dd_config_verify_only": "Verify that the display is enabled", + "dd_hdr_option": "HDR", + "dd_hdr_option_automatic": "Switch on/off the HDR mode as requested by the client", + "dd_hdr_option_disabled": "Do not change HDR settings", + "dd_options_header": "Advanced display device options", + "dd_refresh_rate_option": "Refresh rate", + "dd_refresh_rate_option_automatic": "Use FPS value provided by the client", + "dd_refresh_rate_option_disabled": "Do not change refresh rate", + "dd_refresh_rate_option_manual": "Use manually entered refresh rate", + "dd_refresh_rate_option_manual_desc": "Enter the refresh rate to be used", + "dd_resolution_option": "Resolution", + "dd_resolution_option_automatic": "Use resolution provided by the client", + "dd_resolution_option_disabled": "Do not change resolution", + "dd_resolution_option_manual": "Use manually entered resolution", + "dd_resolution_option_manual_desc": "Enter the resolution to be used", + "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", + "dd_wa_hdr_toggle_desc_windows": "When using virtual display device as for streaming, it might display incorrect HDR color. With this option enabled, Sunshine will try to mitigate this issue.", + "dd_wa_hdr_toggle_windows": "Enable high-contrast workaround for HDR", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder", @@ -169,8 +194,8 @@ "gamepad_desc": "Choose which type of gamepad to emulate on the host", "gamepad_ds4": "DS4 (PS4)", "gamepad_ds5": "DS5 (PS5)", - "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_manual": "Manual DS4 options", + "gamepad_switch": "Nintendo Pro (Switch)", "gamepad_x360": "X360 (Xbox 360)", "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", @@ -189,8 +214,8 @@ "key_repeat_delay_desc": "Control how fast keys will repeat themselves. The initial delay in milliseconds before repeating keys.", "key_repeat_frequency": "Key Repeat Frequency", "key_repeat_frequency_desc": "How often keys repeat every second. This configurable option supports decimals.", - "key_rightalt_to_key_windows": "Map Right Alt key to Windows key", "key_rightalt_to_key_win_desc": "It may be possible that you cannot send the Windows Key from Moonlight directly. In those cases it may be useful to make Sunshine think the Right Alt key is the Windows key", + "key_rightalt_to_key_windows": "Map Right Alt key to Windows key", "keyboard": "Enable Keyboard Input", "keyboard_desc": "Allows guests to control the host system with the keyboard", "lan_encryption_mode": "LAN Encryption Mode", @@ -376,6 +401,10 @@ "third_party_notice": "Third Party Notice" }, "troubleshooting": { + "dd_reset": "Reset Persistent Display Device Settings", + "dd_reset_desc": "If Sunshine is stuck trying to restore the changed display device settings, you can reset the settings and proceed to restore the display state manually.", + "dd_reset_error": "Error while resetting persistence!", + "dd_reset_success": "Success resetting persistence!", "force_close": "Force Close", "force_close_desc": "If Moonlight complains about an app currently running, force closing the app should fix the issue.", "force_close_error": "Error while closing Application", @@ -404,4 +433,4 @@ "login": "Login", "welcome_success": "This page will reload soon, your browser will ask you for the new credentials" } -} +} \ No newline at end of file diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index 5e02b8c8113..9ac45698e23 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -75,6 +75,25 @@

{{ $t('troubleshooting.restart_sunshine') }}

+ +
+
+

{{ $t('troubleshooting.dd_reset') }}

+
+

{{ $t('troubleshooting.dd_reset_desc') }}

+
+ {{ $t('troubleshooting.dd_reset_success') }} +
+
+ {{ $t('troubleshooting.dd_reset_error') }} +
+
+ +
+
+
@@ -141,11 +160,14 @@

{{ $t('troubleshooting.logs') }}

clients: [], closeAppPressed: false, closeAppStatus: null, + ddResetPressed: false, + ddResetStatus: null, logs: 'Loading...', logFilter: null, logInterval: null, restartPressed: false, showApplyMessage: false, + platform: "", unpairAllPressed: false, unpairAllStatus: null, }; @@ -159,6 +181,12 @@

{{ $t('troubleshooting.logs') }}

} }, created() { + fetch("/api/config") + .then((r) => r.json()) + .then((r) => { + this.platform = r.platform; + }); + this.logInterval = setInterval(() => { this.refreshLogs(); }, 5000); @@ -236,6 +264,18 @@

{{ $t('troubleshooting.logs') }}

method: "POST", }); }, + ddResetPersistence() { + this.ddResetPressed = true; + fetch("/api/reset-display-device-persistence", { method: "POST" }) + .then((r) => r.json()) + .then((r) => { + this.ddResetPressed = false; + this.ddResetStatus = r.status.toString() === "true"; + setTimeout(() => { + this.ddResetStatus = null; + }, 5000); + }); + }, }, }); diff --git a/tests/unit/test_display_device.cpp b/tests/unit/test_display_device.cpp new file mode 100644 index 00000000000..f08a5d62056 --- /dev/null +++ b/tests/unit/test_display_device.cpp @@ -0,0 +1,276 @@ +/** + * @file tests/unit/test_display_device.cpp + * @brief Test src/display_device.*. + */ +#include "../tests_common.h" + +#include +#include +#include + +namespace { + using config_option_e = config::video_t::dd_t::config_option_e; + using device_prep_t = display_device::SingleDisplayConfiguration::DevicePreparation; + + using hdr_option_e = config::video_t::dd_t::hdr_option_e; + using hdr_state_e = display_device::HdrState; + + using resolution_option_e = config::video_t::dd_t::resolution_option_e; + using resolution_t = display_device::Resolution; + + using refresh_rate_option_e = config::video_t::dd_t::refresh_rate_option_e; + using rational_t = display_device::Rational; + + struct failed_to_parse_resolution_tag_t {}; + struct failed_to_parse_refresh_rate_tag_t {}; + struct no_refresh_rate_tag_t {}; + struct no_resolution_tag_t {}; + + struct client_resolution_t { + int width; + int height; + }; + + using client_fps_t = int; + using sops_enabled_t = bool; + using client_wants_hdr_t = bool; + + constexpr unsigned int max_uint { std::numeric_limits::max() }; + const std::string max_uint_string { std::to_string(std::numeric_limits::max()) }; + + template + struct DisplayDeviceConfigTest: testing::TestWithParam {}; +} // namespace + +using ParseDeviceId = DisplayDeviceConfigTest>; +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseDeviceId, + testing::Values( + std::make_pair(""s, ""s), + std::make_pair("SomeId"s, "SomeId"s), + std::make_pair("{daeac860-f4db-5208-b1f5-cf59444fb768}"s, "{daeac860-f4db-5208-b1f5-cf59444fb768}"s))); +TEST_P(ParseDeviceId, IntegrationTest) { + const auto &[input_value, expected_value] = GetParam(); + + config::video_t video_config {}; + video_config.dd.configuration_option = config_option_e::verify_only; + video_config.output_name = input_value; + + const auto result { display_device::parse_configuration(video_config, {}) }; + EXPECT_EQ(std::get(result).m_device_id, expected_value); +} + +using ParseConfigOption = DisplayDeviceConfigTest>>; +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseConfigOption, + testing::Values( + std::make_pair(config_option_e::disabled, std::nullopt), + std::make_pair(config_option_e::verify_only, device_prep_t::VerifyOnly), + std::make_pair(config_option_e::ensure_active, device_prep_t::EnsureActive), + std::make_pair(config_option_e::ensure_primary, device_prep_t::EnsurePrimary), + std::make_pair(config_option_e::ensure_only_display, device_prep_t::EnsureOnlyDisplay))); +TEST_P(ParseConfigOption, IntegrationTest) { + const auto &[input_value, expected_value] = GetParam(); + + config::video_t video_config {}; + video_config.dd.configuration_option = input_value; + + const auto result { display_device::parse_configuration(video_config, {}) }; + if (const auto *parsed_config { std::get_if(&result) }; parsed_config) { + ASSERT_EQ(parsed_config->m_device_prep, expected_value); + } + else { + ASSERT_EQ(std::get_if(&result) != nullptr, !expected_value); + } +} + +using ParseHdrOption = DisplayDeviceConfigTest, std::optional>>; +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseHdrOption, + testing::Values( + std::make_pair(std::make_pair(hdr_option_e::disabled, client_wants_hdr_t { true }), std::nullopt), + std::make_pair(std::make_pair(hdr_option_e::disabled, client_wants_hdr_t { false }), std::nullopt), + std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t { true }), hdr_state_e::Enabled), + std::make_pair(std::make_pair(hdr_option_e::automatic, client_wants_hdr_t { false }), hdr_state_e::Disabled))); +TEST_P(ParseHdrOption, IntegrationTest) { + const auto &[input_value, expected_value] = GetParam(); + const auto &[input_hdr_option, input_enable_hdr] = input_value; + + config::video_t video_config {}; + video_config.dd.configuration_option = config_option_e::verify_only; + video_config.dd.hdr_option = input_hdr_option; + + rtsp_stream::launch_session_t session {}; + session.enable_hdr = input_enable_hdr; + + const auto result { display_device::parse_configuration(video_config, session) }; + EXPECT_EQ(std::get(result).m_hdr_state, expected_value); +} + +using ParseResolutionOption = DisplayDeviceConfigTest>, + std::variant>>; +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseResolutionOption, + testing::Values( + //---- Disabled cases ---- + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { true }, client_resolution_t { 1920, 1080 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { true }, "1920x1080"s), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { true }, client_resolution_t { -1, -1 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { true }, "invalid_res"s), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { false }, client_resolution_t { 1920, 1080 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { false }, "1920x1080"s), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { false }, client_resolution_t { -1, -1 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::disabled, sops_enabled_t { false }, "invalid_res"s), no_resolution_tag_t {}), + //---- Automatic cases ---- + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, client_resolution_t { 1920, 1080 }), resolution_t { 1920, 1080 }), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, "1920x1080"s), resolution_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, client_resolution_t { -1, -1 }), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, "invalid_res"s), resolution_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { false }, client_resolution_t { 1920, 1080 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { false }, "1920x1080"s), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { false }, client_resolution_t { -1, -1 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { false }, "invalid_res"s), no_resolution_tag_t {}), + //---- Manual cases ---- + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, client_resolution_t { 1920, 1080 }), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "1920x1080"s), resolution_t { 1920, 1080 }), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, client_resolution_t { -1, -1 }), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "invalid_res"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { false }, client_resolution_t { 1920, 1080 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { false }, "1920x1080"s), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { false }, client_resolution_t { -1, -1 }), no_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { false }, "invalid_res"s), no_resolution_tag_t {}), + //---- Both negative values from client are checked ---- + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, client_resolution_t { 0, 0 }), resolution_t { 0, 0 }), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, client_resolution_t { -1, 0 }), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::automatic, sops_enabled_t { true }, client_resolution_t { 0, -1 }), failed_to_parse_resolution_tag_t {}), + //---- Resolution string format validation ---- + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "0x0"s), resolution_t { 0, 0 }), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "0x"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "x0"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "-1x1"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "1x-1"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "x0x0"s), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, "0x0x"s), failed_to_parse_resolution_tag_t {}), + //---- String number is out of bounds ---- + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, max_uint_string + "x"s + max_uint_string), resolution_t { max_uint, max_uint }), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, max_uint_string + "0"s + "x"s + max_uint_string), failed_to_parse_resolution_tag_t {}), + std::make_pair(std::make_tuple(resolution_option_e::manual, sops_enabled_t { true }, max_uint_string + "x"s + max_uint_string + "0"s), failed_to_parse_resolution_tag_t {}))); +TEST_P(ParseResolutionOption, IntegrationTest) { + const auto &[input_value, expected_value] = GetParam(); + const auto &[input_resolution_option, input_enable_sops, input_resolution] = input_value; + + config::video_t video_config {}; + video_config.dd.configuration_option = config_option_e::verify_only; + video_config.dd.resolution_option = input_resolution_option; + + rtsp_stream::launch_session_t session {}; + session.enable_sops = input_enable_sops; + + if (const auto *client_res { std::get_if(&input_resolution) }; client_res) { + video_config.dd.manual_resolution = {}; + session.width = client_res->width; + session.height = client_res->height; + } + else { + video_config.dd.manual_resolution = std::get(input_resolution); + session.width = {}; + session.height = {}; + } + + const auto result { display_device::parse_configuration(video_config, session) }; + if (const auto *failed_option { std::get_if(&expected_value) }; failed_option) { + EXPECT_NO_THROW(std::get(result)); + } + else { + std::optional expected_resolution; + if (const auto *valid_resolution_option { std::get_if(&expected_value) }; valid_resolution_option) { + expected_resolution = *valid_resolution_option; + } + + EXPECT_EQ(std::get(result).m_resolution, expected_resolution); + } +} + +using ParseRefreshRateOption = DisplayDeviceConfigTest>, + std::variant>>; +INSTANTIATE_TEST_SUITE_P( + DisplayDeviceConfigTest, + ParseRefreshRateOption, + testing::Values( + //---- Disabled cases ---- + std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, client_fps_t { 60 }), no_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "60"s), no_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "59.9885"s), no_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, client_fps_t { -1 }), no_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::disabled, "invalid_refresh_rate"s), no_refresh_rate_tag_t {}), + //---- Automatic cases ---- + std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, client_fps_t { 60 }), rational_t { 60, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "60"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "59.9885"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, client_fps_t { -1 }), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::automatic, "invalid_refresh_rate"s), rational_t { 0, 1 }), + //---- Manual cases ---- + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, client_fps_t { 60 }), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60"s), rational_t { 60, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "59.9885"s), rational_t { 599885, 10000 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, client_fps_t { -1 }), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "invalid_refresh_rate"s), failed_to_parse_refresh_rate_tag_t {}), + //---- Refresh rate string format validation ---- + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0000000000000"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000000.0000000"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "0.0"s), rational_t { 0, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "000000000000010"s), rational_t { 10, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.0000000"s), rational_t { 10, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.1000000"s), rational_t { 101, 10 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000010.0100000"s), rational_t { 1001, 100 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "00000000.1000000"s), rational_t { 1, 10 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60,0"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "-60.0"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60.-0"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "a60.0"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60.0b"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "a60"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "60b"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, "-60"s), failed_to_parse_refresh_rate_tag_t {}), + //---- String number is out of bounds ---- + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string), rational_t { max_uint, 1 }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string + "0"s), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "."s + max_uint_string.substr(1)), rational_t { max_uint, static_cast(std::pow(10, max_uint_string.size() - 1)) }), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "0"s + "."s + max_uint_string.substr(1)), failed_to_parse_refresh_rate_tag_t {}), + std::make_pair(std::make_tuple(refresh_rate_option_e::manual, max_uint_string.substr(0, 1) + "."s + "0"s + max_uint_string.substr(1)), failed_to_parse_refresh_rate_tag_t {}))); +TEST_P(ParseRefreshRateOption, IntegrationTest) { + const auto &[input_value, expected_value] = GetParam(); + const auto &[input_refresh_rate_option, input_refresh_rate] = input_value; + + config::video_t video_config {}; + video_config.dd.configuration_option = config_option_e::verify_only; + video_config.dd.refresh_rate_option = input_refresh_rate_option; + + rtsp_stream::launch_session_t session {}; + if (const auto *client_refresh_rate { std::get_if(&input_refresh_rate) }; client_refresh_rate) { + video_config.dd.manual_refresh_rate = {}; + session.fps = *client_refresh_rate; + } + else { + video_config.dd.manual_refresh_rate = std::get(input_refresh_rate); + session.fps = {}; + } + + const auto result { display_device::parse_configuration(video_config, session) }; + if (const auto *failed_option { std::get_if(&expected_value) }; failed_option) { + EXPECT_NO_THROW(std::get(result)); + } + else { + std::optional expected_refresh_rate; + if (const auto *valid_refresh_rate_option { std::get_if(&expected_value) }; valid_refresh_rate_option) { + expected_refresh_rate = *valid_refresh_rate_option; + } + + EXPECT_EQ(std::get(result).m_refresh_rate, expected_refresh_rate); + } +} From 8f00b290f05162e028d5bcb4f6f4ff2527ad5bb2 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 12 Dec 2024 16:56:02 +0200 Subject: [PATCH 2/5] Review fixes: vue and json --- .../tabs/audiovideo/DisplayDeviceOptions.vue | 4 ++-- .../common/assets/web/public/assets/locale/en.json | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue index 129dcf48e9e..dcd14db4c2a 100644 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue @@ -32,7 +32,7 @@ const config = ref(props.config) {{ $t('config.dd_config_label') }}
{{ $t('config.dd_config_revert_delay_desc') }} diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 163711b9bc2..8068ad9a804 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -150,31 +150,30 @@ "controller_desc": "Allows guests to control the host system with a gamepad / controller", "credentials_file": "Credentials File", "credentials_file_desc": "Store Username/Password separately from Sunshine's state file.", - "dd_config_disabled": "Disabled", "dd_config_ensure_active": "Activate the display automatically", "dd_config_ensure_only_display": "Deactivate other displays and activate only the specified display", "dd_config_ensure_primary": "Activate the display automatically and make it a primary display", "dd_config_label": "Device configuration", "dd_config_revert_delay": "Config revert delay", "dd_config_revert_delay_desc": "Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps.", - "dd_config_verify_only": "Verify that the display is enabled", + "dd_config_verify_only": "Verify that the display is enabled (default)", "dd_hdr_option": "HDR", - "dd_hdr_option_automatic": "Switch on/off the HDR mode as requested by the client", + "dd_hdr_option_automatic": "Switch on/off the HDR mode as requested by the client (default)", "dd_hdr_option_disabled": "Do not change HDR settings", "dd_options_header": "Advanced display device options", "dd_refresh_rate_option": "Refresh rate", - "dd_refresh_rate_option_automatic": "Use FPS value provided by the client", + "dd_refresh_rate_option_automatic": "Use FPS value provided by the client (default)", "dd_refresh_rate_option_disabled": "Do not change refresh rate", "dd_refresh_rate_option_manual": "Use manually entered refresh rate", "dd_refresh_rate_option_manual_desc": "Enter the refresh rate to be used", "dd_resolution_option": "Resolution", - "dd_resolution_option_automatic": "Use resolution provided by the client", + "dd_resolution_option_automatic": "Use resolution provided by the client (default)", "dd_resolution_option_disabled": "Do not change resolution", "dd_resolution_option_manual": "Use manually entered resolution", "dd_resolution_option_manual_desc": "Enter the resolution to be used", "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", "dd_wa_hdr_toggle_desc_windows": "When using virtual display device as for streaming, it might display incorrect HDR color. With this option enabled, Sunshine will try to mitigate this issue.", - "dd_wa_hdr_toggle_windows": "Enable high-contrast workaround for HDR", + "dd_wa_hdr_toggle_windows": "Enable high-contrast workaround for HDR (default: off)", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder", @@ -433,4 +432,4 @@ "login": "Login", "welcome_success": "This page will reload soon, your browser will ask you for the new credentials" } -} \ No newline at end of file +} From e7359559c8d682732ccd239080ffda273e39e476 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 12 Dec 2024 18:12:32 +0200 Subject: [PATCH 3/5] Review fixes: configuration.md (mostly) --- docs/configuration.md | 113 +++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 39 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a31de13553c..d07d667fbdf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -948,15 +948,8 @@ editing the `conf` file in a text editor. Use the examples as reference.
@@ -969,6 +962,27 @@ editing the `conf` file in a text editor. Use the examples as reference. dd_configuration_option = ensure_only_display @endcode + + + + + + + + + + + + + + + + + + + + +
Description - @warning{Windows only!} - Perform additional configuration for the display device: -
    -
  • `disabled` - perform no additional configuration (disables all `dd_` configuration options).
  • -
  • `verify_only` - verify that display is active only (required for changing display mode and other options).
  • -
  • `ensure_active` - activate the display if it's currently inactive.
  • -
  • `ensure_primary` - activate the display if it's currently inactive and make it primary.
  • -
  • `ensure_only_display` - activate the display if it's currently inactive and disable all others.
  • -
+ Perform mandatory verification and additional configuration for the display device. + @note{Applies to Windows only.}
ChoicesdisabledPerform no additional configuration (disables all `dd_` configuration options).
verify_onlyVerify that display is active only (this is a mandatory step without any extra steps to verify display state).
ensure_activeActivate the display if it's currently inactive.
ensure_primaryActivate the display if it's currently inactive and make it primary.
ensure_only_displayActivate the display if it's currently inactive and disable all others.
### dd_resolution_option @@ -977,14 +991,9 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - @note{"Optimize game settings" must be enabled for this option to work.} - Perform additional resolution configuration for the display device: - + Perform additional resolution configuration for the display device. + @note{"Optimize game settings" must be enabled in Moonlight for this option to work.} + @note{Applies to Windows only.} @@ -997,6 +1006,19 @@ editing the `conf` file in a text editor. Use the examples as reference. dd_resolution_option = manual @endcode + + Choices + disabled + Perform no additional configuration. + + + automatic + Change resolution to the requested resolution from the client. + + + manual + Change resolution to the user specified one (set via [dd_manual_resolution](#dd_manual_resolution)). + ### dd_manual_resolution @@ -1005,14 +1027,14 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - @note{`dd_resolution_option` must be set to `manual`} Specify manual resolution to be used. + @note{[dd_resolution_option](#dd_resolution_option) must be set to `manual`} + @note{Applies to Windows only.} Default - No value + n/a Example @@ -1028,13 +1050,8 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - Perform additional refresh rate configuration for the display device: - + Perform additional refresh rate configuration for the display device. + @note{Applies to Windows only.} @@ -1047,6 +1064,19 @@ editing the `conf` file in a text editor. Use the examples as reference. dd_refresh_rate_option = manual @endcode + + Choices + disabled + Perform no additional configuration. + + + automatic + Change refresh rate to the requested FPS value from the client. + + + manual + Change refresh rate to the user specified one (set via [dd_manual_refresh_rate](#dd_manual_refresh_rate)). + ### dd_manual_refresh_rate @@ -1055,14 +1085,14 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - @note{`dd_refresh_rate_option` must be set to `manual`} Specify manual refresh rate to be used. + @note{[dd_refresh_rate_option](#dd_refresh_rate_option) must be set to `manual`} + @note{Applies to Windows only.} Default - No value + n/a Example @@ -1079,12 +1109,8 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - Perform additional HDR configuration for the display device: - + Perform additional HDR configuration for the display device. + @note{Applies to Windows only.} @@ -1097,6 +1123,15 @@ editing the `conf` file in a text editor. Use the examples as reference. dd_hdr_option = disabled @endcode + + Choices + disabled + Perform no additional configuration. + + + automatic + Change HDR to the requested state from the client if the display supports it. + ### dd_wa_hdr_toggle @@ -1105,10 +1140,10 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} - @note{This option works independently of `dd_hdr_option`} When using virtual display device as for streaming, it might display incorrect (high-contrast) color. With this option enabled, Sunshine will try to mitigate this issue. + @note{This option works independently of [dd_hdr_option](#dd_hdr_option)} + @note{Applies to Windows only.} @@ -1129,9 +1164,9 @@ editing the `conf` file in a text editor. Use the examples as reference. Description - @warning{Windows only!} Additional delay in milliseconds to wait before reverting configuration when the app has been closed or the last session terminated. Main purpose is to provide a smoother transition when quickly switching between apps. + @note{Applies to Windows only.} From 88016a4b2f1c8833ea79fb063e7f77c1021ca208 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 12 Dec 2024 18:22:55 +0200 Subject: [PATCH 4/5] Review fixes: fix invalid option name in web_ui --- src_assets/common/assets/web/config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 3c5ec35304a..9ad4199d375 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -176,8 +176,8 @@

{{ $t('config.configuration') }}

"dd_refresh_rate_option": "automatic", "dd_manual_refresh_rate": "", "dd_hdr_option": "automatic", - "dd_hdr_workaround": false, "dd_config_revert_delay": 3000, + "dd_wa_hdr_toggle": false, "min_fps_factor": 1, }, }, From 8900c15229add28004bbb9985d8d443643380366 Mon Sep 17 00:00:00 2001 From: FrogTheFrog Date: Thu, 12 Dec 2024 21:07:39 +0200 Subject: [PATCH 5/5] Review fixes: checkbox values --- docs/configuration.md | 6 ++++-- src_assets/common/assets/web/config.html | 2 +- .../web/configs/tabs/audiovideo/DisplayDeviceOptions.vue | 2 +- src_assets/common/assets/web/public/assets/locale/en.json | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d07d667fbdf..ce701fce51d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1148,12 +1148,14 @@ editing the `conf` file in a text editor. Use the examples as reference. Default - @code{}false@endcode + @code{} + disabled + @endcode Example @code{} - dd_wa_hdr_toggle = true + dd_wa_hdr_toggle = enabled @endcode diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 9ad4199d375..bbba4146ca4 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -177,7 +177,7 @@

{{ $t('config.configuration') }}

"dd_manual_refresh_rate": "", "dd_hdr_option": "automatic", "dd_config_revert_delay": 3000, - "dd_wa_hdr_toggle": false, + "dd_wa_hdr_toggle": "disabled", "min_fps_factor": 1, }, }, diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue index dcd14db4c2a..936790bda23 100644 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayDeviceOptions.vue @@ -98,7 +98,7 @@ const config = ref(props.config)
- +
{{ $tp('config.dd_wa_hdr_toggle_desc') }}
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 8068ad9a804..d1eb2564f2d 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -173,7 +173,7 @@ "dd_resolution_option_manual_desc": "Enter the resolution to be used", "dd_resolution_option_ogs_desc": "\"Optimize game settings\" option must be enabled on the Moonlight client for this to work.", "dd_wa_hdr_toggle_desc_windows": "When using virtual display device as for streaming, it might display incorrect HDR color. With this option enabled, Sunshine will try to mitigate this issue.", - "dd_wa_hdr_toggle_windows": "Enable high-contrast workaround for HDR (default: off)", + "dd_wa_hdr_toggle_windows": "Enable high-contrast workaround for HDR (default: disabled)", "ds4_back_as_touchpad_click": "Map Back/Select to Touchpad Click", "ds4_back_as_touchpad_click_desc": "When forcing DS4 emulation, map Back/Select to Touchpad Click", "encoder": "Force a Specific Encoder",