diff --git a/.gitmodules b/.gitmodules index be1c418a..412ddca6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,10 @@ path = third-party/inputtino url = https://github.com/games-on-whales/inputtino.git branch = stable +[submodule "third-party/libdisplaydevice"] + path = third-party/libdisplaydevice + url = https://github.com/LizardByte/libdisplaydevice.git + branch = master [submodule "third-party/moonlight-common-c"] path = third-party/moonlight-common-c url = https://github.com/moonlight-stream/moonlight-common-c.git diff --git a/cmake/compile_definitions/common.cmake b/cmake/compile_definitions/common.cmake index 02ec7224..0c576426 100644 --- a/cmake/compile_definitions/common.cmake +++ b/cmake/compile_definitions/common.cmake @@ -68,6 +68,8 @@ set(SUNSHINE_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/uuid.h" "${CMAKE_SOURCE_DIR}/src/config.h" "${CMAKE_SOURCE_DIR}/src/config.cpp" + "${CMAKE_SOURCE_DIR}/src/display_device.h" + "${CMAKE_SOURCE_DIR}/src/display_device.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.cpp" "${CMAKE_SOURCE_DIR}/src/entry_handler.h" "${CMAKE_SOURCE_DIR}/src/file_handler.cpp" @@ -146,6 +148,7 @@ list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${MINIUPNP_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} enet + libdisplaydevice::display_device opus ${FFMPEG_LIBRARIES} ${Boost_LIBRARIES} diff --git a/cmake/dependencies/common.cmake b/cmake/dependencies/common.cmake index 810f8a87..27da728b 100644 --- a/cmake/dependencies/common.cmake +++ b/cmake/dependencies/common.cmake @@ -12,6 +12,9 @@ add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/moonlight-common-c/enet") # web server add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/Simple-Web-Server") +# libdisplaydevice +add_subdirectory("${CMAKE_SOURCE_DIR}/third-party/libdisplaydevice") + # common dependencies find_package(OpenSSL REQUIRED) find_package(PkgConfig REQUIRED) diff --git a/docs/configuration.md b/docs/configuration.md index d4e33540..659e3d29 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -865,10 +865,56 @@ editing the `conf` file in a text editor. Use the examples as reference.
**Windows:**
- Enter the following command in command prompt or PowerShell. + During Sunshine startup, you should see the list of detected displays: @code{} - %ProgramFiles%\Sunshine\tools\dxgi-info.exe + Info: Currently available display devices: + [ + { + "device_id": "{64243705-4020-5895-b923-adc862c3457e}", + "display_name": "", + "friendly_name": "IDD HDR", + "info": null + }, + { + "device_id": "{77f67f3e-754f-5d31-af64-ee037e18100a}", + "display_name": "", + "friendly_name": "SunshineHDR", + "info": null + }, + { + "device_id": "{daeac860-f4db-5208-b1f5-cf59444fb768}", + "display_name": "\\\\.\\DISPLAY1", + "friendly_name": "ROG PG279Q", + "info": { + "hdr_state": null, + "origin_point": { + "x": 0, + "y": 0 + }, + "primary": true, + "refresh_rate": { + "type": "rational", + "value": { + "denominator": 1000, + "numerator": 119998 + } + }, + "resolution": { + "height": 1440, + "width": 2560 + }, + "resolution_scale": { + "type": "rational", + "value": { + "denominator": 100, + "numerator": 100 + } + } + } + } + ] @endcode + You need to use the `device_id` value. } @@ -891,7 +937,7 @@ editing the `conf` file in a text editor. Use the examples as reference. Example (Windows) @code{} - output_name = \\.\DISPLAY1 + output_name = {daeac860-f4db-5208-b1f5-cf59444fb768} @endcode diff --git a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml index bead37b8..303f2e7e 100644 --- a/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml +++ b/packaging/linux/flatpak/dev.lizardbyte.app.Sunshine.yml @@ -32,6 +32,9 @@ modules: # Test dependencies - "modules/xvfb/xvfb.json" + # Build dependencies + - "modules/nlohmann_json.json" + # Runtime dependencies - shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json - "modules/avahi.json" diff --git a/packaging/linux/flatpak/modules/nlohmann_json.json b/packaging/linux/flatpak/modules/nlohmann_json.json new file mode 100644 index 00000000..158f8b5b --- /dev/null +++ b/packaging/linux/flatpak/modules/nlohmann_json.json @@ -0,0 +1,15 @@ +{ + "name": "nlohmann_json", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DJSON_MultipleHeaders=OFF", + "-DJSON_BuildTests=OFF" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz", + "sha256": "d6c65aca6b1ed68e7a182f4757257b107ae403032760ed6ef121c9d55e81757d" + } + ] +} diff --git a/src/display_device.cpp b/src/display_device.cpp new file mode 100644 index 00000000..f273104b --- /dev/null +++ b/src/display_device.cpp @@ -0,0 +1,85 @@ +/** + * @file src/display_device.cpp + * @brief Definitions for display device handling. + */ +// header include +#include "display_device.h" + +// lib includes +#include +#include +#include + +// local includes +#include "platform/common.h" + +// platform-specific includes +#ifdef _WIN32 + #include + #include + #include +#endif + +namespace display_device { + namespace { + /** + * @brief A global for the settings manager interface whose lifetime is managed by `display_device::init()`. + */ + std::unique_ptr> SM_INSTANCE; + + /** + * @brief Construct a settings manager interface to manage display device settings. + * @return An interface or nullptr if the OS does not support the interface. + */ + std::unique_ptr + make_settings_manager() { +#ifdef _WIN32 + // TODO: In the upcoming PR, add audio context capture and settings persistence + return std::make_unique( + std::make_shared(std::make_shared()), + nullptr, + std::make_unique(nullptr), + WinWorkarounds {}); +#else + return nullptr; +#endif + } + } // namespace + + std::unique_ptr + init() { + // We can support re-init without any issues, however we should make sure to cleanup first! + SM_INSTANCE = nullptr; + + // If we fail to create settings manager, this means platform is not supported and + // we will need to provided error-free passtrough in other methods + if (auto settings_manager { make_settings_manager() }) { + SM_INSTANCE = std::make_unique>(std::move(settings_manager)); + + const auto available_devices { 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 + } + + class deinit_t: public platf::deinit_t { + public: + ~deinit_t() override { + // TODO: In the upcoming PR, execute recovery once here + SM_INSTANCE = nullptr; + } + }; + return std::make_unique(); + } + + std::string + map_output_name(const std::string &output_name) { + if (!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); }); + } +} // namespace display_device diff --git a/src/display_device.h b/src/display_device.h new file mode 100644 index 00000000..6562f5a3 --- /dev/null +++ b/src/display_device.h @@ -0,0 +1,39 @@ +/** + * @file src/display_device.h + * @brief Declarations for display device handling. + */ +#pragma once + +// lib includes +#include + +// forward declarations +namespace platf { + class deinit_t; +} // namespace platf + +namespace display_device { + /** + * @brief Initialize the implementation and perform the initial state recovery (if needed). + * @returns A deinit_t instance that performs cleanup when destroyed. + * + * @examples + * const auto init_guard { display_device::init() }; + * @examples_end + */ + std::unique_ptr + init(); + + /** + * @brief Map the output name to a specific display. + * @param output_name The user-configurable output name. + * @returns Mapped display name or empty string if the output name could not be mapped. + * + * @examples + * const auto mapped_name_config { map_output_name(config::video.output_name) }; + * const auto mapped_name_custom { map_output_name("{some-device-id}") }; + * @examples_end + */ + std::string + map_output_name(const std::string &output_name); +} // namespace display_device diff --git a/src/logging.cpp b/src/logging.cpp index 6d108563..e4057d9e 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -15,6 +15,7 @@ #include #include #include +#include // local includes #include "logging.h" @@ -106,6 +107,7 @@ namespace logging { } setup_av_logging(min_log_level); + setup_libdisplaydevice_logging(min_log_level); sink = boost::make_shared(); @@ -159,6 +161,37 @@ namespace logging { }); } + void + setup_libdisplaydevice_logging(int min_log_level) { + constexpr int min_level { static_cast(display_device::Logger::LogLevel::verbose) }; + constexpr int max_level { static_cast(display_device::Logger::LogLevel::fatal) }; + const auto log_level { static_cast(std::min(std::max(min_level, min_log_level), max_level)) }; + + display_device::Logger::get().setLogLevel(log_level); + display_device::Logger::get().setCustomCallback([](const display_device::Logger::LogLevel level, const std::string &message) { + switch (level) { + case display_device::Logger::LogLevel::verbose: + BOOST_LOG(verbose) << message; + break; + case display_device::Logger::LogLevel::debug: + BOOST_LOG(debug) << message; + break; + case display_device::Logger::LogLevel::info: + BOOST_LOG(info) << message; + break; + case display_device::Logger::LogLevel::warning: + BOOST_LOG(warning) << message; + break; + case display_device::Logger::LogLevel::error: + BOOST_LOG(error) << message; + break; + case display_device::Logger::LogLevel::fatal: + BOOST_LOG(fatal) << message; + break; + } + }); + } + void log_flush() { if (sink) { diff --git a/src/logging.h b/src/logging.h index ee580e5d..88f26274 100644 --- a/src/logging.h +++ b/src/logging.h @@ -66,6 +66,13 @@ namespace logging { void setup_av_logging(int min_log_level); + /** + * @brief Setup logging for libdisplaydevice. + * @param min_log_level The log level. + */ + void + setup_libdisplaydevice_logging(int min_log_level); + /** * @brief Flush the log. * @examples diff --git a/src/main.cpp b/src/main.cpp index c4ded3d5..b9ffc049 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ // local includes #include "confighttp.h" +#include "display_device.h" #include "entry_handler.h" #include "globals.h" #include "httpcommon.h" @@ -133,6 +134,14 @@ main(int argc, char *argv[]) { return fn->second(argv[0], config::sunshine.cmd.argc, config::sunshine.cmd.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(); + if (!display_device_deinit_guard) { + BOOST_LOG(error) << "Display device session failed to initialize"sv; + } + #ifdef WIN32 // Modify relevant NVIDIA control panel settings if the system has corresponding gpu if (nvprefs_instance.load()) { @@ -230,7 +239,7 @@ main(int argc, char *argv[]) { // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); - on_signal(SIGINT, [&force_shutdown, shutdown_event]() { + on_signal(SIGINT, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Interrupt handler called"sv; auto task = []() { @@ -241,9 +250,10 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard = nullptr; }); - on_signal(SIGTERM, [&force_shutdown, shutdown_event]() { + on_signal(SIGTERM, [&force_shutdown, &display_device_deinit_guard, shutdown_event]() { BOOST_LOG(info) << "Terminate handler called"sv; auto task = []() { @@ -254,6 +264,7 @@ main(int argc, char *argv[]) { force_shutdown = task_pool.pushDelayed(task, 10s).task_id; shutdown_event->raise(true); + display_device_deinit_guard = nullptr; }); #ifdef _WIN32 diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index 6be72233..bc6cf394 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -8,6 +8,7 @@ #include #include +#include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" @@ -541,7 +542,7 @@ const KeyCodeMap kKeyCodesMap[] = { // Default to main display macos_input->display = CGMainDisplayID(); - auto output_name = config::video.output_name; + auto output_name = display_device::map_output_name(config::video.output_name); // If output_name is set, try to find the display with that display id if (!output_name.empty()) { uint32_t max_display = 32; diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index c6167886..3239c68b 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -16,6 +16,7 @@ typedef long NTSTATUS; #include "display.h" #include "misc.h" #include "src/config.h" +#include "src/display_device.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/video.h" @@ -1101,7 +1102,8 @@ namespace platf { BOOST_LOG(debug) << "Detecting monitors..."sv; // We must set the GPU preference before calling any DXGI APIs! - if (!dxgi::probe_for_gpu_preference(config::video.output_name)) { + 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; } diff --git a/src/video.cpp b/src/video.cpp index 20d22385..bf039ca8 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -18,6 +18,7 @@ extern "C" { #include "cbs.h" #include "config.h" +#include "display_device.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -994,6 +995,8 @@ namespace video { */ void refresh_displays(platf::mem_type_e dev_type, std::vector &display_names, int ¤t_display_index) { + // It is possible that the output name may be empty even if it wasn't before (device disconnected) or vice-versa + const auto output_name { display_device::map_output_name(config::video.output_name) }; std::string current_display_name; // If we have a current display index, let's start with that @@ -1012,7 +1015,7 @@ namespace video { return; } else if (display_names.empty()) { - display_names.emplace_back(config::video.output_name); + display_names.emplace_back(output_name); } // We now have a new display name list, so reset the index back to 0 @@ -1032,7 +1035,7 @@ namespace video { } else { for (int x = 0; x < display_names.size(); ++x) { - if (display_names[x] == config::video.output_name) { + if (display_names[x] == output_name) { current_display_index = x; return; } @@ -2346,6 +2349,7 @@ namespace video { bool validate_encoder(encoder_t &encoder, bool expect_failure) { + const auto output_name { display_device::map_output_name(config::video.output_name) }; std::shared_ptr disp; BOOST_LOG(info) << "Trying encoder ["sv << encoder.name << ']'; @@ -2365,7 +2369,7 @@ namespace video { config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0, 0 }; // If the encoder isn't supported at all (not even H.264), bail early - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config_autoselect); + reset_display(disp, encoder.platform_formats->dev_type, output_name, config_autoselect); if (!disp) { return false; } @@ -2473,7 +2477,7 @@ namespace video { const config_t generic_hdr_config = { 1920, 1080, 60, 1000, 1, 0, 3, 1, 1, 0 }; // Reset the display since we're switching from SDR to HDR - reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, generic_hdr_config); + reset_display(disp, encoder.platform_formats->dev_type, output_name, generic_hdr_config); if (!disp) { return false; } @@ -2654,8 +2658,9 @@ namespace video { } if (chosen_encoder == nullptr) { + const auto output_name { display_device::map_output_name(config::video.output_name) }; BOOST_LOG(fatal) << "Unable to find display or encoder during startup."sv; - if (!config::video.adapter_name.empty() || !config::video.output_name.empty()) { + if (!config::video.adapter_name.empty() || !output_name.empty()) { BOOST_LOG(fatal) << "Please ensure your manually chosen GPU and monitor are connected and powered on."sv; } else { diff --git a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue index 21160dfe..0f18f9a1 100644 --- a/src_assets/common/assets/web/configs/tabs/AudioVideo.vue +++ b/src_assets/common/assets/web/configs/tabs/AudioVideo.vue @@ -3,8 +3,7 @@ import {ref} from 'vue' import {$tp} from '../../platform-i18n' import PlatformLayout from '../../PlatformLayout.vue' import AdapterNameSelector from './audiovideo/AdapterNameSelector.vue' -import LegacyDisplayOutputSelector from './audiovideo/LegacyDisplayOutputSelector.vue' -import NewDisplayOutputSelector from './audiovideo/NewDisplayOutputSelector.vue' +import DisplayOutputSelector from './audiovideo/DisplayOutputSelector.vue' import DisplayDeviceOptions from "./audiovideo/DisplayDeviceOptions.vue"; import DisplayModesSettings from "./audiovideo/DisplayModesSettings.vue"; @@ -71,7 +70,7 @@ const config = ref(props.config) :config="config" /> - diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue new file mode 100644 index 00000000..d5f906e7 --- /dev/null +++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue @@ -0,0 +1,53 @@ + + + diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue deleted file mode 100644 index 9dc3b21f..00000000 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/src_assets/common/assets/web/configs/tabs/audiovideo/NewDisplayOutputSelector.vue b/src_assets/common/assets/web/configs/tabs/audiovideo/NewDisplayOutputSelector.vue deleted file mode 100644 index d9d79bdc..00000000 --- a/src_assets/common/assets/web/configs/tabs/audiovideo/NewDisplayOutputSelector.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index a41617fe..b277c8af 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -252,9 +252,9 @@ "origin_web_ui_allowed_pc": "Only localhost may access Web UI", "origin_web_ui_allowed_wan": "Anyone may access Web UI", "output_name_desc_unix": "During Sunshine startup, you should see the list of detected displays. Note: You need to use the id value inside the parenthesis. Below is an example; the actual output can be found in the Troubleshooting tab.", - "output_name_desc_windows": "Manually specify a display to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. The appropriate values can be found using the following command:", + "output_name_desc_windows": "Manually specify a display device id to use for capture. If unset, the primary display is captured. Note: If you specified a GPU above, this display must be connected to that GPU. During Sunshine startup, you should see the list of detected displays. Below is an example; the actual output can be found in the Troubleshooting tab.", "output_name_unix": "Display number", - "output_name_windows": "Output Name", + "output_name_windows": "Display Device Id", "ping_timeout": "Ping Timeout", "ping_timeout_desc": "How long to wait in milliseconds for data from moonlight before shutting down the stream", "pkey": "Private Key", diff --git a/third-party/libdisplaydevice b/third-party/libdisplaydevice new file mode 160000 index 00000000..bbcd179c --- /dev/null +++ b/third-party/libdisplaydevice @@ -0,0 +1 @@ +Subproject commit bbcd179c475eeccf1928b1249da2b663efefc949