Support headless mode
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
namespace config {
|
||||
struct video_t {
|
||||
bool headless_mode;
|
||||
// ffmpeg params
|
||||
int qp; // higher == more compression and less quality
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.<xmlattr>.status_code", 503);
|
||||
tree.put("root.<xmlattr>.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.<xmlattr>.status_code", 404);
|
||||
tree.put("root.<xmlattr>.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.<xmlattr>.status_code", err);
|
||||
tree.put("root.<xmlattr>.status_message", "Failed to start the specified application");
|
||||
tree.put(
|
||||
"root.<xmlattr>.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;
|
||||
|
||||
127
src/process.cpp
127
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<rtsp_stream::launch_session_t> launch_session) {
|
||||
proc_t::execute(int app_id, const ctx_t& _app, std::shared_ptr<rtsp_stream::launch_session_t> 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;
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace proc {
|
||||
_apps(std::move(apps)) {}
|
||||
|
||||
int
|
||||
execute(int app_id, std::shared_ptr<rtsp_stream::launch_session_t> launch_session);
|
||||
execute(int app_id, const ctx_t& _app, std::shared_ptr<rtsp_stream::launch_session_t> launch_session);
|
||||
|
||||
/**
|
||||
* @return `_app_id` if a process is running, otherwise returns `0`
|
||||
|
||||
@@ -149,7 +149,7 @@ const config = ref(props.config)
|
||||
|
||||
<!-- Mapping Key AltRight to Key Windows -->
|
||||
<div class="mb-3" v-if="config.keyboard === 'enabled'">
|
||||
<label for="key_rightalt_to_key_win" class="form-label">{{ $t('config.key_rightalt_to_key_win') }}</label>
|
||||
<label for="key_rightalt_to_key_win" class="form-label">{{ $t('config.key_rightalt_to_key_windows') }}</label>
|
||||
<select id="key_rightalt_to_key_win" class="form-select" v-model="config.key_rightalt_to_key_win">
|
||||
<option value="disabled">{{ $t('_common.disabled') }}</option>
|
||||
<option value="enabled">{{ $t('_common.enabled_def') }}</option>
|
||||
|
||||
@@ -35,8 +35,15 @@ const fpsIn = ref("")
|
||||
<div class="form-text">{{ $t('config.min_fps_factor_desc') }}</div>
|
||||
</div>
|
||||
|
||||
<!--headless_mode-->
|
||||
<div class="mb-3">
|
||||
<input type="checkbox" min="1" max="3" class="form-check-input" id="headless_mode" placeholder="1" v-model="config.headless_mode" true-value="enabled" false-value="disabled"/>
|
||||
<label for="qp" class="form-check-label">{{ $t('config.headless_mode') }}</label>
|
||||
<div class="form-text">{{ $t('config.headless_mode_desc') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="alert" :class="[vdisplay === '0' ? 'alert-success' : 'alert-warning']">
|
||||
<label><i class="fa-solid fa-xl fa-circle-info"></i> SudoVDA Driver status: {{currentDriverStatus}}</label>
|
||||
<i class="fa-solid fa-xl fa-circle-info"></i> SudoVDA Driver status: {{currentDriverStatus}}
|
||||
</div>
|
||||
<div class="form-text" v-if="vdisplay !== '0'">Please ensure SudoVDA driver is installed to the latest version and enabled properly.</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 的支持",
|
||||
|
||||
Reference in New Issue
Block a user