diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 9c87307a..242d653f 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -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 disconnect(resp_https_t response, req_https_t request) { if (!authenticate(response, request)) return; @@ -945,7 +1005,7 @@ namespace confighttp { pt::write_json(data, outputTree); response->write(data.str()); }); - + try { pt::read_json(ss, inputTree); std::string uuid = inputTree.get("uuid"); @@ -1033,6 +1093,7 @@ namespace confighttp { server.resource["^/api/clients/update$"]["POST"] = updateClient; server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/disconnect$"]["POST"] = disconnect; + server.resource["^/api/apps/launch$"]["POST"] = launchApp; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/images/apollo.ico$"]["GET"] = getFaviconImage; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 7949dc8f..427d03d9 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -180,7 +180,6 @@ namespace nvhttp { client_t client_root; std::atomic session_id_counter; - using args_t = SimpleWeb::CaseInsensitiveMultimap; using resp_https_t = std::shared_ptr::Response>; using req_https_t = std::shared_ptr::Request>; using resp_http_t = std::shared_ptr::Response>; @@ -192,7 +191,7 @@ namespace nvhttp { }; 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); if (it == std::end(args)) { if (default_value != NULL) { @@ -346,30 +345,58 @@ namespace nvhttp { } std::shared_ptr - 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(); launch_session->id = ++session_id_counter; - auto rikey = util::from_hex_vec(get_arg(args, "rikey"), true); - std::copy(rikey.cbegin(), rikey.cend(), std::back_inserter(launch_session->gcm_key)); + // If launched from client + 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; - std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); - // Split mode by the char "x", to populate width/height/fps - int x = 0; - std::string segment; - while (std::getline(mode, segment, 'x')) { - if (x == 0) launch_session->width = atoi(segment.c_str()); - if (x == 1) launch_session->height = atoi(segment.c_str()); - if (x == 2) launch_session->fps = atoi(segment.c_str()); - x++; + launch_session->host_audio = host_audio; + std::stringstream mode = std::stringstream(get_arg(args, "mode", "0x0x0")); + // Split mode by the char "x", to populate width/height/fps + int x = 0; + std::string segment; + while (std::getline(mode, segment, 'x')) { + if (x == 0) launch_session->width = atoi(segment.c_str()); + if (x == 1) launch_session->height = atoi(segment.c_str()); + if (x == 2) launch_session->fps = atoi(segment.c_str()); + 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(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->unique_id = named_cert_p->uuid; 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->surround_info = util::from_view(get_arg(args, "surroundAudioInfo", "196610")); 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->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(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; } @@ -1032,8 +1039,11 @@ namespace nvhttp { return; } + auto appid_str = get_arg(args, "appid"); + auto appid = util::from_view(appid_str); + 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()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { @@ -1046,9 +1056,6 @@ namespace nvhttp { return; } - auto appid = util::from_view(get_arg(args, "appid")); - auto appid_str = std::to_string(appid); - if (appid > 0) { const auto& apps = proc::proc.get_apps(); 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()); if (!launch_session->rtsp_cipher && encryption_mode == config::ENCRYPTION_MODE_MANDATORY) { diff --git a/src/nvhttp.h b/src/nvhttp.h index 10391b46..45b8b1a6 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -10,6 +10,7 @@ #include // lib includes +#include #include // local includes @@ -24,6 +25,8 @@ using namespace std::chrono_literals; */ namespace nvhttp { + using args_t = SimpleWeb::CaseInsensitiveMultimap; + /** * @brief The protocol version. * @details The version of the GameStream protocol we are mocking. @@ -57,6 +60,12 @@ namespace nvhttp { void start(); + std::string + get_arg(const args_t &args, const char *name, const char *default_value = nullptr); + + std::shared_ptr + 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. * @param pin The user supplied pin. diff --git a/src/platform/windows/virtual_display.cpp b/src/platform/windows/virtual_display.cpp index f2b77cec..73ca1bdb 100644 --- a/src/platform/windows/virtual_display.cpp +++ b/src/platform/windows/virtual_display.cpp @@ -89,6 +89,7 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) { result = ChangeDisplaySettingsExW(displayDevice.DeviceName, &devMode, NULL, CDS_UPDATEREGISTRY | CDS_NORESET, NULL); if (result != DISP_CHANGE_SUCCESSFUL) { + wprintf(L"[SUDOVDA] Changing config for display %ls failed!\n\n", displayDevice.DeviceName); return false; } } @@ -98,10 +99,17 @@ bool setPrimaryDisplay(const wchar_t* primaryDeviceName) { primaryDevMode.dmPosition.x = 0; primaryDevMode.dmPosition.y = 0; 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); if (result != DISP_CHANGE_SUCCESSFUL) { + wprintf(L"[SUDOVDA] Applying display coinfig failed!\n\n"); return false; } diff --git a/src/process.cpp b/src/process.cpp index 610aa681..6ba8941e 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -170,8 +170,8 @@ namespace proc { _app_id = app_id; _launch_session = launch_session; - uint32_t client_width = launch_session->width; - uint32_t client_height = launch_session->height; + uint32_t client_width = launch_session->width ? launch_session->width : 1920; + uint32_t client_height = launch_session->height ? launch_session->height : 1080; uint32_t render_width = client_width; uint32_t render_height = client_height; @@ -215,7 +215,7 @@ namespace proc { launch_session->device_name.c_str(), render_width, render_height, - launch_session->fps, + launch_session->fps ? launch_session->fps : 60, launch_session->display_guid ); @@ -223,8 +223,11 @@ namespace proc { std::wstring currentPrimaryDisplayName = VDISPLAY::getPrimaryDisplay(); - // Apply display settings - VDISPLAY::changeDisplaySettings(vdisplayName.c_str(), render_width, render_height, launch_session->fps); + // When launched through config ui, don't change display settings + 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 bool shouldSetPrimary = false; @@ -237,11 +240,12 @@ namespace proc { // Set primary display if needed if (shouldSetPrimary) { - VDISPLAY::setPrimaryDisplay( - (launch_session->virtual_display || _app.virtual_display_primary) - ? vdisplayName.c_str() - : prevPrimaryDisplayName.c_str() - ); + auto disp = (launch_session->virtual_display || _app.virtual_display_primary) + ? vdisplayName + : prevPrimaryDisplayName; + BOOST_LOG(info) << "Setting display " << disp << " primary!!!"; + + VDISPLAY::setPrimaryDisplay(disp.c_str()); } // Set virtual_display to true when everything went fine @@ -373,7 +377,7 @@ namespace proc { #ifdef _WIN32 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. auto retryInterval = 200ms; while (is_changing_settings_going_to_fail()) { @@ -384,6 +388,7 @@ namespace proc { std::this_thread::sleep_for(retryInterval); retryInterval *= 2; } + // We should have got the actual streaming display by now std::string currentDisplay = this->display_name; if (!currentDisplay.empty()) { @@ -419,6 +424,10 @@ namespace proc { fg.disable(); +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::update_tray_playing(_app.name); +#endif + return 0; } @@ -778,36 +787,6 @@ namespace proc { std::set ids; std::vector apps; 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) { proc::ctx_t ctx; @@ -915,6 +894,35 @@ namespace proc { 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 { std::move(this_env), std::move(apps) }; diff --git a/src/stream.cpp b/src/stream.cpp index 4507cdfd..2526e5c3 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -2123,9 +2123,6 @@ namespace stream { // If this is the first session, invoke the platform callbacks if (++running_sessions == 1) { 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; diff --git a/src/system_tray.cpp b/src/system_tray.cpp index b447e38a..fdb9c074 100644 --- a/src/system_tray.cpp +++ b/src/system_tray.cpp @@ -262,10 +262,10 @@ namespace system_tray { tray_update(&tray); tray.icon = TRAY_ICON_PLAYING; - tray.notification_title = "Stream Started"; + tray.notification_title = "App launched"; char 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()); #ifdef _WIN32 strncpy(msg, convertUtf8ToCurrentCodepage(msg).c_str(), std::size(msg) - 1); @@ -273,7 +273,7 @@ namespace system_tray { #endif tray.notification_text = msg; tray.notification_icon = TRAY_ICON_PLAYING; - tray.tooltip = msg; + tray.tooltip = PROJECT_NAME; tray.menu[2].text = force_close_msg; tray_update(&tray); } @@ -299,7 +299,7 @@ namespace system_tray { tray.notification_title = "Stream Paused"; tray.notification_text = msg; tray.notification_icon = TRAY_ICON_PAUSING; - tray.tooltip = msg; + tray.tooltip = PROJECT_NAME; tray_update(&tray); } diff --git a/src_assets/common/assets/web/apps.html b/src_assets/common/assets/web/apps.html index ab804bf0..5686b393 100644 --- a/src_assets/common/assets/web/apps.html +++ b/src_assets/common/assets/web/apps.html @@ -89,10 +89,13 @@ {{app.name}} - + - @@ -407,14 +410,7 @@ }; }, created() { - fetch("/api/apps", { - credentials: 'include' - }) - .then((r) => r.json()) - .then((r) => { - console.log(r); - this.apps = r.apps; - }); + this.loadApps(); fetch("/api/config", { credentials: 'include' @@ -423,6 +419,15 @@ .then(r => this.platform = r.platform); }, methods: { + loadApps() { + fetch("/api/apps", { + credentials: 'include' + }) + .then(r => r.json()) + .then(r => { + this.apps = r.apps.map(i => ({...i, launching: false})); + }); + }, newApp() { this.editForm = { name: "", @@ -443,6 +448,25 @@ this.editForm.index = -1; 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) { this.editForm = JSON.parse(JSON.stringify(this.apps[id])); this.editForm.index = id; 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 90a2e44a..622ea6bd 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -74,6 +74,10 @@ "global_prep_name": "Global Prep Commands", "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.", + "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...", "name": "Name", "output_desc": "The file where the output of the command is stored, if it is not specified, the output is ignored", 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 22a43f6d..3007aa17 100644 --- a/src_assets/common/assets/web/public/assets/locale/zh.json +++ b/src_assets/common/assets/web/public/assets/locale/zh.json @@ -72,6 +72,10 @@ "global_prep_name": "全局预处理命令", "image": "图片", "image_desc": "发送到客户端的应用程序图标/图片/图像的路径。图片必须是 PNG 文件。如果未设置,Apollo 将发送默认图片。", + "launch": "启动", + "launch_warning": "确定要启动此应用吗?这将会终止当前已启动的应用。", + "launch_success": "应用启动成功!", + "launch_failed": "应用启动失败:", "loading": "加载中...", "name": "名称", "output_desc": "存储命令输出的文件,如果未指定,输出将被忽略",