mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-02-24 00:39:57 +00:00
feat(display): Configure display device based on user config (#3441)
This commit is contained in:
parent
df0bc3f82f
commit
76bea8acb9
@ -958,6 +958,247 @@ editing the `conf` file in a text editor. Use the examples as reference.
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_configuration_option
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
Perform mandatory verification and additional configuration for the display device.
|
||||
@note{Applies to Windows only.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}verify_only@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_configuration_option = ensure_only_display
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="5">Choices</td>
|
||||
<td>disabled</td>
|
||||
<td>Perform no additional configuration (disables all `dd_` configuration options).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>verify_only</td>
|
||||
<td>Verify that display is active only (this is a mandatory step without any extra steps to verify display state).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ensure_active</td>
|
||||
<td>Activate the display if it's currently inactive.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ensure_primary</td>
|
||||
<td>Activate the display if it's currently inactive and make it primary.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ensure_only_display</td>
|
||||
<td>Activate the display if it's currently inactive and disable all others.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_resolution_option
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
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.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}auto@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_resolution_option = manual
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">Choices</td>
|
||||
<td>disabled</td>
|
||||
<td>Perform no additional configuration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>auto</td>
|
||||
<td>Change resolution to the requested resolution from the client.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>manual</td>
|
||||
<td>Change resolution to the user specified one (set via [dd_manual_resolution](#dd_manual_resolution)).</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_manual_resolution
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
Specify manual resolution to be used.
|
||||
@note{[dd_resolution_option](#dd_resolution_option) must be set to `manual`}
|
||||
@note{Applies to Windows only.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">n/a</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_manual_resolution = 1920x1080
|
||||
@endcode</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_refresh_rate_option
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
Perform additional refresh rate configuration for the display device.
|
||||
@note{Applies to Windows only.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}auto@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_refresh_rate_option = manual
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">Choices</td>
|
||||
<td>disabled</td>
|
||||
<td>Perform no additional configuration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>auto</td>
|
||||
<td>Change refresh rate to the requested FPS value from the client.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>manual</td>
|
||||
<td>Change refresh rate to the user specified one (set via [dd_manual_refresh_rate](#dd_manual_refresh_rate)).</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_manual_refresh_rate
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
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.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">n/a</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_manual_resolution = 120
|
||||
dd_manual_resolution = 59.95
|
||||
@endcode</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_hdr_option
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
Perform additional HDR configuration for the display device.
|
||||
@note{Applies to Windows only.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}auto@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_hdr_option = disabled
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Choices</td>
|
||||
<td>disabled</td>
|
||||
<td>Perform no additional configuration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>auto</td>
|
||||
<td>Change HDR to the requested state from the client if the display supports it.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_wa_hdr_toggle
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
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.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}
|
||||
disabled
|
||||
@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_wa_hdr_toggle = enabled
|
||||
@endcode</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### dd_config_revert_delay
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td colspan="2">
|
||||
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.}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td colspan="2">@code{}3000@endcode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Example</td>
|
||||
<td colspan="2">@code{}
|
||||
dd_config_revert_delay = 1500
|
||||
@endcode</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### min_fps_factor
|
||||
|
||||
<table>
|
||||
|
@ -20,16 +20,6 @@ namespace audio {
|
||||
using opus_t = util::safe_ptr<OpusMSEncoder, opus_multistream_encoder_destroy>;
|
||||
using sample_queue_t = std::shared_ptr<safe::queue_t<std::vector<float>>>;
|
||||
|
||||
struct audio_ctx_t {
|
||||
// We want to change the sink for the first stream only
|
||||
std::unique_ptr<std::atomic_bool> sink_flag;
|
||||
|
||||
std::unique_ptr<platf::audio_control_t> 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<audio_ctx_t>(start_audio_control, stop_audio_control);
|
||||
|
||||
void
|
||||
encodeThread(sample_queue_t samples, config_t config, void *channel_data) {
|
||||
auto packets = mail::man->queue<packet_t>(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<audio_ctx_t>(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;
|
||||
|
44
src/audio.h
44
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<MAX_FLAGS> flags;
|
||||
};
|
||||
|
||||
struct audio_ctx_t {
|
||||
// We want to change the sink for the first stream only
|
||||
std::unique_ptr<std::atomic_bool> sink_flag;
|
||||
|
||||
std::unique_ptr<platf::audio_control_t> control;
|
||||
|
||||
bool restore_sink;
|
||||
platf::sink_t sink;
|
||||
};
|
||||
|
||||
using buffer_t = util::buffer_t<std::uint8_t>;
|
||||
using packet_t = std::pair<void *, buffer_t>;
|
||||
using audio_ctx_ref_t = safe::shared_t<audio_ctx_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
|
||||
|
@ -328,13 +328,65 @@ 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_2_ARG_(str, val) \
|
||||
if (value == #str##sv) return video_t::dd_t::resolution_option_e::val
|
||||
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
|
||||
_CONVERT_(disabled);
|
||||
_CONVERT_2_ARG_(auto, automatic);
|
||||
_CONVERT_(manual);
|
||||
#undef _CONVERT_
|
||||
#undef _CONVERT_2_ARG_
|
||||
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_2_ARG_(str, val) \
|
||||
if (value == #str##sv) return video_t::dd_t::refresh_rate_option_e::val
|
||||
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
|
||||
_CONVERT_(disabled);
|
||||
_CONVERT_2_ARG_(auto, automatic);
|
||||
_CONVERT_(manual);
|
||||
#undef _CONVERT_
|
||||
#undef _CONVERT_2_ARG_
|
||||
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_2_ARG_(str, val) \
|
||||
if (value == #str##sv) return video_t::dd_t::hdr_option_e::val
|
||||
#define _CONVERT_(x) _CONVERT_2_ARG_(x, x)
|
||||
_CONVERT_(disabled);
|
||||
_CONVERT_2_ARG_(auto, automatic);
|
||||
#undef _CONVERT_
|
||||
#undef _CONVERT_2_ARG_
|
||||
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 +437,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 +1017,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 +1089,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<int>::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);
|
||||
|
43
src/config.h
43
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 {
|
||||
|
@ -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;
|
||||
|
@ -6,12 +6,19 @@
|
||||
#include "display_device.h"
|
||||
|
||||
// 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"
|
||||
#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 whose lifetime is managed by `display_device::init()`.
|
||||
* @brief A global for the settings manager interface and other settings whose lifetime is managed by `display_device::init(...)`.
|
||||
*/
|
||||
std::unique_ptr<RetryScheduler<SettingsManagerInterface>> SM_INSTANCE;
|
||||
struct {
|
||||
std::mutex mutex {};
|
||||
std::chrono::milliseconds config_revert_delay { 0 };
|
||||
std::unique_ptr<RetryScheduler<SettingsManagerInterface>> 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<boost::optional<audio_context_t>> context_scheduler { std::make_unique<boost::optional<audio_context_t>>(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<unsigned int>::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> 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<Resolution> &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<FloatingPoint> 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<FloatingPoint> &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<unsigned int>(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<SingleDisplayConfiguration::DevicePreparation>
|
||||
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<rtsp_stream::launch_session_t> 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<unsigned int>(session.width),
|
||||
static_cast<unsigned int>(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 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<rtsp_stream::launch_session_t> 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
|
||||
*/
|
||||
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<unsigned int>(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<rtsp_stream::launch_session_t> 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<HdrState>
|
||||
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<SettingsManagerInterface>
|
||||
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<SettingsManager>(
|
||||
std::make_shared<WinDisplayDevice>(std::make_shared<WinApiLayer>()),
|
||||
nullptr,
|
||||
std::make_unique<PersistentState>(nullptr),
|
||||
WinWorkarounds {});
|
||||
std::make_shared<sunshine_audio_context_t>(),
|
||||
std::make_unique<PersistentState>(
|
||||
std::make_shared<FileSettingsPersistence>(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<platf::deinit_t>
|
||||
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<RetryScheduler<SettingsManagerInterface>>(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<RetryScheduler<SettingsManagerInterface>>(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<deinit_t>();
|
||||
@ -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<SingleDisplayConfiguration>(&result) }; parsed_config) {
|
||||
configure_display(*parsed_config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto *disabled { std::get_if<configuration_disabled_tag_t>(&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<failed_to_parse_tag_t, configuration_disabled_tag_t, SingleDisplayConfiguration>
|
||||
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
|
||||
|
@ -5,24 +5,35 @@
|
||||
#pragma once
|
||||
|
||||
// lib includes
|
||||
#include <display_device/types.h>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
// 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<platf::deinit_t>
|
||||
init();
|
||||
[[nodiscard]] std::unique_ptr<platf::deinit_t>
|
||||
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<rtsp_stream::launch_session_t> 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<rtsp_stream::launch_session_t> 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<SingleDisplayConfiguration>(&result) }; parsed_config) {
|
||||
* configure_display(*config);
|
||||
* }
|
||||
* @examples_end
|
||||
*/
|
||||
[[nodiscard]] std::variant<failed_to_parse_tag_t, configuration_disabled_tag_t, SingleDisplayConfiguration>
|
||||
parse_configuration(const config::video_t &video_config, const rtsp_stream::launch_session_t &session);
|
||||
} // namespace display_device
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<SunshineHTTPS>(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.<xmlattr>.status_code", 503);
|
||||
tree.put("root.<xmlattr>.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
|
||||
|
@ -550,6 +550,14 @@ namespace platf {
|
||||
virtual std::unique_ptr<mic_t>
|
||||
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_t>
|
||||
sink_info() = 0;
|
||||
|
||||
|
@ -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<int>();
|
||||
|
@ -81,6 +81,12 @@ namespace platf {
|
||||
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_t>
|
||||
sink_info() override {
|
||||
sink_t sink;
|
||||
|
@ -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<bool>(matched);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract virtual audio sink information possibly encoded in the sink name.
|
||||
* @param sink The sink name
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -170,6 +170,14 @@
|
||||
"install_steam_audio_drivers": "enabled",
|
||||
"adapter_name": "",
|
||||
"output_name": "",
|
||||
"dd_configuration_option": "verify_only",
|
||||
"dd_resolution_option": "auto",
|
||||
"dd_manual_resolution": "",
|
||||
"dd_refresh_rate_option": "auto",
|
||||
"dd_manual_refresh_rate": "",
|
||||
"dd_hdr_option": "auto",
|
||||
"dd_config_revert_delay": 3000,
|
||||
"dd_wa_hdr_toggle": "disabled",
|
||||
"min_fps_factor": 1,
|
||||
},
|
||||
},
|
||||
|
@ -74,6 +74,11 @@ const config = ref(props.config)
|
||||
:config="config"
|
||||
/>
|
||||
|
||||
<DisplayDeviceOptions
|
||||
:platform="platform"
|
||||
:config="config"
|
||||
/>
|
||||
|
||||
<!-- Display Modes -->
|
||||
<DisplayModesSettings
|
||||
:platform="platform"
|
||||
|
@ -1,45 +1,127 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { $tp } from '../../../platform-i18n'
|
||||
import PlatformLayout from '../../../PlatformLayout.vue'
|
||||
import Checkbox from "../../../Checkbox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
platform: String,
|
||||
config: Object,
|
||||
display_mode_remapping: Array
|
||||
config: Object
|
||||
})
|
||||
|
||||
const config = ref(props.config)
|
||||
const display_mode_remapping = ref(props.display_mode_remapping)
|
||||
|
||||
// TODO: Sample for use in PR #2032
|
||||
function getRemappingType()
|
||||
{
|
||||
// Assuming here that at least one setting is set to "automatic"
|
||||
if (config.value.resolution_change !== 'automatic') {
|
||||
return "refresh_rate_only";
|
||||
}
|
||||
if (config.value.refresh_rate_change !== 'automatic') {
|
||||
return "resolution_only";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function addRemapping(type) {
|
||||
let template = {
|
||||
type: type,
|
||||
received_resolution: "",
|
||||
received_fps: "",
|
||||
final_resolution: "",
|
||||
final_refresh_rate: "",
|
||||
};
|
||||
|
||||
display_mode_remapping.value.push(template);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<!-- TODO: Implement on PR #2032 -->
|
||||
</div>
|
||||
<PlatformLayout :platform="platform">
|
||||
<template #windows>
|
||||
<div class="mb-3 accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#panelsStayOpen-collapseOne">
|
||||
{{ $t('config.dd_options_header') }}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show"
|
||||
aria-labelledby="panelsStayOpen-headingOne">
|
||||
<div class="accordion-body">
|
||||
|
||||
<!-- Configuration option -->
|
||||
<div class="mb-3">
|
||||
<label for="dd_configuration_option" class="form-label">
|
||||
{{ $t('config.dd_config_label') }}
|
||||
</label>
|
||||
<select id="dd_configuration_option" class="form-select" v-model="config.dd_configuration_option">
|
||||
<option value="disabled">{{ $t('_common.disabled') }}</option>
|
||||
<option value="verify_only">{{ $t('config.dd_config_verify_only') }}</option>
|
||||
<option value="ensure_active">{{ $t('config.dd_config_ensure_active') }}</option>
|
||||
<option value="ensure_primary">{{ $t('config.dd_config_ensure_primary') }}</option>
|
||||
<option value="ensure_only_display">{{ $t('config.dd_config_ensure_only_display') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Resolution option -->
|
||||
<div class="mb-3" v-if="config.dd_configuration_option !== 'disabled'">
|
||||
<label for="dd_resolution_option" class="form-label">
|
||||
{{ $t('config.dd_resolution_option') }}
|
||||
</label>
|
||||
<select id="dd_resolution_option" class="form-select" v-model="config.dd_resolution_option">
|
||||
<option value="disabled">{{ $t('config.dd_resolution_option_disabled') }}</option>
|
||||
<option value="auto">{{ $t('config.dd_resolution_option_auto') }}</option>
|
||||
<option value="manual">{{ $t('config.dd_resolution_option_manual') }}</option>
|
||||
</select>
|
||||
<div class="form-text"
|
||||
v-if="config.dd_resolution_option === 'auto' || config.dd_resolution_option === 'manual'">
|
||||
{{ $t('config.dd_resolution_option_ogs_desc') }}
|
||||
</div>
|
||||
|
||||
<!-- Manual resolution -->
|
||||
<div class="mt-2 ps-4" v-if="config.dd_resolution_option === 'manual'">
|
||||
<div class="form-text">
|
||||
{{ $t('config.dd_resolution_option_manual_desc') }}
|
||||
</div>
|
||||
<input type="text" class="form-control" id="dd_manual_resolution" placeholder="2560x1440"
|
||||
v-model="config.dd_manual_resolution" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refresh rate option -->
|
||||
<div class="mb-3" v-if="config.dd_configuration_option !== 'disabled'">
|
||||
<label for="dd_refresh_rate_option" class="form-label">
|
||||
{{ $t('config.dd_refresh_rate_option') }}
|
||||
</label>
|
||||
<select id="dd_refresh_rate_option" class="form-select" v-model="config.dd_refresh_rate_option">
|
||||
<option value="disabled">{{ $t('config.dd_refresh_rate_option_disabled') }}</option>
|
||||
<option value="auto">{{ $t('config.dd_refresh_rate_option_auto') }}</option>
|
||||
<option value="manual">{{ $t('config.dd_refresh_rate_option_manual') }}</option>
|
||||
</select>
|
||||
|
||||
<!-- Manual refresh rate -->
|
||||
<div class="mt-2 ps-4" v-if="config.dd_refresh_rate_option === 'manual'">
|
||||
<div class="form-text">
|
||||
{{ $t('config.dd_refresh_rate_option_manual_desc') }}
|
||||
</div>
|
||||
<input type="text" class="form-control" id="dd_manual_refresh_rate" placeholder="59.9558"
|
||||
v-model="config.dd_manual_refresh_rate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HDR option -->
|
||||
<div class="mb-3" v-if="config.dd_configuration_option !== 'disabled'">
|
||||
<label for="dd_hdr_option" class="form-label">
|
||||
{{ $t('config.dd_hdr_option') }}
|
||||
</label>
|
||||
<select id="dd_hdr_option" class="mb-3 form-select" v-model="config.dd_hdr_option">
|
||||
<option value="disabled">{{ $t('config.dd_hdr_option_disabled') }}</option>
|
||||
<option value="auto">{{ $t('config.dd_hdr_option_auto') }}</option>
|
||||
</select>
|
||||
<!-- HDR toggle -->
|
||||
<Checkbox id="dd_wa_hdr_toggle"
|
||||
locale-prefix="config"
|
||||
v-model="config.dd_wa_hdr_toggle"
|
||||
default="false"
|
||||
></Checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Config revert delay -->
|
||||
<div class="mb-3" v-if="config.dd_configuration_option !== 'disabled'">
|
||||
<label for="dd_config_revert_delay" class="form-label">
|
||||
{{ $t('config.dd_config_revert_delay') }}
|
||||
</label>
|
||||
<input type="text" class="form-control" id="dd_config_revert_delay" placeholder="3000"
|
||||
v-model="config.dd_config_revert_delay" />
|
||||
<div class="form-text">
|
||||
{{ $t('config.dd_config_revert_delay_desc') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #linux>
|
||||
</template>
|
||||
<template #macos>
|
||||
</template>
|
||||
</PlatformLayout>
|
||||
</template>
|
||||
|
@ -152,6 +152,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_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 (default)",
|
||||
"dd_hdr_option": "HDR",
|
||||
"dd_hdr_option_auto": "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_auto": "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_auto": "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": "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": "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",
|
||||
@ -379,6 +403,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",
|
||||
|
@ -75,6 +75,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Reset persistent display device settings -->
|
||||
<div class="card p-2 my-4" v-if="platform === 'windows'">
|
||||
<div class="card-body">
|
||||
<h2 id="dd_reset">{{ $t('troubleshooting.dd_reset') }}</h2>
|
||||
<br>
|
||||
<p style="white-space: pre-line">{{ $t('troubleshooting.dd_reset_desc') }}</p>
|
||||
<div class="alert alert-success" v-if="ddResetStatus === true">
|
||||
{{ $t('troubleshooting.dd_reset_success') }}
|
||||
</div>
|
||||
<div class="alert alert-danger" v-if="ddResetStatus === false">
|
||||
{{ $t('troubleshooting.dd_reset_error') }}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-warning" :disabled="ddResetPressed" @click="ddResetPersistence">
|
||||
{{ $t('troubleshooting.dd_reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Unpair Clients -->
|
||||
<div class="card my-4">
|
||||
<div class="card-body">
|
||||
@ -141,11 +160,14 @@
|
||||
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 @@
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch("/api/config")
|
||||
.then((r) => r.json())
|
||||
.then((r) => {
|
||||
this.platform = r.platform;
|
||||
});
|
||||
|
||||
this.logInterval = setInterval(() => {
|
||||
this.refreshLogs();
|
||||
}, 5000);
|
||||
@ -236,6 +264,18 @@
|
||||
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);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
276
tests/unit/test_display_device.cpp
Normal file
276
tests/unit/test_display_device.cpp
Normal file
@ -0,0 +1,276 @@
|
||||
/**
|
||||
* @file tests/unit/test_display_device.cpp
|
||||
* @brief Test src/display_device.*.
|
||||
*/
|
||||
#include "../tests_common.h"
|
||||
|
||||
#include <src/config.h>
|
||||
#include <src/display_device.h>
|
||||
#include <src/rtsp.h>
|
||||
|
||||
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<unsigned int>::max() };
|
||||
const std::string max_uint_string { std::to_string(std::numeric_limits<unsigned int>::max()) };
|
||||
|
||||
template <class T>
|
||||
struct DisplayDeviceConfigTest: testing::TestWithParam<T> {};
|
||||
} // namespace
|
||||
|
||||
using ParseDeviceId = DisplayDeviceConfigTest<std::pair<std::string, std::string>>;
|
||||
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<display_device::SingleDisplayConfiguration>(result).m_device_id, expected_value);
|
||||
}
|
||||
|
||||
using ParseConfigOption = DisplayDeviceConfigTest<std::pair<config_option_e, std::optional<device_prep_t>>>;
|
||||
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<display_device::SingleDisplayConfiguration>(&result) }; parsed_config) {
|
||||
ASSERT_EQ(parsed_config->m_device_prep, expected_value);
|
||||
}
|
||||
else {
|
||||
ASSERT_EQ(std::get_if<display_device::configuration_disabled_tag_t>(&result) != nullptr, !expected_value);
|
||||
}
|
||||
}
|
||||
|
||||
using ParseHdrOption = DisplayDeviceConfigTest<std::pair<std::pair<hdr_option_e, client_wants_hdr_t>, std::optional<hdr_state_e>>>;
|
||||
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<display_device::SingleDisplayConfiguration>(result).m_hdr_state, expected_value);
|
||||
}
|
||||
|
||||
using ParseResolutionOption = DisplayDeviceConfigTest<std::pair<std::tuple<resolution_option_e, sops_enabled_t, std::variant<client_resolution_t, std::string>>,
|
||||
std::variant<failed_to_parse_resolution_tag_t, no_resolution_tag_t, resolution_t>>>;
|
||||
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<client_resolution_t>(&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<std::string>(input_resolution);
|
||||
session.width = {};
|
||||
session.height = {};
|
||||
}
|
||||
|
||||
const auto result { display_device::parse_configuration(video_config, session) };
|
||||
if (const auto *failed_option { std::get_if<failed_to_parse_resolution_tag_t>(&expected_value) }; failed_option) {
|
||||
EXPECT_NO_THROW(std::get<display_device::failed_to_parse_tag_t>(result));
|
||||
}
|
||||
else {
|
||||
std::optional<resolution_t> expected_resolution;
|
||||
if (const auto *valid_resolution_option { std::get_if<resolution_t>(&expected_value) }; valid_resolution_option) {
|
||||
expected_resolution = *valid_resolution_option;
|
||||
}
|
||||
|
||||
EXPECT_EQ(std::get<display_device::SingleDisplayConfiguration>(result).m_resolution, expected_resolution);
|
||||
}
|
||||
}
|
||||
|
||||
using ParseRefreshRateOption = DisplayDeviceConfigTest<std::pair<std::tuple<refresh_rate_option_e, std::variant<client_fps_t, std::string>>,
|
||||
std::variant<failed_to_parse_refresh_rate_tag_t, no_refresh_rate_tag_t, rational_t>>>;
|
||||
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<unsigned int>(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<client_fps_t>(&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<std::string>(input_refresh_rate);
|
||||
session.fps = {};
|
||||
}
|
||||
|
||||
const auto result { display_device::parse_configuration(video_config, session) };
|
||||
if (const auto *failed_option { std::get_if<failed_to_parse_refresh_rate_tag_t>(&expected_value) }; failed_option) {
|
||||
EXPECT_NO_THROW(std::get<display_device::failed_to_parse_tag_t>(result));
|
||||
}
|
||||
else {
|
||||
std::optional<display_device::FloatingPoint> expected_refresh_rate;
|
||||
if (const auto *valid_refresh_rate_option { std::get_if<rational_t>(&expected_value) }; valid_refresh_rate_option) {
|
||||
expected_refresh_rate = *valid_refresh_rate_option;
|
||||
}
|
||||
|
||||
EXPECT_EQ(std::get<display_device::SingleDisplayConfiguration>(result).m_refresh_rate, expected_refresh_rate);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user