diff --git a/src/audio.cpp b/src/audio.cpp index bd4c8efe..05672b2e 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -132,6 +132,13 @@ namespace audio { void capture(safe::mail_t mail, config_t config, void *channel_data) { auto shutdown_event = mail->event(mail::shutdown); + + if (config.input_only) { + BOOST_LOG(info) << "Input only session, audio will not be captured."sv; + shutdown_event->view(); + return; + } + auto stream = stream_configs[map_stream(config.channels, config.flags[config_t::HIGH_QUALITY])]; if (config.flags[config_t::CUSTOM_SURROUND_PARAMS]) { apply_surround_params(stream, config.customStreamParams); @@ -148,6 +155,7 @@ namespace audio { // Wait for shutdown to be signalled if we fail init. // This allows streaming to continue without audio. shutdown_event->view(); + return; }); auto &control = ref->control; diff --git a/src/audio.h b/src/audio.h index 927dfdef..6add0584 100644 --- a/src/audio.h +++ b/src/audio.h @@ -52,6 +52,8 @@ namespace audio { int channels; int mask; + bool input_only; + stream_params_t customStreamParams; std::bitset flags; diff --git a/src/config.cpp b/src/config.cpp index b79191dc..8a5ef10d 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -418,6 +418,7 @@ namespace config { video_t video { false, // headless_mode true, // limit_framerate + false, // double_framerate 28, // qp 0, // hevc_mode @@ -543,6 +544,7 @@ namespace config { true, // always send scancodes true, // high resolution scrolling true, // native pen/touch support + false, // enable input only mode }; sunshine_t sunshine { @@ -1091,6 +1093,7 @@ namespace config { bool_f(vars, "headless_mode", video.headless_mode); bool_f(vars, "limit_framerate", video.limit_framerate); + bool_f(vars, "double_framerate", video.double_framerate); int_f(vars, "qp", video.qp); int_between_f(vars, "hevc_mode", video.hevc_mode, { 0, 3 }); int_between_f(vars, "av1_mode", video.av1_mode, { 0, 3 }); @@ -1263,6 +1266,7 @@ namespace config { bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling); bool_f(vars, "native_pen_touch", input.native_pen_touch); + bool_f(vars, "enable_input_only_mode", input.enable_input_only_mode); bool_f(vars, "hide_tray_controls", sunshine.hide_tray_controls); bool_f(vars, "enable_pairing", sunshine.enable_pairing); diff --git a/src/config.h b/src/config.h index 9c104165..4ba342fc 100644 --- a/src/config.h +++ b/src/config.h @@ -17,6 +17,7 @@ namespace config { struct video_t { bool headless_mode; bool limit_framerate; + bool double_framerate; // ffmpeg params int qp; // higher == more compression and less quality @@ -198,6 +199,8 @@ namespace config { bool high_resolution_scrolling; bool native_pen_touch; + + bool enable_input_only_mode; }; namespace flag { diff --git a/src/confighttp.cpp b/src/confighttp.cpp index a321b9bd..62705358 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -1163,7 +1163,7 @@ namespace confighttp { 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 launch_session = nvhttp::make_launch_session(true, false, appid, args, &named_cert); auto err = proc::proc.execute(appid, app, launch_session); if (err) { bad_request(response, request, err == 503 diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 3083b070..79ebb107 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -360,7 +360,7 @@ namespace nvhttp { } std::shared_ptr - make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p) { + make_launch_session(bool host_audio, bool input_only, 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; @@ -420,6 +420,8 @@ namespace nvhttp { launch_session->client_do_cmds = named_cert_p->do_cmds; launch_session->client_undo_cmds = named_cert_p->undo_cmds; + launch_session->input_only = input_only; + return launch_session; } @@ -960,7 +962,10 @@ namespace nvhttp { tree.put("root.PairStatus", pair_status); if constexpr (std::is_same_v) { - auto current_appid = proc::proc.running(); + int current_appid = 0; + if (!config::input.enable_input_only_mode || rtsp_stream::session_count() == 0) { + current_appid = proc::proc.running(); + } tree.put("root.currentgame", current_appid); tree.put("root.state", current_appid > 0 ? "SUNSHINE_SERVER_BUSY" : "SUNSHINE_SERVER_FREE"); } else { @@ -1046,7 +1051,14 @@ namespace nvhttp { auto named_cert_p = get_verified_cert(request); if (!!(named_cert_p->perm & PERM::_all_actions)) { + auto current_appid = proc::proc.running(); + auto input_only_id_int = util::from_view(proc::input_only_app_id); + auto should_hide_inactive_apps = config::input.enable_input_only_mode && rtsp_stream::session_count() != 0 && current_appid != input_only_id_int; for (auto &app : proc::proc.get_apps()) { + auto appid = util::from_view(app.id); + if (should_hide_inactive_apps && appid != current_appid && appid != input_only_id_int) { + continue; + } pt::ptree app_node; app_node.put("IsHdrSupported"s, video::active_hevc_mode == 3 ? 1 : 0); @@ -1086,8 +1098,22 @@ namespace nvhttp { response->close_connection_after_response = true; }); + auto args = request->parse_query_string(); + + auto appid_str = get_arg(args, "appid"); + auto appid = util::from_view(appid_str); + auto current_appid = proc::proc.running(); + bool is_input_only = config::input.enable_input_only_mode && appid_str == proc::input_only_app_id; + auto named_cert_p = get_verified_cert(request); - if (!(named_cert_p->perm & PERM::launch)) { + auto perm = PERM::launch; + + // If we have already launched an app, we should allow clients with view permission to join the input only or current app's session. + if (current_appid > 0 && (is_input_only || appid == current_appid)) { + perm = PERM::_allow_view; + } + + if (!(named_cert_p->perm & perm)) { BOOST_LOG(debug) << "Permission LaunchApp denied for [" << named_cert_p->name << "] (" << (uint32_t)named_cert_p->perm << ")"; tree.put("root.resume", 0); @@ -1096,8 +1122,6 @@ namespace nvhttp { return; } - - auto args = request->parse_query_string(); if ( args.find("rikey"s) == std::end(args) || args.find("rikeyid"s) == std::end(args) || @@ -1110,20 +1134,18 @@ namespace nvhttp { return; } - auto current_appid = proc::proc.running(); - if (current_appid > 0) { - tree.put("root.resume", 0); - tree.put("root..status_code", 400); - tree.put("root..status_message", "An app is already running on this host"); + if (!is_input_only) { + if (current_appid > 0 && current_appid != util::from_view(proc::input_only_app_id) && appid != current_appid) { + tree.put("root.resume", 0); + tree.put("root..status_code", 400); + tree.put("root..status_message", "An app is already running on this host"); - 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")); - auto launch_session = make_launch_session(host_audio, appid, args, named_cert_p); + auto launch_session = make_launch_session(host_audio, is_input_only, 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) { @@ -1136,7 +1158,18 @@ namespace nvhttp { return; } - if (appid > 0) { + if (is_input_only) { + BOOST_LOG(info) << "Launching input only session..."sv; + + // Still probe encoders once, if input only session is launched first + // But we're ignoring if it's successful or not + if (rtsp_stream::session_count() == 0 && !proc::proc.virtual_display) { + video::probe_encoders(); + if (current_appid == 0) { + proc::proc.launch_input_only(); + } + } + } else if (appid > 0 && appid != current_appid) { 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; @@ -1229,7 +1262,7 @@ namespace nvhttp { if (no_active_sessions && args.find("localAudioPlayMode"s) != std::end(args)) { host_audio = util::from_view(get_arg(args, "localAudioPlayMode")); } - auto launch_session = make_launch_session(host_audio, 0, args, named_cert_p); + auto launch_session = make_launch_session(host_audio, false, 0, args, named_cert_p); if (!proc::proc.allow_client_commands) { launch_session->client_do_cmds.clear(); diff --git a/src/nvhttp.h b/src/nvhttp.h index 1ecc8e62..3f83ee9f 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -70,7 +70,7 @@ namespace nvhttp { extract_command_entries(const boost::property_tree::ptree& pt, const std::string& key); std::shared_ptr - make_launch_session(bool host_audio, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p); + make_launch_session(bool host_audio, bool input_only, int appid, const args_t &args, const crypto::named_cert_t* named_cert_p); /** * @brief Setup the nvhttp server. diff --git a/src/process.cpp b/src/process.cpp index 56b27161..dc1e73da 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -49,6 +49,8 @@ namespace proc { proc_t proc; + std::string input_only_app_id; + #ifdef _WIN32 VDISPLAY::DRIVER_STATUS vDisplayDriverStatus = VDISPLAY::DRIVER_STATUS::UNKNOWN; @@ -158,10 +160,21 @@ namespace proc { return cmd_path.parent_path(); } + void + proc_t::launch_input_only() { + _app_id = util::from_view(input_only_app_id); + placebo = true; + } + int proc_t::execute(int app_id, const ctx_t& app, std::shared_ptr launch_session) { - // Ensure starting from a clean slate - terminate(); + if (_app_id == util::from_view(input_only_app_id)) { + terminate(); + std::this_thread::sleep_for(1s); + } else { + // Ensure starting from a clean slate + terminate(); + } _app = app; _app_id = app_id; @@ -241,12 +254,18 @@ namespace proc { memcpy(&launch_session->display_guid, &device_uuid, sizeof(GUID)); + int target_fps = launch_session->fps ? launch_session->fps : 60; + + if (config::video.double_framerate) { + target_fps *= 2; + } + std::wstring vdisplayName = VDISPLAY::createVirtualDisplay( device_uuid_str.c_str(), device_name.c_str(), render_width, render_height, - launch_session->fps ? launch_session->fps : 60, + target_fps, launch_session->display_guid ); @@ -291,7 +310,7 @@ namespace proc { // 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()) { + if (rtsp_stream::session_count() == 0 && video::probe_encoders()) { return 503; } @@ -901,6 +920,73 @@ namespace proc { std::set ids; std::vector apps; int i = 0; + + // Input Only entry + if (config::input.enable_input_only_mode) { + proc::ctx_t ctx; + // ctx.uuid = ""; // We're not using uuid for this special entry + ctx.name = "Input Only"; + ctx.image_path = parse_env_val(this_env, "input_only.png"); + ctx.virtual_display = false; + ctx.scale_factor = 100; + ctx.use_app_identity = false; + ctx.per_client_app_identity = false; + ctx.allow_client_commands = false; + + 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); + + input_only_app_id = ctx.id; + + apps.emplace_back(std::move(ctx)); + } + + // Virtual Display entry + #ifdef _WIN32 + if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { + proc::ctx_t ctx; + // ctx.uuid = ""; // We're not using uuid for this special entry + ctx.name = "Virtual Display"; + ctx.image_path = parse_env_val(this_env, "virtual_desktop.png"); + ctx.virtual_display = true; + ctx.scale_factor = 100; + ctx.use_app_identity = false; + ctx.per_client_app_identity = false; + ctx.allow_client_commands = false; + + 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; @@ -1032,38 +1118,6 @@ namespace proc { apps.emplace_back(std::move(ctx)); } - #ifdef _WIN32 - if (vDisplayDriverStatus == VDISPLAY::DRIVER_STATUS::OK) { - proc::ctx_t ctx; - // ctx.uuid = ""; // We're not using uuid for this special entry - ctx.name = "Virtual Display"; - ctx.image_path = parse_env_val(this_env, "virtual_desktop.png"); - ctx.virtual_display = true; - ctx.scale_factor = 100; - ctx.use_app_identity = false; - ctx.per_client_app_identity = false; - ctx.allow_client_commands = false; - - 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/process.h b/src/process.h index af58e8dd..afc04942 100644 --- a/src/process.h +++ b/src/process.h @@ -96,6 +96,9 @@ namespace proc { _env(std::move(env)), _apps(std::move(apps)) {} + void + launch_input_only(); + int execute(int app_id, const ctx_t& _app, std::shared_ptr launch_session); @@ -178,4 +181,5 @@ namespace proc { terminate_process_group(boost::process::v1::child &proc, boost::process::v1::group &group, std::chrono::seconds exit_timeout); extern proc_t proc; + extern std::string input_only_app_id; } // namespace proc diff --git a/src/rtsp.cpp b/src/rtsp.cpp index a490ad8e..6bfe6730 100644 --- a/src/rtsp.cpp +++ b/src/rtsp.cpp @@ -1056,6 +1056,8 @@ namespace rtsp_stream { config.monitor.encodingFramerate = config.monitor.framerate; } + config.monitor.input_only = session.input_only; + configuredBitrateKbps = util::from_view(args.at("x-ml-video.configuredBitrateKbps"sv)); } catch (std::out_of_range &) { @@ -1099,6 +1101,8 @@ namespace rtsp_stream { config.audio.flags[audio::config_t::CUSTOM_SURROUND_PARAMS] = valid; } + config.audio.input_only = session.input_only; + // If the client sent a configured bitrate, we will choose the actual bitrate ourselves // by using FEC percentage and audio quality settings. If the calculated bitrate ends up // too low, we'll allow it to exceed the limits rather than reducing the encoding bitrate diff --git a/src/rtsp.h b/src/rtsp.h index 2c1b40a4..160b47f3 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -36,6 +36,7 @@ namespace rtsp_stream { std::string unique_id; crypto::PERM perm; + bool input_only; bool host_audio; int width; int height; diff --git a/src/video.cpp b/src/video.cpp index 0ed000f7..03b7103c 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -1847,6 +1847,20 @@ namespace video { } } + if (config.input_only) { + BOOST_LOG(info) << "Input only session, video will not be captured."sv; + + // Encode the dummy img only once + if (encode(frame_nr++, *session, packets, channel_data, std::chrono::steady_clock::now())) { + BOOST_LOG(error) << "Could not encode dummy video packet"sv; + return; + } + + shutdown_event->view(); + + return; + } + std::chrono::steady_clock::time_point last_frame_timestamp; std::chrono::steady_clock::time_point last_encoded_timestamp = std::chrono::steady_clock::now(); bool stop_encoding = false; diff --git a/src/video.h b/src/video.h index 94cda5b9..2b38f96d 100644 --- a/src/video.h +++ b/src/video.h @@ -41,6 +41,8 @@ namespace video { int chromaSamplingType; // 0 - 4:2:0, 1 - 4:4:4 int enableIntraRefresh; // 0 - disabled, 1 - enabled + + bool input_only; }; platf::mem_type_e