diff --git a/.gitmodules b/.gitmodules
index be1c418a365..412ddca6a92 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 02ec72247c4..0c576426eb9 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 810f8a878fe..27da728b631 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 d4e3354045b..659e3d29d5c 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/src/display_device.cpp b/src/display_device.cpp
new file mode 100644
index 00000000000..f273104bd34
--- /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 00000000000..6562f5a3dcc
--- /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 6d1085630d9..e4057d9eaf9 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 ee580e5db87..88f2627462b 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 c4ded3d5ec4..b9ffc049128 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 6be72233a6e..bc6cf394519 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 c6167886925..3239c68b00c 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 20d22385353..bf039ca85e3 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 21160dfedbc..0f18f9a14dc 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 00000000000..d5f906e7bfa
--- /dev/null
+++ b/src_assets/common/assets/web/configs/tabs/audiovideo/DisplayOutputSelector.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
{{ $tp('config.output_name') }}
+
+
+ {{ $tp('config.output_name_desc') }}
+
+
+
+ {
+ "device_id": "{de9bb7e2-186e-505b-9e93-f48793333810}"
+ "display_name": "\\\\.\\DISPLAY1"
+ "friendly_name": "ROG PG279Q"
+ ...
+ }
+
+
+
+
+ Info: Detecting displays
+ Info: Detected display: DVI-D-0 (id: 0) connected: false
+ Info: Detected display: HDMI-0 (id: 1) connected: true
+ Info: Detected display: DP-0 (id: 2) connected: true
+ Info: Detected display: DP-1 (id: 3) connected: false
+ Info: Detected display: DVI-D-1 (id: 4) connected: false
+
+
+
+
+ Info: Detecting displays
+ Info: Detected display: Monitor-0 (id: 3) connected: true
+ Info: Detected display: Monitor-1 (id: 2) connected: true
+
+
+
+
+
+
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 9dc3b21f609..00000000000
--- a/src_assets/common/assets/web/configs/tabs/audiovideo/LegacyDisplayOutputSelector.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
{{ $tp('config.output_name') }}
-
-
- {{ $tp('config.output_name_desc') }}
-
-
- tools\dxgi-info.exe
-
-
-
- Info: Detecting displays
- Info: Detected display: DVI-D-0 (id: 0) connected: false
- Info: Detected display: HDMI-0 (id: 1) connected: true
- Info: Detected display: DP-0 (id: 2) connected: true
- Info: Detected display: DP-1 (id: 3) connected: false
- Info: Detected display: DVI-D-1 (id: 4) connected: false
-
-
-
-
- Info: Detecting displays
- Info: Detected display: Monitor-0 (id: 3) connected: true
- Info: Detected display: Monitor-1 (id: 2) connected: true
-
-
-
-
-
-
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 d9d79bdccd7..00000000000
--- a/src_assets/common/assets/web/configs/tabs/audiovideo/NewDisplayOutputSelector.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
{{ $tp('config.output_name') }}
-
-
-
{{ $tp('config.output_name_desc') }}
-
-
- DEVICE ID: {de9bb7e2-186e-505b-9e93-f48793333810}
- DISPLAY NAME: \\.\DISPLAY1
- FRIENDLY NAME: ROG PG279Q
- DEVICE STATE: PRIMARY
- HDR STATE: UNKNOWN
-
-
-
-
-
-
-
-
-
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 a41617fe682..b277c8af306 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 00000000000..48ca85e8e7c
--- /dev/null
+++ b/third-party/libdisplaydevice
@@ -0,0 +1 @@
+Subproject commit 48ca85e8e7c3c01fef5db7f0befced7ea21d6238