Launch apps from web UI
This commit is contained in:
@@ -929,6 +929,66 @@ namespace confighttp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void launchApp(resp_https_t response, req_https_t request) {
|
||||||
|
if (!authenticate(response, request)) return;
|
||||||
|
|
||||||
|
print_req(request);
|
||||||
|
|
||||||
|
pt::ptree outputTree;
|
||||||
|
|
||||||
|
auto g = util::fail_guard([&]() {
|
||||||
|
std::ostringstream data;
|
||||||
|
pt::write_json(data, outputTree);
|
||||||
|
response->write(data.str());
|
||||||
|
});
|
||||||
|
|
||||||
|
auto args = request->parse_query_string();
|
||||||
|
if (
|
||||||
|
args.find("id"s) == std::end(args)
|
||||||
|
) {
|
||||||
|
outputTree.put("status", false);
|
||||||
|
outputTree.put("error", "Missing a required launch parameter");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto idx_str = nvhttp::get_arg(args, "id");
|
||||||
|
auto idx = util::from_view(idx_str);
|
||||||
|
|
||||||
|
const auto& apps = proc::proc.get_apps();
|
||||||
|
|
||||||
|
if (idx >= apps.size()) {
|
||||||
|
BOOST_LOG(error) << "Couldn't find app with index ["sv << idx_str << ']';
|
||||||
|
outputTree.put("status", false);
|
||||||
|
outputTree.put("error", "Cannot find requested application");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& app = apps[idx];
|
||||||
|
auto appid = util::from_view(app.id);
|
||||||
|
|
||||||
|
crypto::named_cert_t named_cert {
|
||||||
|
.name = "",
|
||||||
|
.uuid = http::unique_id,
|
||||||
|
.perm = crypto::PERM::_all,
|
||||||
|
};
|
||||||
|
|
||||||
|
BOOST_LOG(info) << "Launching app ["sv << app.name << "] from web UI"sv;
|
||||||
|
|
||||||
|
auto launch_session = nvhttp::make_launch_session(true, appid, args, &named_cert);
|
||||||
|
auto err = proc::proc.execute(appid, app, launch_session);
|
||||||
|
if (err) {
|
||||||
|
outputTree.put("status", false);
|
||||||
|
outputTree.put("error",
|
||||||
|
err == 503
|
||||||
|
? "Failed to initialize video capture/encoding. Is a display connected and turned on?"
|
||||||
|
: "Failed to start the specified application");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
outputTree.put("status", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
disconnect(resp_https_t response, req_https_t request) {
|
disconnect(resp_https_t response, req_https_t request) {
|
||||||
if (!authenticate(response, request)) return;
|
if (!authenticate(response, request)) return;
|
||||||
@@ -945,7 +1005,7 @@ namespace confighttp {
|
|||||||
pt::write_json(data, outputTree);
|
pt::write_json(data, outputTree);
|
||||||
response->write(data.str());
|
response->write(data.str());
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pt::read_json(ss, inputTree);
|
pt::read_json(ss, inputTree);
|
||||||
std::string uuid = inputTree.get<std::string>("uuid");
|
std::string uuid = inputTree.get<std::string>("uuid");
|
||||||
@@ -1033,6 +1093,7 @@ namespace confighttp {
|
|||||||
server.resource["^/api/clients/update$"]["POST"] = updateClient;
|
server.resource["^/api/clients/update$"]["POST"] = updateClient;
|
||||||
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
|
server.resource["^/api/clients/unpair$"]["POST"] = unpair;
|
||||||
server.resource["^/api/clients/disconnect$"]["POST"] = disconnect;
|
server.resource["^/api/clients/disconnect$"]["POST"] = disconnect;
|
||||||
|
server.resource["^/api/apps/launch$"]["POST"] = launchApp;
|
||||||
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
server.resource["^/api/apps/close$"]["POST"] = closeApp;
|
||||||
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
server.resource["^/api/covers/upload$"]["POST"] = uploadCover;
|
||||||
server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage;
|
server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage;
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ namespace nvhttp {
|
|||||||
client_t client_root;
|
client_t client_root;
|
||||||
std::atomic<uint32_t> session_id_counter;
|
std::atomic<uint32_t> session_id_counter;
|
||||||
|
|
||||||
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
|
||||||
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
|
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Response>;
|
||||||
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
|
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SunshineHTTPS>::Request>;
|
||||||
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
|
using resp_http_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTP>::Response>;
|
||||||
@@ -192,7 +191,7 @@ namespace nvhttp {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
get_arg(const args_t &args, const char *name, const char *default_value = nullptr) {
|
get_arg(const args_t &args, const char *name, const char *default_value) {
|
||||||
auto it = args.find(name);
|
auto it = args.find(name);
|
||||||
if (it == std::end(args)) {
|
if (it == std::end(args)) {
|
||||||
if (default_value != NULL) {
|
if (default_value != NULL) {
|
||||||
@@ -346,30 +345,58 @@ namespace nvhttp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<rtsp_stream::launch_session_t>
|
std::shared_ptr<rtsp_stream::launch_session_t>
|
||||||
make_launch_session(bool host_audio, const args_t &args, const crypto::named_cert_t* named_cert_p) {
|
make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p) {
|
||||||
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
|
auto launch_session = std::make_shared<rtsp_stream::launch_session_t>();
|
||||||
|
|
||||||
launch_session->id = ++session_id_counter;
|
launch_session->id = ++session_id_counter;
|
||||||
|
|
||||||
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
|
// If launched from client
|
||||||
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
|
if (named_cert_p->uuid != http::unique_id) {
|
||||||
|
auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true);
|
||||||
|
std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key));
|
||||||
|
|
||||||
launch_session->host_audio = host_audio;
|
launch_session->host_audio = host_audio;
|
||||||
std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0"));
|
std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0"));
|
||||||
// Split mode by the char "x", to populate width/height/fps
|
// Split mode by the char "x", to populate width/height/fps
|
||||||
int x = 0;
|
int x = 0;
|
||||||
std::string segment;
|
std::string segment;
|
||||||
while (std::getline(mode, segment, 'x')) {
|
while (std::getline(mode, segment, 'x')) {
|
||||||
if (x == 0) launch_session->width = atoi(segment.c_str());
|
if (x == 0) launch_session->width = atoi(segment.c_str());
|
||||||
if (x == 1) launch_session->height = atoi(segment.c_str());
|
if (x == 1) launch_session->height = atoi(segment.c_str());
|
||||||
if (x == 2) launch_session->fps = atoi(segment.c_str());
|
if (x == 2) launch_session->fps = atoi(segment.c_str());
|
||||||
x++;
|
x++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypted RTSP is enabled with client reported corever >= 1
|
||||||
|
auto corever = util::from_view(get_arg(args, "corever", "0"));
|
||||||
|
if (corever >= 1) {
|
||||||
|
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
|
||||||
|
launch_session->gcm_key, false
|
||||||
|
};
|
||||||
|
launch_session->rtsp_iv_counter = 0;
|
||||||
|
}
|
||||||
|
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
|
||||||
|
|
||||||
|
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
|
||||||
|
unsigned char raw_payload[8];
|
||||||
|
RAND_bytes(raw_payload, sizeof(raw_payload));
|
||||||
|
launch_session->av_ping_payload = util::hex_vec(raw_payload);
|
||||||
|
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
|
||||||
|
|
||||||
|
launch_session->iv.resize(16);
|
||||||
|
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
|
||||||
|
auto prepend_iv_p = (uint8_t *) &prepend_iv;
|
||||||
|
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
|
||||||
|
} else {
|
||||||
|
launch_session->width = 0;
|
||||||
|
launch_session->height = 0;
|
||||||
|
launch_session->fps = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name;
|
launch_session->device_name = named_cert_p->name.empty() ? "ApolloDisplay"s : named_cert_p->name;
|
||||||
launch_session->unique_id = named_cert_p->uuid;
|
launch_session->unique_id = named_cert_p->uuid;
|
||||||
launch_session->perm = named_cert_p->perm;
|
launch_session->perm = named_cert_p->perm;
|
||||||
launch_session->appid = util::from_view(get_arg(args, "appid", "unknown"));
|
launch_session->appid = appid;
|
||||||
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
|
launch_session->enable_sops = util::from_view(get_arg(args, "sops", "0"));
|
||||||
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
|
launch_session->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610"));
|
||||||
launch_session->surround_params = (get_arg(args, "surroundParams", ""));
|
launch_session->surround_params = (get_arg(args, "surroundParams", ""));
|
||||||
@@ -378,26 +405,6 @@ namespace nvhttp {
|
|||||||
launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0"));
|
launch_session->virtual_display = util::from_view(get_arg(args, "virtualDisplay", "0"));
|
||||||
launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100"));
|
launch_session->scale_factor = util::from_view(get_arg(args, "scaleFactor", "100"));
|
||||||
|
|
||||||
// Encrypted RTSP is enabled with client reported corever >= 1
|
|
||||||
auto corever = util::from_view(get_arg(args, "corever", "0"));
|
|
||||||
if (corever >= 1) {
|
|
||||||
launch_session->rtsp_cipher = crypto::cipher::gcm_t {
|
|
||||||
launch_session->gcm_key, false
|
|
||||||
};
|
|
||||||
launch_session->rtsp_iv_counter = 0;
|
|
||||||
}
|
|
||||||
launch_session->rtsp_url_scheme = launch_session->rtsp_cipher ? "rtspenc://"s : "rtsp://"s;
|
|
||||||
|
|
||||||
// Generate the unique identifiers for this connection that we will send later during RTSP handshake
|
|
||||||
unsigned char raw_payload[8];
|
|
||||||
RAND_bytes(raw_payload, sizeof(raw_payload));
|
|
||||||
launch_session->av_ping_payload = util::hex_vec(raw_payload);
|
|
||||||
RAND_bytes((unsigned char *) &launch_session->control_connect_data, sizeof(launch_session->control_connect_data));
|
|
||||||
|
|
||||||
launch_session->iv.resize(16);
|
|
||||||
uint32_t prepend_iv = util::endian::big<uint32_t>(util::from_view(get_arg(args, "rikeyid")));
|
|
||||||
auto prepend_iv_p = (uint8_t *) &prepend_iv;
|
|
||||||
std::copy(prepend_iv_p, prepend_iv_p + sizeof(prepend_iv), std::begin(launch_session->iv));
|
|
||||||
return launch_session;
|
return launch_session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,8 +1039,11 @@ namespace nvhttp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto appid_str = get_arg(args, "appid");
|
||||||
|
auto appid = util::from_view(appid_str);
|
||||||
|
|
||||||
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
host_audio = util::from_view(get_arg(args, "localAudioPlayMode"));
|
||||||
auto launch_session = make_launch_session(host_audio, args, named_cert_p);
|
auto launch_session = make_launch_session(host_audio, appid, args, named_cert_p);
|
||||||
|
|
||||||
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
|
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
|
||||||
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
||||||
@@ -1046,9 +1056,6 @@ namespace nvhttp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto appid = util::from_view(get_arg(args, "appid"));
|
|
||||||
auto appid_str = std::to_string(appid);
|
|
||||||
|
|
||||||
if (appid > 0) {
|
if (appid > 0) {
|
||||||
const auto& apps = proc::proc.get_apps();
|
const auto& apps = proc::proc.get_apps();
|
||||||
auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) {
|
auto app_iter = std::find_if(apps.begin(), apps.end(), [&appid_str](const auto _app) {
|
||||||
@@ -1161,7 +1168,7 @@ namespace nvhttp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto launch_session = make_launch_session(host_audio, args, named_cert_p);
|
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());
|
auto encryption_mode = net::encryption_mode_for_address(request->remote_endpoint().address());
|
||||||
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
|
||||||
// lib includes
|
// lib includes
|
||||||
|
#include <Simple-Web-Server/server_https.hpp>
|
||||||
#include <boost/property_tree/ptree.hpp>
|
#include <boost/property_tree/ptree.hpp>
|
||||||
|
|
||||||
// local includes
|
// local includes
|
||||||
@@ -24,6 +25,8 @@ using namespace std::chrono_literals;
|
|||||||
*/
|
*/
|
||||||
namespace nvhttp {
|
namespace nvhttp {
|
||||||
|
|
||||||
|
using args_t = SimpleWeb::CaseInsensitiveMultimap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The protocol version.
|
* @brief The protocol version.
|
||||||
* @details The version of the GameStream protocol we are mocking.
|
* @details The version of the GameStream protocol we are mocking.
|
||||||
@@ -57,6 +60,12 @@ namespace nvhttp {
|
|||||||
void
|
void
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
std::string
|
||||||
|
get_arg(const args_t &args, const char *name, const char *default_value = nullptr);
|
||||||
|
|
||||||
|
std::shared_ptr<rtsp_stream::launch_session_t>
|
||||||
|
make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Compare the user supplied pin to the Moonlight pin.
|
* @brief Compare the user supplied pin to the Moonlight pin.
|
||||||
* @param pin The user supplied pin.
|
* @param pin The user supplied pin.
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) {
|
|||||||
|
|
||||||
result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL);
|
result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL);
|
||||||
if (result != DISP_CHANGE_SUCCESSFUL) {
|
if (result != DISP_CHANGE_SUCCESSFUL) {
|
||||||
|
wprintf(L"[SUDOVDA] Changing config for display %ls failed!\n\n", displayDevice.DeviceName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,10 +99,17 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) {
|
|||||||
primaryDevMode.dmPosition.x = 0;
|
primaryDevMode.dmPosition.x = 0;
|
||||||
primaryDevMode.dmPosition.y = 0;
|
primaryDevMode.dmPosition.y = 0;
|
||||||
primaryDevMode.dmFields = DM_POSITION;
|
primaryDevMode.dmFields = DM_POSITION;
|
||||||
ChangeDisplaySettingsExW(primaryDeviceName, &primaryDevMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET | CDS_SET_PRIMARY, NULL);
|
result = ChangeDisplaySettingsExW(primaryDeviceName, &primaryDevMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET | CDS_SET_PRIMARY, NULL);
|
||||||
|
if (result != DISP_CHANGE_SUCCESSFUL) {
|
||||||
|
wprintf(L"[SUDOVDA] Changing config for primary display %ls failed!\n\n", primaryDeviceName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
wprintf(L"[SUDOVDA] Applying primary display %ls ...\n\n", primaryDeviceName);
|
||||||
|
|
||||||
result = ChangeDisplaySettingsExW(NULL, NULL, NULL, 0, NULL);
|
result = ChangeDisplaySettingsExW(NULL, NULL, NULL, 0, NULL);
|
||||||
if (result != DISP_CHANGE_SUCCESSFUL) {
|
if (result != DISP_CHANGE_SUCCESSFUL) {
|
||||||
|
wprintf(L"[SUDOVDA] Applying display coinfig failed!\n\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,8 +170,8 @@ namespace proc {
|
|||||||
_app_id = app_id;
|
_app_id = app_id;
|
||||||
_launch_session = launch_session;
|
_launch_session = launch_session;
|
||||||
|
|
||||||
uint32_t client_width = launch_session->width;
|
uint32_t client_width = launch_session->width ? launch_session->width : 1920;
|
||||||
uint32_t client_height = launch_session->height;
|
uint32_t client_height = launch_session->height ? launch_session->height : 1080;
|
||||||
|
|
||||||
uint32_t render_width = client_width;
|
uint32_t render_width = client_width;
|
||||||
uint32_t render_height = client_height;
|
uint32_t render_height = client_height;
|
||||||
@@ -215,7 +215,7 @@ namespace proc {
|
|||||||
launch_session->device_name.c_str(),
|
launch_session->device_name.c_str(),
|
||||||
render_width,
|
render_width,
|
||||||
render_height,
|
render_height,
|
||||||
launch_session->fps,
|
launch_session->fps ? launch_session->fps : 60,
|
||||||
launch_session->display_guid
|
launch_session->display_guid
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -223,8 +223,11 @@ namespace proc {
|
|||||||
|
|
||||||
std::wstring currentPrimaryDisplayName = VDISPLAY::getPrimaryDisplay();
|
std::wstring currentPrimaryDisplayName = VDISPLAY::getPrimaryDisplay();
|
||||||
|
|
||||||
// Apply display settings
|
// When launched through config ui, don't change display settings
|
||||||
VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, launch_session->fps);
|
if (launch_session->width && launch_session->height && launch_session->fps) {
|
||||||
|
// Apply display settings
|
||||||
|
VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, launch_session->fps);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if we need to set the virtual display as primary
|
// Determine if we need to set the virtual display as primary
|
||||||
bool shouldSetPrimary = false;
|
bool shouldSetPrimary = false;
|
||||||
@@ -237,11 +240,12 @@ namespace proc {
|
|||||||
|
|
||||||
// Set primary display if needed
|
// Set primary display if needed
|
||||||
if (shouldSetPrimary) {
|
if (shouldSetPrimary) {
|
||||||
VDISPLAY::setPrimaryDisplay(
|
auto disp = (launch_session->virtual_display || _app.virtual_display_primary)
|
||||||
(launch_session->virtual_display || _app.virtual_display_primary)
|
? vdisplayName
|
||||||
? vdisplayName.c_str()
|
: prevPrimaryDisplayName;
|
||||||
: prevPrimaryDisplayName.c_str()
|
BOOST_LOG(info) << "Setting display " << disp << " primary!!!";
|
||||||
);
|
|
||||||
|
VDISPLAY::setPrimaryDisplay(disp.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set virtual_display to true when everything went fine
|
// Set virtual_display to true when everything went fine
|
||||||
@@ -373,7 +377,7 @@ namespace proc {
|
|||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{
|
auto resetHDRThread = std::thread([this, enable_hdr = launch_session->enable_hdr]{
|
||||||
// Windows doesn't seem to be able to set HDR correctly when a display is just connected,
|
// Windows doesn't seem to be able to set HDR correctly when a display is just connected / changed resolution,
|
||||||
// so we have tooggle HDR for the virtual display manually after a delay.
|
// so we have tooggle HDR for the virtual display manually after a delay.
|
||||||
auto retryInterval = 200ms;
|
auto retryInterval = 200ms;
|
||||||
while (is_changing_settings_going_to_fail()) {
|
while (is_changing_settings_going_to_fail()) {
|
||||||
@@ -384,6 +388,7 @@ namespace proc {
|
|||||||
std::this_thread::sleep_for(retryInterval);
|
std::this_thread::sleep_for(retryInterval);
|
||||||
retryInterval *= 2;
|
retryInterval *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should have got the actual streaming display by now
|
// We should have got the actual streaming display by now
|
||||||
std::string currentDisplay = this->display_name;
|
std::string currentDisplay = this->display_name;
|
||||||
if (!currentDisplay.empty()) {
|
if (!currentDisplay.empty()) {
|
||||||
@@ -419,6 +424,10 @@ namespace proc {
|
|||||||
|
|
||||||
fg.disable();
|
fg.disable();
|
||||||
|
|
||||||
|
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
|
||||||
|
system_tray::update_tray_playing(_app.name);
|
||||||
|
#endif
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -778,36 +787,6 @@ namespace proc {
|
|||||||
std::set<std::string> ids;
|
std::set<std::string> ids;
|
||||||
std::vector<proc::ctx_t> apps;
|
std::vector<proc::ctx_t> apps;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
|
|
||||||
proc::ctx_t ctx;
|
|
||||||
ctx.name = "Virtual Display";
|
|
||||||
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
|
|
||||||
ctx.virtual_display = true;
|
|
||||||
ctx.virtual_display_primary = true;
|
|
||||||
ctx.scale_factor = 100;
|
|
||||||
|
|
||||||
ctx.elevated = false;
|
|
||||||
ctx.auto_detach = true;
|
|
||||||
ctx.wait_all = true;
|
|
||||||
ctx.exit_timeout = 5s;
|
|
||||||
|
|
||||||
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
|
||||||
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
|
||||||
// Avoid using index to generate id if possible
|
|
||||||
ctx.id = std::get<0>(possible_ids);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Fallback to include index on collision
|
|
||||||
ctx.id = std::get<1>(possible_ids);
|
|
||||||
}
|
|
||||||
ids.insert(ctx.id);
|
|
||||||
|
|
||||||
apps.emplace_back(std::move(ctx));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (auto &[_, app_node] : apps_node) {
|
for (auto &[_, app_node] : apps_node) {
|
||||||
proc::ctx_t ctx;
|
proc::ctx_t ctx;
|
||||||
|
|
||||||
@@ -915,6 +894,35 @@ namespace proc {
|
|||||||
apps.emplace_back(std::move(ctx));
|
apps.emplace_back(std::move(ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) {
|
||||||
|
proc::ctx_t ctx;
|
||||||
|
ctx.name = "Virtual Display";
|
||||||
|
ctx.image_path = parse_env_val(this_env, "virtual_desktop.png");
|
||||||
|
ctx.virtual_display = true;
|
||||||
|
ctx.virtual_display_primary = true;
|
||||||
|
ctx.scale_factor = 100;
|
||||||
|
|
||||||
|
ctx.elevated = false;
|
||||||
|
ctx.auto_detach = true;
|
||||||
|
ctx.wait_all = true;
|
||||||
|
ctx.exit_timeout = 5s;
|
||||||
|
|
||||||
|
auto possible_ids = calculate_app_id(ctx.name, ctx.image_path, i++);
|
||||||
|
if (ids.count(std::get<0>(possible_ids)) == 0) {
|
||||||
|
// Avoid using index to generate id if possible
|
||||||
|
ctx.id = std::get<0>(possible_ids);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fallback to include index on collision
|
||||||
|
ctx.id = std::get<1>(possible_ids);
|
||||||
|
}
|
||||||
|
ids.insert(ctx.id);
|
||||||
|
|
||||||
|
apps.emplace_back(std::move(ctx));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return proc::proc_t {
|
return proc::proc_t {
|
||||||
std::move(this_env), std::move(apps)
|
std::move(this_env), std::move(apps)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2123,9 +2123,6 @@ namespace stream {
|
|||||||
// If this is the first session, invoke the platform callbacks
|
// If this is the first session, invoke the platform callbacks
|
||||||
if (++running_sessions == 1) {
|
if (++running_sessions == 1) {
|
||||||
platf::streaming_will_start();
|
platf::streaming_will_start();
|
||||||
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
|
|
||||||
system_tray::update_tray_playing(proc::proc.get_last_run_app_name());
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -262,10 +262,10 @@ namespace system_tray {
|
|||||||
|
|
||||||
tray_update(&tray);
|
tray_update(&tray);
|
||||||
tray.icon = TRAY_ICON_PLAYING;
|
tray.icon = TRAY_ICON_PLAYING;
|
||||||
tray.notification_title = "Stream Started";
|
tray.notification_title = "App launched";
|
||||||
char msg[256];
|
char msg[256];
|
||||||
static char force_close_msg[256];
|
static char force_close_msg[256];
|
||||||
snprintf(msg, std::size(msg), "Streaming started for %s", app_name.c_str());
|
snprintf(msg, std::size(msg), "%s launched.", app_name.c_str());
|
||||||
snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str());
|
snprintf(force_close_msg, std::size(force_close_msg), "Force close [%s]", app_name.c_str());
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
|
strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1);
|
||||||
@@ -273,7 +273,7 @@ namespace system_tray {
|
|||||||
#endif
|
#endif
|
||||||
tray.notification_text = msg;
|
tray.notification_text = msg;
|
||||||
tray.notification_icon = TRAY_ICON_PLAYING;
|
tray.notification_icon = TRAY_ICON_PLAYING;
|
||||||
tray.tooltip = msg;
|
tray.tooltip = PROJECT_NAME;
|
||||||
tray.menu[2].text = force_close_msg;
|
tray.menu[2].text = force_close_msg;
|
||||||
tray_update(&tray);
|
tray_update(&tray);
|
||||||
}
|
}
|
||||||
@@ -299,7 +299,7 @@ namespace system_tray {
|
|||||||
tray.notification_title = "Stream Paused";
|
tray.notification_title = "Stream Paused";
|
||||||
tray.notification_text = msg;
|
tray.notification_text = msg;
|
||||||
tray.notification_icon = TRAY_ICON_PAUSING;
|
tray.notification_icon = TRAY_ICON_PAUSING;
|
||||||
tray.tooltip = msg;
|
tray.tooltip = PROJECT_NAME;
|
||||||
tray_update(&tray);
|
tray_update(&tray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,10 +89,13 @@
|
|||||||
<tr v-for="(app,i) in apps" :key="i">
|
<tr v-for="(app,i) in apps" :key="i">
|
||||||
<td>{{app.name}}</td>
|
<td>{{app.name}}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-primary mx-1" @click="editApp(i)">
|
<button class="btn btn-success me-2" :disabled="app.launching" @click="launchApp(i)">
|
||||||
|
<i class="fas fa-play"></i> {{ $t('apps.launch') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary me-2" :disabled="app.launching" @click="editApp(i)">
|
||||||
<i class="fas fa-edit"></i> {{ $t('apps.edit') }}
|
<i class="fas fa-edit"></i> {{ $t('apps.edit') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger mx-1" @click="showDeleteForm(i)">
|
<button class="btn btn-danger" :disabled="app.launching" @click="showDeleteForm(i)">
|
||||||
<i class="fas fa-trash"></i> {{ $t('apps.delete') }}
|
<i class="fas fa-trash"></i> {{ $t('apps.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -407,14 +410,7 @@
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
fetch("/api/apps", {
|
this.loadApps();
|
||||||
credentials: 'include'
|
|
||||||
})
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((r) => {
|
|
||||||
console.log(r);
|
|
||||||
this.apps = r.apps;
|
|
||||||
});
|
|
||||||
|
|
||||||
fetch("/api/config", {
|
fetch("/api/config", {
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
@@ -423,6 +419,15 @@
|
|||||||
.then(r => this.platform = r.platform);
|
.then(r => this.platform = r.platform);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadApps() {
|
||||||
|
fetch("/api/apps", {
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(r => {
|
||||||
|
this.apps = r.apps.map(i => ({...i, launching: false}));
|
||||||
|
});
|
||||||
|
},
|
||||||
newApp() {
|
newApp() {
|
||||||
this.editForm = {
|
this.editForm = {
|
||||||
name: "",
|
name: "",
|
||||||
@@ -443,6 +448,25 @@
|
|||||||
this.editForm.index = -1;
|
this.editForm.index = -1;
|
||||||
this.showEditForm = true;
|
this.showEditForm = true;
|
||||||
},
|
},
|
||||||
|
launchApp(id) {
|
||||||
|
const app = this.apps[id];
|
||||||
|
if (confirm(this.$t('apps.launch_warning'))) {
|
||||||
|
app.launching = true;
|
||||||
|
fetch("/api/apps/launch?id=" + id, {
|
||||||
|
credentials: 'include',
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(r => {
|
||||||
|
if (r.status == "true") {
|
||||||
|
alert(this.$t('apps.launch_success'));
|
||||||
|
} else {
|
||||||
|
alert(this.$t('apps.launch_failed') + r.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => app.launching = false);
|
||||||
|
}
|
||||||
|
},
|
||||||
editApp(id) {
|
editApp(id) {
|
||||||
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
|
this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
|
||||||
this.editForm.index = id;
|
this.editForm.index = id;
|
||||||
|
|||||||
@@ -74,6 +74,10 @@
|
|||||||
"global_prep_name": "Global Prep Commands",
|
"global_prep_name": "Global Prep Commands",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.",
|
"image_desc": "Application icon/picture/image path that will be sent to client. Image must be a PNG file. If not set, Apollo will send default box image.",
|
||||||
|
"launch": "Launch",
|
||||||
|
"launch_warning": "Are you sure you want to launch this app? This will terminate the currently running app.",
|
||||||
|
"launch_success": "App launched successfully!",
|
||||||
|
"launch_failed": "App launch failed: ",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored",
|
"output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored",
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
"global_prep_name": "全局预处理命令",
|
"global_prep_name": "全局预处理命令",
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
"image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置,Apollo 将发送默认图片。",
|
"image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置,Apollo 将发送默认图片。",
|
||||||
|
"launch": "启动",
|
||||||
|
"launch_warning": "确定要启动此应用吗?这将会终止当前已启动的应用。",
|
||||||
|
"launch_success": "应用启动成功!",
|
||||||
|
"launch_failed": "应用启动失败:",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"output_desc": "存储命令输出的文件,如果未指定,输出将被忽略",
|
"output_desc": "存储命令输出的文件,如果未指定,输出将被忽略",
|
||||||
|
|||||||
Reference in New Issue
Block a user