Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -260,6 +248,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
|
||||
|
||||
125
src/config.cpp
125
src/config.cpp
@@ -330,6 +330,91 @@ 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
|
||||
}
|
||||
|
||||
video_t::dd_t::mode_remapping_t
|
||||
mode_remapping_from_view(const std::string_view value) {
|
||||
const auto parse_entry_list { [](const auto &entry_list, auto &output_field) {
|
||||
for (auto &[_, entry] : entry_list) {
|
||||
auto requested_resolution = entry.template get_optional<std::string>("requested_resolution"s);
|
||||
auto requested_fps = entry.template get_optional<std::string>("requested_fps"s);
|
||||
auto final_resolution = entry.template get_optional<std::string>("final_resolution"s);
|
||||
auto final_refresh_rate = entry.template get_optional<std::string>("final_refresh_rate"s);
|
||||
|
||||
output_field.push_back(video_t::dd_t::mode_remapping_entry_t {
|
||||
requested_resolution.value_or(""),
|
||||
requested_fps.value_or(""),
|
||||
final_resolution.value_or(""),
|
||||
final_refresh_rate.value_or("") });
|
||||
}
|
||||
} };
|
||||
|
||||
// We need to add a wrapping object to make it valid JSON, otherwise ptree cannot parse it.
|
||||
std::stringstream json_stream;
|
||||
json_stream << "{\"dd_mode_remapping\":" << value << "}";
|
||||
|
||||
boost::property_tree::ptree json_tree;
|
||||
boost::property_tree::read_json(json_stream, json_tree);
|
||||
|
||||
video_t::dd_t::mode_remapping_t output;
|
||||
parse_entry_list(json_tree.get_child("dd_mode_remapping.mixed"), output.mixed);
|
||||
parse_entry_list(json_tree.get_child("dd_mode_remapping.resolution_only"), output.resolution_only);
|
||||
parse_entry_list(json_tree.get_child("dd_mode_remapping.refresh_rate_only"), output.refresh_rate_only);
|
||||
|
||||
return output;
|
||||
}
|
||||
} // namespace dd
|
||||
|
||||
video_t video {
|
||||
false, // headless_mode
|
||||
false, // follow_client_hdr
|
||||
@@ -339,7 +424,6 @@ namespace config {
|
||||
0, // hevc_mode
|
||||
0, // av1_mode
|
||||
|
||||
1, // min_fps_factor
|
||||
2, // min_threads
|
||||
{
|
||||
"superfast"s, // preset
|
||||
@@ -391,6 +475,19 @@ namespace config {
|
||||
{}, // 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
|
||||
{}, // mode_remapping
|
||||
{} // wa
|
||||
}, // display_device
|
||||
|
||||
1 // min_fps_factor
|
||||
"1920x1080x60", // fallback_mode
|
||||
};
|
||||
|
||||
@@ -998,9 +1095,9 @@ namespace config {
|
||||
bool_f(vars, "follow_client_hdr", video.follow_client_hdr);
|
||||
bool_f(vars, "set_vdisplay_primary", video.set_vdisplay_primary);
|
||||
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);
|
||||
@@ -1071,8 +1168,25 @@ namespace config {
|
||||
string_f(vars, "encoder", video.encoder);
|
||||
string_f(vars, "adapter_name", video.adapter_name);
|
||||
string_f(vars, "output_name", video.output_name);
|
||||
string_f(vars, "fallback_mode", video.fallback_mode);
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
generic_f(vars, "dd_mode_remapping", video.dd.mode_remapping, dd::mode_remapping_from_view);
|
||||
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 });
|
||||
string_f(vars, "fallback_mode", video.fallback_mode);
|
||||
|
||||
path_f(vars, "pkey", nvhttp.pkey);
|
||||
path_f(vars, "cert", nvhttp.cert);
|
||||
@@ -1171,6 +1285,7 @@ namespace config {
|
||||
}
|
||||
|
||||
string_restricted_f(vars, "locale", config::sunshine.locale, {
|
||||
"bg"sv, // Bulgarian
|
||||
"de"sv, // German
|
||||
"en"sv, // English
|
||||
"en_GB"sv, // English (UK)
|
||||
@@ -1179,10 +1294,14 @@ namespace config {
|
||||
"fr"sv, // French
|
||||
"it"sv, // Italian
|
||||
"ja"sv, // Japanese
|
||||
"ko"sv, // Korean
|
||||
"pl"sv, // Polish
|
||||
"pt"sv, // Portuguese
|
||||
"pt_BR"sv, // Portuguese (Brazilian)
|
||||
"ru"sv, // Russian
|
||||
"sv"sv, // Swedish
|
||||
"tr"sv, // Turkish
|
||||
"uk"sv, // Ukrainian
|
||||
"zh"sv, // Chinese
|
||||
});
|
||||
|
||||
|
||||
56
src/config.h
56
src/config.h
@@ -24,7 +24,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;
|
||||
@@ -83,6 +82,61 @@ namespace config {
|
||||
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.
|
||||
};
|
||||
|
||||
struct mode_remapping_entry_t {
|
||||
std::string requested_resolution;
|
||||
std::string requested_fps;
|
||||
std::string final_resolution;
|
||||
std::string final_refresh_rate;
|
||||
};
|
||||
|
||||
struct mode_remapping_t {
|
||||
std::vector<mode_remapping_entry_t> mixed; ///< To be used when `resolution_option` and `refresh_rate_option` is set to `automatic`.
|
||||
std::vector<mode_remapping_entry_t> resolution_only; ///< To be use when only `resolution_option` is set to `automatic`.
|
||||
std::vector<mode_remapping_entry_t> refresh_rate_only; ///< To be use when only `refresh_rate_option` is set to `automatic`.
|
||||
};
|
||||
|
||||
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).
|
||||
mode_remapping_t mode_remapping;
|
||||
workarounds_t wa;
|
||||
} dd;
|
||||
|
||||
int min_fps_factor; // Minimum fps target, determines minimum frame time
|
||||
std::string fallback_mode;
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -61,6 +62,10 @@ namespace confighttp {
|
||||
REMOVE ///< Remove client
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Log the request details.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
print_req(const req_https_t &request) {
|
||||
BOOST_LOG(debug) << "METHOD :: "sv << request->method;
|
||||
@@ -79,6 +84,23 @@ namespace confighttp {
|
||||
BOOST_LOG(debug) << " [--] "sv;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a response.
|
||||
* @param response The HTTP response object.
|
||||
* @param output_tree The JSON tree to send.
|
||||
*/
|
||||
void
|
||||
send_response(resp_https_t response, const pt::ptree &output_tree) {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, output_tree);
|
||||
response->write(data.str());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a 401 Unauthorized response.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
send_unauthorized(resp_https_t response, req_https_t request) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
@@ -86,6 +108,12 @@ namespace confighttp {
|
||||
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a redirect response.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* @param path The path to redirect to.
|
||||
*/
|
||||
void
|
||||
send_redirect(resp_https_t response, req_https_t request, const char *path) {
|
||||
auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
|
||||
@@ -174,17 +202,58 @@ namespace confighttp {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a 404 Not Found response.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
not_found(resp_https_t response, req_https_t request) {
|
||||
not_found(resp_https_t response, [[maybe_unused]] req_https_t request) {
|
||||
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_not_found;
|
||||
|
||||
pt::ptree tree;
|
||||
tree.put("root.<xmlattr>.status_code", 404);
|
||||
tree.put("status_code", static_cast<int>(code));
|
||||
tree.put("error", "Not Found");
|
||||
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, tree);
|
||||
|
||||
pt::write_xml(data, tree);
|
||||
response->write(SimpleWeb::StatusCode::client_error_not_found, data.str());
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "application/json");
|
||||
|
||||
response->write(code, data.str(), headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send a 400 Bad Request response.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* @param error_message The error message to include in the response.
|
||||
*/
|
||||
void
|
||||
bad_request(resp_https_t response, [[maybe_unused]] req_https_t request, const std::string &error_message = "Bad Request") {
|
||||
constexpr SimpleWeb::StatusCode code = SimpleWeb::StatusCode::client_error_bad_request;
|
||||
|
||||
pt::ptree tree;
|
||||
tree.put("status_code", static_cast<int>(code));
|
||||
tree.put("status", false);
|
||||
tree.put("error", error_message);
|
||||
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, tree);
|
||||
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", "application/json");
|
||||
|
||||
response->write(code, data.str(), headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the index page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
|
||||
*/
|
||||
void
|
||||
fetchStaticPage(resp_https_t response, req_https_t request, const std::string& page, bool needsAuthenticate) {
|
||||
if (needsAuthenticate) {
|
||||
@@ -201,31 +270,61 @@ namespace confighttp {
|
||||
response->write(content, headers);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Get the PIN page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getIndexPage(resp_https_t response, req_https_t request) {
|
||||
fetchStaticPage(response, request, "index.html", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the apps page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getPinPage(resp_https_t response, req_https_t request) {
|
||||
fetchStaticPage(response, request, "pin.html", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the clients page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getAppsPage(resp_https_t response, req_https_t request) {
|
||||
fetchStaticPage(response, request, "apps.html", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the configuration page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getConfigPage(resp_https_t response, req_https_t request) {
|
||||
fetchStaticPage(response, request, "config.html", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the password page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getPasswordPage(resp_https_t response, req_https_t request) {
|
||||
fetchStaticPage(response, request, "password.html", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the welcome page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getWelcomePage(resp_https_t response, req_https_t request) {
|
||||
if (!checkIPOrigin(response, request)) {
|
||||
@@ -240,6 +339,11 @@ namespace confighttp {
|
||||
fetchStaticPage(response, request, "welcome.html", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the troubleshooting page.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getLoginPage(resp_https_t response, req_https_t request) {
|
||||
if (!checkIPOrigin(response, request)) {
|
||||
@@ -260,6 +364,9 @@ namespace confighttp {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the favicon image.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* @todo combine function with getSunshineLogoImage and possibly getNodeModules
|
||||
* @todo use mime_types map
|
||||
*/
|
||||
@@ -279,6 +386,9 @@ namespace confighttp {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the Sunshine logo image.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* @todo combine function with getFaviconImage and possibly getNodeModules
|
||||
* @todo use mime_types map
|
||||
*/
|
||||
@@ -297,12 +407,23 @@ namespace confighttp {
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a path is a child of another path.
|
||||
* @param base The base path.
|
||||
* @param query The path to check.
|
||||
* @return True if the path is a child of the base path, false otherwise.
|
||||
*/
|
||||
bool
|
||||
isChildPath(fs::path const &base, fs::path const &query) {
|
||||
auto relPath = fs::relative(base, query);
|
||||
return *(relPath.begin()) != fs::path("..");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get an asset from the node_modules directory.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*/
|
||||
void
|
||||
getNodeModules(resp_https_t response, req_https_t request) {
|
||||
if (!checkIPOrigin(response, request)) {
|
||||
@@ -319,32 +440,37 @@ namespace confighttp {
|
||||
// Don't do anything if file does not exist or is outside the assets directory
|
||||
if (!isChildPath(filePath, nodeModulesPath)) {
|
||||
BOOST_LOG(warning) << "Someone requested a path " << filePath << " that is outside the assets folder";
|
||||
response->write(SimpleWeb::StatusCode::client_error_bad_request, "Bad Request");
|
||||
bad_request(response, request);
|
||||
return;
|
||||
}
|
||||
else if (!fs::exists(filePath)) {
|
||||
response->write(SimpleWeb::StatusCode::client_error_not_found);
|
||||
if (!fs::exists(filePath)) {
|
||||
not_found(response, request);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
auto relPath = fs::relative(filePath, webDirPath);
|
||||
// get the mime type from the file extension mime_types map
|
||||
// remove the leading period from the extension
|
||||
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
||||
// check if the extension is in the map at the x position
|
||||
if (mimeType != mime_types.end()) {
|
||||
// if it is, set the content type to the mime type
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", mimeType->second);
|
||||
std::ifstream in(filePath.string(), std::ios::binary);
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
// do not return any file if the type is not in the map
|
||||
|
||||
auto relPath = fs::relative(filePath, webDirPath);
|
||||
// get the mime type from the file extension mime_types map
|
||||
// remove the leading period from the extension
|
||||
auto mimeType = mime_types.find(relPath.extension().string().substr(1));
|
||||
// check if the extension is in the map at the x position
|
||||
if (mimeType == mime_types.end()) {
|
||||
bad_request(response, request);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it is, set the content type to the mime type
|
||||
SimpleWeb::CaseInsensitiveMultimap headers;
|
||||
headers.emplace("Content-Type", mimeType->second);
|
||||
std::ifstream in(filePath.string(), std::ios::binary);
|
||||
response->write(SimpleWeb::StatusCode::success_ok, in, headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the list of available applications.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/apps| GET| null}
|
||||
*/
|
||||
void
|
||||
getApps(resp_https_t response, req_https_t request) {
|
||||
@@ -363,6 +489,8 @@ namespace confighttp {
|
||||
* @brief Get the logs from the log file.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/logs| GET| null}
|
||||
*/
|
||||
void
|
||||
getLogs(resp_https_t response, req_https_t request) {
|
||||
@@ -378,7 +506,7 @@ namespace confighttp {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Save an application. If the application already exists, it will be updated, otherwise it will be added.
|
||||
* @brief Save an application. To save a new application the index must be `-1`. To update an existing application, you must provide the current index of the application.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
* The body for the post request should be JSON serialized in the following format:
|
||||
@@ -407,6 +535,8 @@ namespace confighttp {
|
||||
* "uuid": "C3445C24-871A-FD23-0708-615C121B5B78"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/apps| POST| {"name":"Hello, World!","index":-1}}
|
||||
*/
|
||||
void
|
||||
saveApp(resp_https_t response, req_https_t request) {
|
||||
@@ -417,42 +547,35 @@ namespace confighttp {
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
pt::ptree inputTree, fileTree;
|
||||
|
||||
BOOST_LOG(info) << config::stream.file_apps;
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::ptree fileTree;
|
||||
pt::ptree inputTree;
|
||||
pt::ptree outputTree;
|
||||
pt::read_json(ss, inputTree);
|
||||
pt::read_json(config::stream.file_apps, fileTree);
|
||||
|
||||
proc::migrate_apps(&fileTree, &inputTree);
|
||||
|
||||
pt::write_json(config::stream.file_apps, fileTree);
|
||||
proc::refresh(config::stream.file_apps);
|
||||
|
||||
outputTree.put("status", true);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveApp: "sv << e.what();
|
||||
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid Input JSON");
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Delete an application.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/apps/9999| DELETE| null}
|
||||
*/
|
||||
void
|
||||
deleteApp(resp_https_t response, req_https_t request) {
|
||||
@@ -499,13 +622,8 @@ namespace confighttp {
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "DeleteApp: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", "Invalid File JSON");
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
|
||||
outputTree.put("status", "true");
|
||||
proc::refresh(config::stream.file_apps);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,9 +634,11 @@ namespace confighttp {
|
||||
* @code{.json}
|
||||
* {
|
||||
* "key": "igdb_<game_id>",
|
||||
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.png",
|
||||
* "url": "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/<slug>.png"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/covers/upload| POST| {"key":"igdb_1234","url":"https://images.igdb.com/igdb/image/upload/t_cover_big_2x/abc123.png"}}
|
||||
*/
|
||||
void
|
||||
uploadCover(resp_https_t response, req_https_t request) {
|
||||
@@ -528,31 +648,19 @@ namespace confighttp {
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
SimpleWeb::StatusCode code = SimpleWeb::StatusCode::success_ok;
|
||||
if (outputTree.get_child_optional("error").has_value()) {
|
||||
code = SimpleWeb::StatusCode::client_error_bad_request;
|
||||
}
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(code, data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
pt::read_json(ss, inputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "UploadCover: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
bad_request(response, request, e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
auto key = inputTree.get("key", "");
|
||||
if (key.empty()) {
|
||||
outputTree.put("error", "Cover key is required");
|
||||
bad_request(response, request, "Cover key is required");
|
||||
return;
|
||||
}
|
||||
auto url = inputTree.get("url", "");
|
||||
@@ -563,11 +671,11 @@ namespace confighttp {
|
||||
std::basic_string path = coverdir + http::url_escape(key) + ".png";
|
||||
if (!url.empty()) {
|
||||
if (http::url_get_host(url) != "images.igdb.com") {
|
||||
outputTree.put("error", "Only images.igdb.com is allowed");
|
||||
bad_request(response, request, "Only images.igdb.com is allowed");
|
||||
return;
|
||||
}
|
||||
if (!http::download_file(url, path)) {
|
||||
outputTree.put("error", "Failed to download cover");
|
||||
bad_request(response, request, "Failed to download cover");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -577,13 +685,17 @@ namespace confighttp {
|
||||
std::ofstream imgfile(path);
|
||||
imgfile.write(data.data(), (int) data.size());
|
||||
}
|
||||
outputTree.put("status", true);
|
||||
outputTree.put("path", path);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the configuration settings.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/config| GET| null}
|
||||
*/
|
||||
void
|
||||
getConfig(resp_https_t response, req_https_t request) {
|
||||
@@ -592,14 +704,7 @@ namespace confighttp {
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("status", true);
|
||||
outputTree.put("platform", SUNSHINE_PLATFORM);
|
||||
outputTree.put("version", PROJECT_VER);
|
||||
#ifdef _WIN32
|
||||
@@ -611,12 +716,16 @@ namespace confighttp {
|
||||
for (auto &[name, value] : vars) {
|
||||
outputTree.put(std::move(name), std::move(value));
|
||||
}
|
||||
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get the locale setting. This endpoint does not require authentication.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/configLocale| GET| null}
|
||||
*/
|
||||
void
|
||||
getLocale(resp_https_t response, req_https_t request) {
|
||||
@@ -625,15 +734,9 @@ namespace confighttp {
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.put("status", "true");
|
||||
outputTree.put("status", true);
|
||||
outputTree.put("locale", config::sunshine.locale);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -648,6 +751,8 @@ namespace confighttp {
|
||||
* @endcode
|
||||
*
|
||||
* @attention{It is recommended to ONLY save the config settings that differ from the default behavior.}
|
||||
*
|
||||
* @api_examples{/api/config| POST| {"key":"value"}}
|
||||
*/
|
||||
void
|
||||
saveConfig(resp_https_t response, req_https_t request) {
|
||||
@@ -658,16 +763,10 @@ namespace confighttp {
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
pt::ptree outputTree;
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
pt::ptree inputTree;
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::ptree inputTree;
|
||||
pt::ptree outputTree;
|
||||
pt::read_json(ss, inputTree);
|
||||
for (const auto &kv : inputTree) {
|
||||
std::string value = inputTree.get<std::string>(kv.first);
|
||||
@@ -676,12 +775,12 @@ namespace confighttp {
|
||||
configStream << kv.first << " = " << value << std::endl;
|
||||
}
|
||||
file_handler::write_file(config::sunshine.config_file.c_str(), configStream.str());
|
||||
outputTree.put("status", true);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SaveConfig: "sv << e.what();
|
||||
outputTree.put("status", "false");
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,6 +788,8 @@ namespace confighttp {
|
||||
* @brief Restart Sunshine.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/restart| POST| null}
|
||||
*/
|
||||
void
|
||||
restart(resp_https_t response, req_https_t request) {
|
||||
@@ -729,6 +830,24 @@ namespace confighttp {
|
||||
write_resp.detach();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reset the display device persistence.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/reset-display-device-persistence| POST| null}
|
||||
*/
|
||||
void
|
||||
resetDisplayDevicePersistence(resp_https_t response, req_https_t request) {
|
||||
if (!authenticate(response, request)) return;
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
outputTree.put("status", display_device::reset_persistence());
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Update existing credentials.
|
||||
* @param response The HTTP response object.
|
||||
@@ -743,6 +862,8 @@ namespace confighttp {
|
||||
* "confirmNewPassword": "Confirm New Password"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/password| POST| {"currentUsername":"admin","currentPassword":"admin","newUsername":"admin","newPassword":"admin","confirmNewPassword":"admin"}}
|
||||
*/
|
||||
void
|
||||
savePassword(resp_https_t response, req_https_t request) {
|
||||
@@ -750,20 +871,15 @@ namespace confighttp {
|
||||
|
||||
print_req(request);
|
||||
|
||||
std::vector<std::string> errors = {};
|
||||
std::stringstream ss;
|
||||
std::stringstream configStream;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::ptree inputTree;
|
||||
pt::ptree outputTree;
|
||||
pt::read_json(ss, inputTree);
|
||||
auto username = inputTree.count("currentUsername") > 0 ? inputTree.get<std::string>("currentUsername") : "";
|
||||
auto newUsername = inputTree.get<std::string>("newUsername");
|
||||
@@ -772,15 +888,13 @@ namespace confighttp {
|
||||
auto confirmPassword = inputTree.count("confirmNewPassword") > 0 ? inputTree.get<std::string>("confirmNewPassword") : "";
|
||||
if (newUsername.length() == 0) newUsername = username;
|
||||
if (newUsername.length() == 0) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Username");
|
||||
errors.emplace_back("Invalid Username");
|
||||
}
|
||||
else {
|
||||
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
|
||||
if (config::sunshine.username.empty() || (boost::iequals(username, config::sunshine.username) && hash == config::sunshine.password)) {
|
||||
if (newPassword.empty() || newPassword != confirmPassword) {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Password Mismatch");
|
||||
errors.emplace_back("Password Mismatch");
|
||||
}
|
||||
else {
|
||||
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
|
||||
@@ -793,16 +907,25 @@ namespace confighttp {
|
||||
}
|
||||
}
|
||||
else {
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", "Invalid Current Credentials");
|
||||
errors.emplace_back("Invalid Current Credentials");
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.empty()) {
|
||||
// join the errors array
|
||||
std::string error = std::accumulate(errors.begin(), errors.end(), std::string(),
|
||||
[](const std::string &a, const std::string &b) {
|
||||
return a.empty() ? b : a + ", " + b;
|
||||
});
|
||||
bad_request(response, request, error);
|
||||
return;
|
||||
}
|
||||
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePassword: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,6 +983,8 @@ namespace confighttp {
|
||||
* "name": "Friendly Client Name"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/pin| POST| {"pin":"1234","name":"My PC"}}
|
||||
*/
|
||||
void
|
||||
savePin(resp_https_t response, req_https_t request) {
|
||||
@@ -870,26 +995,19 @@ namespace confighttp {
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::ptree inputTree;
|
||||
pt::ptree outputTree;
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string pin = inputTree.get<std::string>("pin");
|
||||
std::string name = inputTree.get<std::string>("name");
|
||||
outputTree.put("status", nvhttp::pin(pin, name));
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "SavePin: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,6 +1094,8 @@ namespace confighttp {
|
||||
* @brief Unpair all clients.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/clients/unpair-all| POST| null}
|
||||
*/
|
||||
void
|
||||
unpairAll(resp_https_t response, req_https_t request) {
|
||||
@@ -983,16 +1103,12 @@ namespace confighttp {
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
nvhttp::erase_all_clients();
|
||||
proc::proc.terminate();
|
||||
|
||||
pt::ptree outputTree;
|
||||
outputTree.put("status", true);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1005,6 +1121,8 @@ namespace confighttp {
|
||||
* "uuid": "<uuid>"
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @api_examples{/api/unpair| POST| {"uuid":"1234"}}
|
||||
*/
|
||||
void
|
||||
unpair(resp_https_t response, req_https_t request) {
|
||||
@@ -1015,25 +1133,18 @@ namespace confighttp {
|
||||
std::stringstream ss;
|
||||
ss << request->content.rdbuf();
|
||||
|
||||
pt::ptree inputTree, outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
try {
|
||||
// TODO: Input Validation
|
||||
pt::ptree inputTree;
|
||||
pt::ptree outputTree;
|
||||
pt::read_json(ss, inputTree);
|
||||
std::string uuid = inputTree.get<std::string>("uuid");
|
||||
outputTree.put("status", nvhttp::unpair_client(uuid));
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
catch (std::exception &e) {
|
||||
BOOST_LOG(warning) << "Unpair: "sv << e.what();
|
||||
outputTree.put("status", false);
|
||||
outputTree.put("error", e.what());
|
||||
return;
|
||||
bad_request(response, request, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1131,6 +1242,8 @@ namespace confighttp {
|
||||
* @brief Get the list of paired clients.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/clients/list| GET| null}
|
||||
*/
|
||||
void
|
||||
listClients(resp_https_t response, req_https_t request) {
|
||||
@@ -1138,26 +1251,21 @@ namespace confighttp {
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree named_certs = nvhttp::get_all_clients();
|
||||
const pt::ptree named_certs = nvhttp::get_all_clients();
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
outputTree.put("status", false);
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
outputTree.add_child("named_certs", named_certs);
|
||||
outputTree.put("status", true);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Close the currently running application.
|
||||
* @param response The HTTP response object.
|
||||
* @param request The HTTP request object.
|
||||
*
|
||||
* @api_examples{/api/apps/close| POST| null}
|
||||
*/
|
||||
void
|
||||
closeApp(resp_https_t response, req_https_t request) {
|
||||
@@ -1165,16 +1273,11 @@ namespace confighttp {
|
||||
|
||||
print_req(request);
|
||||
|
||||
pt::ptree outputTree;
|
||||
|
||||
auto g = util::fail_guard([&]() {
|
||||
std::ostringstream data;
|
||||
pt::write_json(data, outputTree);
|
||||
response->write(data.str());
|
||||
});
|
||||
|
||||
proc::proc.terminate();
|
||||
|
||||
pt::ptree outputTree;
|
||||
outputTree.put("status", true);
|
||||
send_response(response, outputTree);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1185,6 +1288,18 @@ namespace confighttp {
|
||||
auto address_family = net::af_from_enum_string(config::sunshine.address_family);
|
||||
|
||||
https_server_t server { config::nvhttp.cert, config::nvhttp.pkey };
|
||||
server.default_resource["DELETE"] = [](resp_https_t response, req_https_t request) {
|
||||
bad_request(response, request);
|
||||
};
|
||||
server.default_resource["PATCH"] = [](resp_https_t response, req_https_t request) {
|
||||
bad_request(response, request);
|
||||
};
|
||||
server.default_resource["POST"] = [](resp_https_t response, req_https_t request) {
|
||||
bad_request(response, request);
|
||||
};
|
||||
server.default_resource["PUT"] = [](resp_https_t response, req_https_t request) {
|
||||
bad_request(response, request);
|
||||
};
|
||||
server.default_resource["GET"] = not_found;
|
||||
server.resource["^/$"]["GET"] = getIndexPage;
|
||||
server.resource["^/pin/?$"]["GET"] = getPinPage;
|
||||
@@ -1208,6 +1323,7 @@ namespace confighttp {
|
||||
server.resource["^/api/configLocale$"]["GET"] = getLocale;
|
||||
server.resource["^/api/restart$"]["POST"] = restart;
|
||||
server.resource["^/api/quit$"]["POST"] = quit;
|
||||
server.resource["^/api/reset-display-device-persistence$"]["POST"] = resetDisplayDevicePersistence;
|
||||
server.resource["^/api/password$"]["POST"] = savePassword;
|
||||
server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll;
|
||||
server.resource["^/api/clients/list$"]["GET"] = listClients;
|
||||
|
||||
@@ -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,699 @@
|
||||
|
||||
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) };
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 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.
|
||||
* @param allow_decimal_point Specify whether the decimal point is allowed or not.
|
||||
* @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, const bool allow_decimal_point = true) {
|
||||
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 { allow_decimal_point ? R"(^(\d+)(?:\.(\d+))?$)" : R"(^(\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 (allow_decimal_point && 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 << ". Must have a pattern of " << (allow_decimal_point ? R"("123" or "123.456")" : R"("123")") << "!";
|
||||
}
|
||||
|
||||
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 Indicates which remapping fields and config structure shall be used.
|
||||
*/
|
||||
enum class remapping_type_e {
|
||||
mixed, ///! Both reseolution and refresh rate may be remapped
|
||||
resolution_only, ///! Only resolution will be remapped
|
||||
refresh_rate_only ///! Only refresh rate will be remapped
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Determine the ramapping type from the user config.
|
||||
* @param video_config User's video related configuration.
|
||||
* @returns Enum value if remapping can be performed, null optional if remapping shall be skipped.
|
||||
*/
|
||||
std::optional<remapping_type_e>
|
||||
determine_remapping_type(const config::video_t &video_config) {
|
||||
using dd_t = config::video_t::dd_t;
|
||||
const bool auto_resolution { video_config.dd.resolution_option == dd_t::resolution_option_e::automatic };
|
||||
const bool auto_refresh_rate { video_config.dd.refresh_rate_option == dd_t::refresh_rate_option_e::automatic };
|
||||
|
||||
if (auto_resolution && auto_refresh_rate) {
|
||||
return remapping_type_e::mixed;
|
||||
}
|
||||
|
||||
if (auto_resolution) {
|
||||
return remapping_type_e::resolution_only;
|
||||
}
|
||||
|
||||
if (auto_refresh_rate) {
|
||||
return remapping_type_e::refresh_rate_only;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Contains remapping data parsed from the string values.
|
||||
*/
|
||||
struct parsed_remapping_entry_t {
|
||||
std::optional<Resolution> requested_resolution;
|
||||
std::optional<FloatingPoint> requested_fps;
|
||||
std::optional<Resolution> final_resolution;
|
||||
std::optional<FloatingPoint> final_refresh_rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Check if resolution is to be mapped based on remmaping type.
|
||||
* @param type Remapping type to check.
|
||||
* @returns True if resolution is to be mapped, false otherwise.
|
||||
*/
|
||||
bool
|
||||
is_resolution_mapped(const remapping_type_e type) {
|
||||
return type == remapping_type_e::resolution_only || type == remapping_type_e::mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if FPS is to be mapped based on remmaping type.
|
||||
* @param type Remapping type to check.
|
||||
* @returns True if FPS is to be mapped, false otherwise.
|
||||
*/
|
||||
bool
|
||||
is_fps_mapped(const remapping_type_e type) {
|
||||
return type == remapping_type_e::refresh_rate_only || type == remapping_type_e::mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse the remapping entry from the config into an internal structure.
|
||||
* @param entry Entry to parse.
|
||||
* @param type Specify which entry fields should be parsed.
|
||||
* @returns Parsed structure or null optional if a necessary field could not be parsed.
|
||||
*/
|
||||
std::optional<parsed_remapping_entry_t>
|
||||
parse_remapping_entry(const config::video_t::dd_t::mode_remapping_entry_t &entry, const remapping_type_e type) {
|
||||
parsed_remapping_entry_t result {};
|
||||
|
||||
if (is_resolution_mapped(type) && (!parse_resolution_string(entry.requested_resolution, result.requested_resolution) ||
|
||||
!parse_resolution_string(entry.final_resolution, result.final_resolution))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (is_fps_mapped(type) && (!parse_refresh_rate_string(entry.requested_fps, result.requested_fps, false) ||
|
||||
!parse_refresh_rate_string(entry.final_refresh_rate, result.final_refresh_rate))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Remap the the requested display mode based on the config.
|
||||
* @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 if the remapping was performed or skipped, false if remapping has failed due to invalid config.
|
||||
*
|
||||
* @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 = remap_display_mode_if_needed(video_config, *launch_session, config);
|
||||
* @examples_end
|
||||
*/
|
||||
bool
|
||||
remap_display_mode_if_needed(const config::video_t &video_config, const rtsp_stream::launch_session_t &session, SingleDisplayConfiguration &config) {
|
||||
const auto remapping_type { determine_remapping_type(video_config) };
|
||||
if (!remapping_type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto &remapping_list { [&]() {
|
||||
using enum remapping_type_e;
|
||||
|
||||
switch (*remapping_type) {
|
||||
case resolution_only:
|
||||
return video_config.dd.mode_remapping.resolution_only;
|
||||
case refresh_rate_only:
|
||||
return video_config.dd.mode_remapping.refresh_rate_only;
|
||||
case mixed:
|
||||
default:
|
||||
return video_config.dd.mode_remapping.mixed;
|
||||
}
|
||||
}() };
|
||||
|
||||
if (remapping_list.empty()) {
|
||||
BOOST_LOG(debug) << "No values are available for display mode remapping.";
|
||||
return true;
|
||||
}
|
||||
BOOST_LOG(debug) << "Trying to remap display modes...";
|
||||
|
||||
const auto entry_to_string { [type = *remapping_type](const config::video_t::dd_t::mode_remapping_entry_t &entry) {
|
||||
const bool mapping_resolution { is_resolution_mapped(type) };
|
||||
const bool mapping_fps { is_fps_mapped(type) };
|
||||
|
||||
// clang-format off
|
||||
return (mapping_resolution ? " - requested resolution: "s + entry.requested_resolution + "\n" : "") +
|
||||
(mapping_fps ? " - requested FPS: "s + entry.requested_fps + "\n" : "") +
|
||||
(mapping_resolution ? " - final resolution: "s + entry.final_resolution + "\n" : "") +
|
||||
(mapping_fps ? " - final refresh rate: "s + entry.final_refresh_rate : "");
|
||||
// clang-format on
|
||||
} };
|
||||
|
||||
for (const auto &entry : remapping_list) {
|
||||
const auto parsed_entry { parse_remapping_entry(entry, *remapping_type) };
|
||||
if (!parsed_entry) {
|
||||
BOOST_LOG(error) << "Failed to parse remapping entry from:\n"
|
||||
<< entry_to_string(entry);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parsed_entry->final_resolution && !parsed_entry->final_refresh_rate) {
|
||||
BOOST_LOG(error) << "At least one final value must be set for remapping display modes! Entry:\n"
|
||||
<< entry_to_string(entry);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!session.enable_sops && (parsed_entry->requested_resolution || parsed_entry->final_resolution)) {
|
||||
BOOST_LOG(warning) << R"(Skipping remapping entry, because the "Optimize game settings" is not set in the client! Entry:\n)"
|
||||
<< entry_to_string(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Note: at this point config should already have parsed resolution set.
|
||||
if (parsed_entry->requested_resolution && parsed_entry->requested_resolution != config.m_resolution) {
|
||||
BOOST_LOG(verbose) << "Skipping remapping because requested resolutions do not match! Entry:\n"
|
||||
<< entry_to_string(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Note: at this point config should already have parsed refresh rate set.
|
||||
if (parsed_entry->requested_fps && parsed_entry->requested_fps != config.m_refresh_rate) {
|
||||
BOOST_LOG(verbose) << "Skipping remapping because requested FPS do not match! Entry:\n"
|
||||
<< entry_to_string(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Remapping requested display mode. Entry:\n"
|
||||
<< entry_to_string(entry);
|
||||
if (parsed_entry->final_resolution) {
|
||||
config.m_resolution = parsed_entry->final_resolution;
|
||||
}
|
||||
if (parsed_entry->final_refresh_rate) {
|
||||
config.m_refresh_rate = parsed_entry->final_refresh_rate;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 +729,99 @@ 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 {};
|
||||
}
|
||||
|
||||
if (!remap_display_mode_if_needed(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
|
||||
|
||||
@@ -139,7 +139,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"
|
||||
@@ -985,12 +986,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 named_cert_p = get_verified_cert(request);
|
||||
@@ -1078,6 +1084,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
|
||||
@@ -1124,7 +1133,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"));
|
||||
}
|
||||
auto launch_session = make_launch_session(host_audio, 0, args, named_cert_p);
|
||||
|
||||
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,
|
||||
@@ -1136,17 +1159,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, 0, args, named_cert_p);
|
||||
|
||||
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;
|
||||
@@ -1203,6 +1217,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>();
|
||||
|
||||
@@ -1685,7 +1685,7 @@ namespace platf {
|
||||
BOOST_LOG((window_system != window_system_e::X11 || config::video.capture == "kms") ? fatal : error)
|
||||
<< "You must run [sudo setcap cap_sys_admin+p $(readlink -f $(which sunshine))] for KMS display capture to work!\n"sv
|
||||
<< "If you installed from AppImage or Flatpak, please refer to the official documentation:\n"sv
|
||||
<< "https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/setup.html#install"sv;
|
||||
<< "https://docs.lizardbyte.dev/projects/sunshine/latest/md_docs_2getting__started.html#linux"sv;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
}
|
||||
|
||||
+ (NSString *)getDisplayName:(CGDirectDisplayID)displayID {
|
||||
NSScreen *screens = [NSScreen screens];
|
||||
for (NSScreen *screen in screens) {
|
||||
for (NSScreen *screen in [NSScreen screens]) {
|
||||
if (screen.deviceDescription[@"NSScreenNumber"] == [NSNumber numberWithUnsignedInt:displayID]) {
|
||||
return screen.localizedName;
|
||||
}
|
||||
|
||||
@@ -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,7 +23,7 @@ namespace platf::dxgi {
|
||||
|
||||
// Add D3D11_CREATE_DEVICE_DEBUG here to enable the D3D11 debug runtime.
|
||||
// You should have a debugger like WinDbg attached to receive debug messages.
|
||||
auto constexpr D3D11_CREATE_DEVICE_FLAGS = D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
|
||||
auto constexpr D3D11_CREATE_DEVICE_FLAGS = 0;
|
||||
|
||||
template <class T>
|
||||
void
|
||||
|
||||
@@ -9,10 +9,22 @@
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/process/v1.hpp>
|
||||
|
||||
#include <MinHook.h>
|
||||
|
||||
// We have to include boost/process/v1.hpp before display.h due to WinSock.h,
|
||||
// but that prevents the definition of NTSTATUS so we must define it ourself.
|
||||
typedef long NTSTATUS;
|
||||
|
||||
// Definition from the WDK's d3dkmthk.h
|
||||
typedef enum _D3DKMT_GPU_PREFERENCE_QUERY_STATE: DWORD {
|
||||
D3DKMT_GPU_PREFERENCE_STATE_UNINITIALIZED, ///< The GPU preference isn't initialized.
|
||||
D3DKMT_GPU_PREFERENCE_STATE_HIGH_PERFORMANCE, ///< The highest performing GPU is preferred.
|
||||
D3DKMT_GPU_PREFERENCE_STATE_MINIMUM_POWER, ///< The minimum-powered GPU is preferred.
|
||||
D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, ///< A GPU preference isn't specified.
|
||||
D3DKMT_GPU_PREFERENCE_STATE_NOT_FOUND, ///< A GPU preference isn't found.
|
||||
D3DKMT_GPU_PREFERENCE_STATE_USER_SPECIFIED_GPU ///< A specific GPU is preferred.
|
||||
} D3DKMT_GPU_PREFERENCE_QUERY_STATE;
|
||||
|
||||
#include "display.h"
|
||||
#include "misc.h"
|
||||
#include "src/config.h"
|
||||
@@ -329,111 +341,6 @@ namespace platf::dxgi {
|
||||
return capture_e::ok;
|
||||
}
|
||||
|
||||
bool
|
||||
set_gpu_preference_on_self(int preference) {
|
||||
// The GPU preferences key uses app path as the value name.
|
||||
WCHAR sunshine_path[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, sunshine_path, ARRAYSIZE(sunshine_path));
|
||||
|
||||
WCHAR value_data[128];
|
||||
swprintf_s(value_data, L"GpuPreference=%d;", preference);
|
||||
|
||||
auto status = RegSetKeyValueW(HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
sunshine_path,
|
||||
REG_SZ,
|
||||
value_data,
|
||||
(wcslen(value_data) + 1) * sizeof(WCHAR));
|
||||
if (status != ERROR_SUCCESS) {
|
||||
BOOST_LOG(error) << "Failed to set GPU preference: "sv << status;
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "Set GPU preference: "sv << preference;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
validate_and_test_gpu_preference(const std::string &display_name, bool verify_frame_capture) {
|
||||
std::string cmd = "tools\\ddprobe.exe";
|
||||
|
||||
// We start at 1 because 0 is automatic selection which can be overridden by
|
||||
// the GPU driver control panel options. Since ddprobe.exe can have different
|
||||
// GPU driver overrides than Sunshine.exe, we want to avoid a scenario where
|
||||
// autoselection might work for ddprobe.exe but not for us.
|
||||
for (int i = 1; i < 5; i++) {
|
||||
// Run the probe tool. It returns the status of DuplicateOutput().
|
||||
//
|
||||
// Arg format: [GPU preference] [Display name] [--verify-frame-capture]
|
||||
HRESULT result;
|
||||
std::vector<std::string> args = { std::to_string(i), display_name };
|
||||
try {
|
||||
if (verify_frame_capture) {
|
||||
args.emplace_back("--verify-frame-capture");
|
||||
}
|
||||
result = bp::system(cmd, bp::args(args), bp::std_out > bp::null, bp::std_err > bp::null);
|
||||
}
|
||||
catch (bp::process_error &e) {
|
||||
BOOST_LOG(error) << "Failed to start ddprobe.exe: "sv << e.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "ddprobe.exe " << boost::algorithm::join(args, " ") << " returned 0x"
|
||||
<< util::hex(result).to_string_view();
|
||||
|
||||
// E_ACCESSDENIED can happen at the login screen. If we get this error,
|
||||
// we know capture would have been supported, because DXGI_ERROR_UNSUPPORTED
|
||||
// would have been raised first if it wasn't.
|
||||
if (result == S_OK || result == E_ACCESSDENIED) {
|
||||
// We found a working GPU preference, so set ourselves to use that.
|
||||
if (set_gpu_preference_on_self(i)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid configuration was found, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// On hybrid graphics systems, Windows will change the order of GPUs reported by
|
||||
// DXGI in accordance with the user's GPU preference. If the selected GPU is a
|
||||
// render-only device with no displays, DXGI will add virtual outputs to the
|
||||
// that device to avoid confusing applications. While this works properly for most
|
||||
// applications, it breaks the Desktop Duplication API because DXGI doesn't proxy
|
||||
// the virtual DXGIOutput to the real GPU it is attached to. When trying to call
|
||||
// DuplicateOutput() on one of these virtual outputs, it fails with DXGI_ERROR_UNSUPPORTED
|
||||
// (even if you try sneaky stuff like passing the ID3D11Device for the iGPU and the
|
||||
// virtual DXGIOutput from the dGPU). Because the GPU preference is once-per-process,
|
||||
// we spawn a helper tool to probe for us before we set our own GPU preference.
|
||||
bool
|
||||
probe_for_gpu_preference(const std::string &display_name) {
|
||||
static bool set_gpu_preference = false;
|
||||
|
||||
// If we've already been through here, there's nothing to do this time.
|
||||
if (set_gpu_preference) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try probing with different GPU preferences and verify_frame_capture flag
|
||||
if (validate_and_test_gpu_preference(display_name, true)) {
|
||||
set_gpu_preference = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If no valid configuration was found, try again with verify_frame_capture == false
|
||||
if (validate_and_test_gpu_preference(display_name, false)) {
|
||||
set_gpu_preference = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If neither worked, return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Tests to determine if the Desktop Duplication API can capture the given output.
|
||||
* @details When testing for enumeration only, we avoid resyncing the thread desktop.
|
||||
@@ -506,6 +413,27 @@ namespace platf::dxgi {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Hook for NtGdiDdDDIGetCachedHybridQueryValue() from win32u.dll.
|
||||
* @param gpuPreference A pointer to the location where the preference will be written.
|
||||
* @return Always STATUS_SUCCESS if valid arguments are provided.
|
||||
*/
|
||||
NTSTATUS
|
||||
__stdcall NtGdiDdDDIGetCachedHybridQueryValueHook(D3DKMT_GPU_PREFERENCE_QUERY_STATE *gpuPreference) {
|
||||
// By faking a cached GPU preference state of D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED, this will
|
||||
// prevent DXGI from performing the normal GPU preference resolution that looks at the registry,
|
||||
// power settings, and the hybrid adapter DDI interface to pick a GPU. Instead, we will not be
|
||||
// bound to any specific GPU. This will prevent DXGI from performing output reparenting (moving
|
||||
// outputs from their true location to the render GPU), which breaks DDA.
|
||||
if (gpuPreference) {
|
||||
*gpuPreference = D3DKMT_GPU_PREFERENCE_STATE_UNSPECIFIED;
|
||||
return 0; // STATUS_SUCCESS
|
||||
}
|
||||
else {
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
display_base_t::init(const ::video::config_t &config, const std::string &display_name) {
|
||||
std::once_flag windows_cpp_once_flag;
|
||||
@@ -515,13 +443,22 @@ namespace platf::dxgi {
|
||||
|
||||
typedef BOOL (*User32_SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT value);
|
||||
|
||||
auto user32 = LoadLibraryA("user32.dll");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
if (f) {
|
||||
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
{
|
||||
auto user32 = LoadLibraryA("user32.dll");
|
||||
auto f = (User32_SetProcessDpiAwarenessContext) GetProcAddress(user32, "SetProcessDpiAwarenessContext");
|
||||
if (f) {
|
||||
f(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
FreeLibrary(user32);
|
||||
}
|
||||
|
||||
FreeLibrary(user32);
|
||||
{
|
||||
// We aren't calling MH_Uninitialize(), but that's okay because this hook lasts for the life of the process
|
||||
MH_Initialize();
|
||||
MH_CreateHookApi(L"win32u.dll", "NtGdiDdDDIGetCachedHybridQueryValue", (void *) NtGdiDdDDIGetCachedHybridQueryValueHook, nullptr);
|
||||
MH_EnableHook(MH_ALL_HOOKS);
|
||||
}
|
||||
});
|
||||
|
||||
// Get rectangle of full desktop for absolute mouse coordinates
|
||||
@@ -530,11 +467,6 @@ namespace platf::dxgi {
|
||||
|
||||
HRESULT status;
|
||||
|
||||
// We must set the GPU preference before calling any DXGI APIs!
|
||||
if (!probe_for_gpu_preference(display_name)) {
|
||||
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
|
||||
}
|
||||
|
||||
status = CreateDXGIFactory1(IID_IDXGIFactory1, (void **) &factory);
|
||||
if (FAILED(status)) {
|
||||
BOOST_LOG(error) << "Failed to create DXGIFactory1 [0x"sv << util::hex(status).to_string_view() << ']';
|
||||
@@ -1101,12 +1033,6 @@ namespace platf {
|
||||
|
||||
BOOST_LOG(debug) << "Detecting monitors..."sv;
|
||||
|
||||
// We must set the GPU preference before calling any DXGI APIs!
|
||||
const auto output_name { display_device::map_output_name(config::video.output_name) };
|
||||
if (!dxgi::probe_for_gpu_preference(output_name)) {
|
||||
BOOST_LOG(warning) << "Failed to set GPU preference. Capture may not work!"sv;
|
||||
}
|
||||
|
||||
// We sync the thread desktop once before we start the enumeration process
|
||||
// to ensure test_dxgi_duplication() returns consistent results for all GPUs
|
||||
// even if the current desktop changes during our enumeration process.
|
||||
|
||||
@@ -760,7 +760,7 @@ namespace platf::dxgi {
|
||||
adapter_p,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
nullptr,
|
||||
D3D11_CREATE_DEVICE_FLAGS,
|
||||
D3D11_CREATE_DEVICE_FLAGS | D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
|
||||
featureLevels, sizeof(featureLevels) / sizeof(D3D_FEATURE_LEVEL),
|
||||
D3D11_SDK_VERSION,
|
||||
&device,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "display_device.h"
|
||||
#include "logging.h"
|
||||
#include "platform/common.h"
|
||||
#include "httpcommon.h"
|
||||
@@ -559,16 +560,18 @@ namespace proc {
|
||||
}
|
||||
#endif
|
||||
|
||||
#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;
|
||||
display_name.clear();
|
||||
initial_hdr = false;
|
||||
|
||||
@@ -21,6 +21,7 @@ extern "C" {
|
||||
|
||||
#include "config.h"
|
||||
#include "crypto.h"
|
||||
#include "display_device.h"
|
||||
#include "globals.h"
|
||||
#include "input.h"
|
||||
#include "logging.h"
|
||||
@@ -2077,11 +2078,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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user