diff --git a/src/config.cpp b/src/config.cpp index 68f975e1..ac3955b8 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -326,6 +326,7 @@ namespace config { } // namespace sw video_t video { + false, // headless_mode 28, // qp 0, // hevc_mode @@ -947,6 +948,7 @@ namespace config { std::cout << "["sv << name << "] -- ["sv << val << ']' << std::endl; } + bool_f(vars, "headless_mode", video.headless_mode); 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 }); diff --git a/src/config.h b/src/config.h index 7989c810..8adcd898 100644 --- a/src/config.h +++ b/src/config.h @@ -15,6 +15,7 @@ namespace config { struct video_t { + bool headless_mode; // ffmpeg params int qp; // higher == more compression and less quality diff --git a/src/main.cpp b/src/main.cpp index 1a1d5ef2..6bfe17f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -280,7 +280,8 @@ main(int argc, char *argv[]) { BOOST_LOG(warning) << "No gamepad input is available"sv; } - if (video::probe_encoders()) { + // Do not probe encoders on startup if headless mode is enabled + if (!config::video.headless_mode && video::probe_encoders()) { BOOST_LOG(error) << "Video failed to find working encoder"sv; } diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 94855109..89bf27fc 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -859,8 +859,6 @@ namespace nvhttp { return; } - auto appid = util::from_view(get_arg(args, "appid")); - auto current_appid = proc::proc.running(); if (current_appid > 0) { tree.put("root.resume", 0); @@ -870,20 +868,6 @@ namespace nvhttp { return; } - // Probe encoders again before streaming to ensure our chosen - // encoder matches the active GPU (which could have changed - // due to hotplugging, driver crash, primary monitor change, - // or any number of other factors). - if (rtsp_stream::session_count() == 0) { - if (video::probe_encoders()) { - tree.put("root..status_code", 503); - tree.put("root..status_message", "Failed to initialize video capture/encoding. Is a display connected and turned on?"); - tree.put("root.gamesession", 0); - - return; - } - } - host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); auto launch_session = make_launch_session(host_audio, args); @@ -898,11 +882,31 @@ namespace nvhttp { return; } + auto appid = util::from_view(get_arg(args, "appid")); + auto appid_str = std::to_string(appid); + if (appid > 0) { - auto err = proc::proc.execute(appid, launch_session); + const auto& apps = proc::proc.get_apps(); + auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) { + return _app.id == appid_str; + }); + + if (app_iter == apps.end()) { + BOOST_LOG(error) << "Couldn't find app with ID ["sv << appid_str << ']'; + tree.put("root..status_code", 404); + tree.put("root..status_message", "Cannot find requested application"); + tree.put("root.gamesession", 0); + return; + } + + auto err = proc::proc.execute(appid, *app_iter, launch_session); if (err) { tree.put("root..status_code", err); - tree.put("root..status_message", "Failed to start the specified application"); + tree.put( + "root..status_message", + err == 503 + ? "Failed to initialize video capture/encoding. Is a display connected and turned on?" + : "Failed to start the specified application"); tree.put("root.gamesession", 0); return; diff --git a/src/process.cpp b/src/process.cpp index e43ec0fe..8d915304 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -28,6 +28,7 @@ #include "httpcommon.h" #include "system_tray.h" #include "utility.h" +#include "video.h" #ifdef _WIN32 // from_utf8() string conversion function @@ -154,23 +155,17 @@ namespace proc { } int - proc_t::execute(int app_id, std::shared_ptr launch_session) { + proc_t::execute(int app_id, const ctx_t& _app, std::shared_ptr launch_session) { // Ensure starting from a clean slate terminate(); - auto iter = std::find_if(_apps.begin(), _apps.end(), [&app_id](const auto app) { - return app.id == std::to_string(app_id); + // Executed when returning from function + auto fg = util::fail_guard([&]() { + terminate(); }); - if (iter == _apps.end()) { - BOOST_LOG(error) << "Couldn't find app with ID ["sv << app_id << ']'; - return 404; - } - _app_id = app_id; - _app = *iter; - _app_prep_begin = std::begin(_app.prep_cmds); - _app_prep_it = _app_prep_begin; + _launch_session = launch_session; uint32_t client_width = launch_session->width; uint32_t client_height = launch_session->height; @@ -193,60 +188,8 @@ namespace proc { render_height &= ~1; } - // Add Stream-specific environment variables - _env["SUNSHINE_APP_ID"] = std::to_string(_app_id); - _env["SUNSHINE_APP_NAME"] = _app.name; - _env["SUNSHINE_CLIENT_UID"] = launch_session->unique_id; - _env["SUNSHINE_CLIENT_NAME"] = launch_session->device_name; - _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); - _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); - _env["SUNSHINE_CLIENT_RENDER_WIDTH"] = std::to_string(launch_session->width); - _env["SUNSHINE_CLIENT_RENDER_HEIGHT"] = std::to_string(launch_session->height); - _env["SUNSHINE_CLIENT_SCALE_FACTOR"] = std::to_string(scale_factor); - _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); - _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; - _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); - _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; - _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; - int channelCount = launch_session->surround_info & (65535); - switch (channelCount) { - case 2: - _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; - break; - case 6: - _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; - break; - case 8: - _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; - break; - } - _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; - - if (!_app.output.empty() && _app.output != "null"sv) { #ifdef _WIN32 - // fopen() interprets the filename as an ANSI string on Windows, so we must convert it - // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. - auto woutput = platf::from_utf8(_app.output); - - // Use _SH_DENYNO to allow us to open this log file again for writing even if it is - // still open from a previous execution. This is required to handle the case of a - // detached process executing again while the previous process is still running. - _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); -#else - _pipe.reset(fopen(_app.output.c_str(), "a")); -#endif - } - - std::error_code ec; - // Executed when returning from function - auto fg = util::fail_guard([&]() { - terminate(); - }); - - _launch_session = launch_session; - -#ifdef _WIN32 - if (launch_session->virtual_display || _app.virtual_display) { + if (config::video.headless_mode || launch_session->virtual_display || _app.virtual_display) { if (vDisplayDriverStatus != VDISPLAY::DRIVER_STATUS::OK) { // Try init driver again initVDisplayDriver(); @@ -302,6 +245,62 @@ namespace proc { } #endif + // Add Stream-specific environment variables + _env["SUNSHINE_APP_ID"] = _app.id; + _env["SUNSHINE_APP_NAME"] = _app.name; + _env["SUNSHINE_CLIENT_UID"] = launch_session->unique_id; + _env["SUNSHINE_CLIENT_NAME"] = launch_session->device_name; + _env["SUNSHINE_CLIENT_WIDTH"] = std::to_string(render_width); + _env["SUNSHINE_CLIENT_HEIGHT"] = std::to_string(render_height); + _env["SUNSHINE_CLIENT_RENDER_WIDTH"] = std::to_string(launch_session->width); + _env["SUNSHINE_CLIENT_RENDER_HEIGHT"] = std::to_string(launch_session->height); + _env["SUNSHINE_CLIENT_SCALE_FACTOR"] = std::to_string(scale_factor); + _env["SUNSHINE_CLIENT_FPS"] = std::to_string(launch_session->fps); + _env["SUNSHINE_CLIENT_HDR"] = launch_session->enable_hdr ? "true" : "false"; + _env["SUNSHINE_CLIENT_GCMAP"] = std::to_string(launch_session->gcmap); + _env["SUNSHINE_CLIENT_HOST_AUDIO"] = launch_session->host_audio ? "true" : "false"; + _env["SUNSHINE_CLIENT_ENABLE_SOPS"] = launch_session->enable_sops ? "true" : "false"; + int channelCount = launch_session->surround_info & (65535); + switch (channelCount) { + case 2: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "2.0"; + break; + case 6: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "5.1"; + break; + case 8: + _env["SUNSHINE_CLIENT_AUDIO_CONFIGURATION"] = "7.1"; + break; + } + _env["SUNSHINE_CLIENT_AUDIO_SURROUND_PARAMS"] = launch_session->surround_params; + + if (!_app.output.empty() && _app.output != "null"sv) { +#ifdef _WIN32 + // fopen() interprets the filename as an ANSI string on Windows, so we must convert it + // to UTF-16 and use the wchar_t variants for proper Unicode log file path support. + auto woutput = platf::from_utf8(_app.output); + + // Use _SH_DENYNO to allow us to open this log file again for writing even if it is + // still open from a previous execution. This is required to handle the case of a + // detached process executing again while the previous process is still running. + _pipe.reset(_wfsopen(woutput.c_str(), L"a", _SH_DENYNO)); +#else + _pipe.reset(fopen(_app.output.c_str(), "a")); +#endif + } + + // Probe encoders again before streaming to ensure our chosen + // encoder matches the active GPU (which could have changed + // due to hotplugging, driver crash, primary monitor change, + // or any number of other factors). + if (video::probe_encoders()) { + return 503; + } + + std::error_code ec; + _app_prep_begin = std::begin(_app.prep_cmds); + _app_prep_it = _app_prep_begin; + for (; _app_prep_it != std::end(_app.prep_cmds); ++_app_prep_it) { auto &cmd = *_app_prep_it; diff --git a/src/process.h b/src/process.h index 199c888f..a23ff9a1 100644 --- a/src/process.h +++ b/src/process.h @@ -89,7 +89,7 @@ namespace proc { _apps(std::move(apps)) {} int - execute(int app_id, std::shared_ptr launch_session); + execute(int app_id, const ctx_t& _app, std::shared_ptr launch_session); /** * @return `_app_id` if a process is running, otherwise returns `0` diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue index a2f0c249..309c0c03 100644 --- a/src_assets/common/assets/web/configs/tabs/Inputs.vue +++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue @@ -149,7 +149,7 @@ const config = ref(props.config)
- + + +
{{ $t('config.headless_mode_desc') }}
+
+
- + SudoVDA Driver status: {{currentDriverStatus}}
Please ensure SudoVDA driver is installed to the latest version and enabled properly.
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 8a6c5d6b..48edd31c 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -183,6 +183,8 @@ "gamepad_xone": "XOne (Xbox One)", "global_prep_cmd": "Command Preparations", "global_prep_cmd_desc": "Configure a list of commands to be executed before or after running any application. If any of the specified preparation commands fail, the application launch process will be aborted.", + "headless_mode": "Headless Mode", + "headless_mode_desc": "Start Apollo in headless mode. When enabled, all apps will start in virtual display.", "hevc_mode": "HEVC Support", "hevc_mode_0": "Apollo will advertise support for HEVC based on encoder capabilities (recommended)", "hevc_mode_1": "Apollo will not advertise support for HEVC", diff --git a/src_assets/common/assets/web/public/assets/locale/zh.json b/src_assets/common/assets/web/public/assets/locale/zh.json index f916693b..03e137c2 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -182,6 +182,8 @@ "gamepad_xone": "Xone (Xbox O1)", "global_prep_cmd": "命令准备工作", "global_prep_cmd_desc": "任何应用运行前/后要运行的命令列表。如果任何前置命令失败,应用的启动过程将被中止。", + "headless_mode": "无头模式", + "headless_mode_desc": "启用后Apollo将支持无显示器模式,所有App都将在虚拟显示器中启动。", "hevc_mode": "HEVC 支持", "hevc_mode_0": "Apollo 将根据编码器能力通告对 HEVC 的支持(推荐)", "hevc_mode_1": "Apollo 将不会通告对 HEVC 的支持",