diff --git a/src/config.cpp b/src/config.cpp index cc7b704b..371e8b24 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1235,6 +1235,8 @@ namespace config { bool_f(vars, "high_resolution_scrolling", input.high_resolution_scrolling); bool_f(vars, "native_pen_touch", input.native_pen_touch); + list_string_f(vars, "owner_client_uuids", input.owner_client_uuids); + bool_f(vars, "notify_pre_releases", sunshine.notify_pre_releases); bool_f(vars, "system_tray", sunshine.system_tray); diff --git a/src/config.h b/src/config.h index e8d1594f..c984bce3 100644 --- a/src/config.h +++ b/src/config.h @@ -203,6 +203,8 @@ namespace config { bool high_resolution_scrolling; bool native_pen_touch; + + std::vector owner_client_uuids; }; namespace flag { diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 85d66077..f718b190 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -40,6 +40,7 @@ #include "nvhttp.h" #include "platform/common.h" #include "process.h" +#include "stream.h" #include "utility.h" #include "uuid.h" @@ -805,6 +806,19 @@ namespace confighttp { send_response(response, output_tree); } + void getActiveSessions(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + nlohmann::json output_tree; + output_tree["sessions"] = stream::get_active_sessions_info(); + output_tree["status"] = true; + send_response(response, output_tree); + } + /** * @brief Unpair a client. * @param response The HTTP response object. @@ -1454,6 +1468,7 @@ namespace confighttp { server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/list$"]["GET"] = getClients; server.resource["^/api/clients/unpair$"]["POST"] = unpair; + server.resource["^/api/sessions/active$"]["GET"] = getActiveSessions; server.resource["^/api/apps/close$"]["POST"] = closeApp; server.resource["^/api/covers/upload$"]["POST"] = uploadCover; server.resource["^/api/covers/([0-9]+)$"]["GET"] = getCover; diff --git a/src/input.cpp b/src/input.cpp index 82c761af..bf4a1859 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -41,6 +41,18 @@ namespace input { #define DISABLE_LEFT_BUTTON_DELAY ((thread_pool_util::ThreadPool::task_id_t) 0x01) #define ENABLE_LEFT_BUTTON_DELAY nullptr + static uint64_t now_ms() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() + ).count(); + } + + static inline void touch_activity(std::atomic *ts) { + if (ts) { + ts->store(now_ms(), std::memory_order_relaxed); + } + } + constexpr auto VKEY_SHIFT = 0x10; constexpr auto VKEY_LSHIFT = 0xA0; constexpr auto VKEY_RSHIFT = 0xA1; @@ -193,6 +205,10 @@ namespace input { int32_t accumulated_vscroll_delta; int32_t accumulated_hscroll_delta; + + std::atomic *activity_keyboard_ms = nullptr; + std::atomic *activity_mouse_ms = nullptr; + std::atomic *activity_gamepad_ms = nullptr; }; /** @@ -446,6 +462,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + input->mouse_left_button_timeout = DISABLE_LEFT_BUTTON_DELAY; platf::move_mouse(platf_input, util::endian::big(packet->deltaX), util::endian::big(packet->deltaY)); } @@ -543,6 +561,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + if (input->mouse_left_button_timeout == DISABLE_LEFT_BUTTON_DELAY) { input->mouse_left_button_timeout = ENABLE_LEFT_BUTTON_DELAY; } @@ -593,6 +613,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + auto release = util::endian::little(packet->header.magic) == MOUSE_BUTTON_UP_EVENT_MAGIC_GEN5; auto button = util::endian::big(packet->button); if (button > 0 && button < mouse_press.size()) { @@ -757,6 +779,8 @@ namespace input { return; } + touch_activity(input->activity_keyboard_ms); + auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; auto keyCode = packet->keyCode & 0x00FF; @@ -817,6 +841,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + if (config::input.high_resolution_scrolling) { platf::scroll(platf_input, util::endian::big(packet->scrollAmt1)); } else { @@ -840,6 +866,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + if (config::input.high_resolution_scrolling) { platf::hscroll(platf_input, util::endian::big(packet->scrollAmount)); } else { @@ -872,6 +900,8 @@ namespace input { return; } + touch_activity(input->activity_gamepad_ms); + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; @@ -912,6 +942,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + // Convert the client normalized coordinates to touchport coordinates auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { @@ -968,6 +1000,8 @@ namespace input { return; } + touch_activity(input->activity_mouse_ms); + // Convert the client normalized coordinates to touchport coordinates auto coords = client_to_touchport(input, {from_clamped_netfloat(packet->x, 0.0f, 1.0f) * 65535.f, from_clamped_netfloat(packet->y, 0.0f, 1.0f) * 65535.f}, {65535.f, 65535.f}); if (!coords) { @@ -1026,6 +1060,8 @@ namespace input { return; } + touch_activity(input->activity_gamepad_ms); + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; @@ -1059,6 +1095,8 @@ namespace input { return; } + touch_activity(input->activity_gamepad_ms); + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; @@ -1091,6 +1129,8 @@ namespace input { return; } + touch_activity(input->activity_gamepad_ms); + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; return; @@ -1116,6 +1156,8 @@ namespace input { return; } + touch_activity(input->activity_gamepad_ms); + if (packet->controllerNumber < 0 || packet->controllerNumber >= input->gamepads.size()) { BOOST_LOG(warning) << "ControllerNumber out of range ["sv << packet->controllerNumber << ']'; diff --git a/src/nvhttp.cpp b/src/nvhttp.cpp index 4d8a87b4..e624bcb3 100644 --- a/src/nvhttp.cpp +++ b/src/nvhttp.cpp @@ -782,6 +782,15 @@ namespace nvhttp { return named_cert_nodes; } + std::string get_client_name(const std::string &uuid) { + for (auto &named_cert : client_root.named_devices) { + if (named_cert.uuid == uuid) { + return named_cert.name; + } + } + return ""; + } + void applist(resp_https_t response, req_https_t request) { print_req(request); diff --git a/src/nvhttp.h b/src/nvhttp.h index 63633707..a0428ec7 100644 --- a/src/nvhttp.h +++ b/src/nvhttp.h @@ -193,6 +193,8 @@ namespace nvhttp { */ nlohmann::json get_all_clients(); + std::string get_client_name(const std::string &uuid); + /** * @brief Remove all paired clients. * @examples diff --git a/src/stream.cpp b/src/stream.cpp index b92e579e..11d87257 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -4,6 +4,7 @@ */ // standard includes +#include #include #include #include @@ -26,6 +27,7 @@ extern "C" { #include "input.h" #include "logging.h" #include "network.h" +#include "nvhttp.h" #include "platform/common.h" #include "process.h" #include "stream.h" @@ -340,6 +342,34 @@ namespace stream { control_server_t control_server; }; + /** + * @brief Per-session input policy (which remote input types are allowed). + */ + struct session_input_policy_t { + bool allow_gamepad = true; + bool allow_keyboard = false; + bool allow_mouse = false; + bool is_owner_session = false; + }; + + /** + * @brief Per-session input activity timestamps (monotonic ms). + */ + struct session_input_activity_t { + std::atomic last_keyboard_ms {0}; + std::atomic last_mouse_ms {0}; + std::atomic last_gamepad_ms {0}; + }; + + /** + * @brief Return current monotonic time in milliseconds. + */ + static uint64_t now_monotonic_ms() { + return std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() + ).count(); + } + struct session_t { config_t config; @@ -347,6 +377,12 @@ namespace stream { std::shared_ptr input; + // Parsec-style per-session state + std::string client_unique_id; + std::string client_name; + session_input_policy_t input_policy; + session_input_activity_t input_activity; + std::thread audioThread; std::thread videoThread; @@ -1896,6 +1932,57 @@ namespace stream { audio::capture(session->mail, session->config.audio, session); } + nlohmann::json get_active_sessions_info() { + auto result = nlohmann::json::array(); + + auto ref = broadcast.ref(); + if (!ref) { + return result; + } + + auto now = now_monotonic_ms(); + + auto lg = ref->control_server._sessions.lock(); + for (auto *session : *ref->control_server._sessions) { + if (session->state.load(std::memory_order_relaxed) != session::state_e::RUNNING) { + continue; + } + + auto last_kb = session->input_activity.last_keyboard_ms.load(std::memory_order_relaxed); + auto last_mouse = session->input_activity.last_mouse_ms.load(std::memory_order_relaxed); + auto last_gp = session->input_activity.last_gamepad_ms.load(std::memory_order_relaxed); + + nlohmann::json entry; + entry["session_id"] = session->launch_session_id; + entry["client_uuid"] = session->client_unique_id; + entry["client_name"] = session->client_name; + entry["is_owner_session"] = session->input_policy.is_owner_session; + + entry["policy"] = { + {"allow_keyboard", session->input_policy.allow_keyboard}, + {"allow_mouse", session->input_policy.allow_mouse}, + {"allow_gamepad", session->input_policy.allow_gamepad}, + }; + + auto kb_ago = last_kb > 0 ? now - last_kb : UINT64_MAX; + auto mouse_ago = last_mouse > 0 ? now - last_mouse : UINT64_MAX; + auto gp_ago = last_gp > 0 ? now - last_gp : UINT64_MAX; + + entry["activity"] = { + {"keyboard_active", kb_ago <= KB_ACTIVE_WINDOW_MS}, + {"mouse_active", mouse_ago <= MOUSE_ACTIVE_WINDOW_MS}, + {"gamepad_active", gp_ago <= GAMEPAD_ACTIVE_WINDOW_MS}, + {"last_keyboard_ms_ago", last_kb > 0 ? kb_ago : -1}, + {"last_mouse_ms_ago", last_mouse > 0 ? mouse_ago : -1}, + {"last_gamepad_ms_ago", last_gp > 0 ? gp_ago : -1}, + }; + + result.push_back(std::move(entry)); + } + + return result; + } + namespace session { std::atomic_uint running_sessions; @@ -1964,6 +2051,10 @@ namespace stream { int start(session_t &session, const std::string &addr_string) { session.input = input::alloc(session.mail); + session.input->activity_keyboard_ms = &session.input_activity.last_keyboard_ms; + session.input->activity_mouse_ms = &session.input_activity.last_mouse_ms; + session.input->activity_gamepad_ms = &session.input_activity.last_gamepad_ms; + session.broadcast_ref = broadcast.ref(); if (!session.broadcast_ref) { return -1; @@ -2010,6 +2101,20 @@ namespace stream { session->shutdown_event = mail->event(mail::shutdown); session->launch_session_id = launch_session.id; + session->client_unique_id = launch_session.unique_id; + session->client_name = nvhttp::get_client_name(launch_session.unique_id); + + auto &owner_uuids = config::input.owner_client_uuids; + bool is_owner = std::find(owner_uuids.begin(), owner_uuids.end(), launch_session.unique_id) != owner_uuids.end(); + + session->input_policy.is_owner_session = is_owner; + session->input_policy.allow_gamepad = true; + session->input_policy.allow_keyboard = is_owner; + session->input_policy.allow_mouse = is_owner; + + if (is_owner) { + BOOST_LOG(info) << "Owner session detected for client: "sv << launch_session.unique_id; + } session->config = config; diff --git a/src/stream.h b/src/stream.h index 53afff4f..5126acaf 100644 --- a/src/stream.h +++ b/src/stream.h @@ -5,10 +5,13 @@ #pragma once // standard includes +#include #include +#include // lib includes #include +#include // local includes #include "audio.h" @@ -20,8 +23,14 @@ namespace stream { constexpr auto CONTROL_PORT = 10; constexpr auto AUDIO_STREAM_PORT = 11; + constexpr uint64_t KB_ACTIVE_WINDOW_MS = 500; + constexpr uint64_t MOUSE_ACTIVE_WINDOW_MS = 500; + constexpr uint64_t GAMEPAD_ACTIVE_WINDOW_MS = 500; + struct session_t; + nlohmann::json get_active_sessions_info(); + struct config_t { audio::config_t audio; video::config_t monitor; diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 1bc7cdb6..fa7ca614 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -211,6 +211,7 @@ "mouse": "disabled", "high_resolution_scrolling": "enabled", "native_pen_touch": "enabled", + "owner_client_uuids": "", "keybindings": "[0x10,0xA0,0x11,0xA2,0x12,0xA4]", // todo: add this to UI }, }, diff --git a/src_assets/common/assets/web/configs/tabs/Inputs.vue b/src_assets/common/assets/web/configs/tabs/Inputs.vue index 64792b07..b7f188a5 100644 --- a/src_assets/common/assets/web/configs/tabs/Inputs.vue +++ b/src_assets/common/assets/web/configs/tabs/Inputs.vue @@ -181,6 +181,15 @@ const config = ref(props.config) v-model="config.native_pen_touch" default="true" > + +
+
+ + +
Comma-separated UUIDs of owner clients. Owner sessions start with keyboard/mouse enabled. Find UUIDs in the Troubleshooting page under paired clients.
+
diff --git a/src_assets/common/assets/web/troubleshooting.html b/src_assets/common/assets/web/troubleshooting.html index c33b3db2..96efa5d1 100644 --- a/src_assets/common/assets/web/troubleshooting.html +++ b/src_assets/common/assets/web/troubleshooting.html @@ -147,6 +147,49 @@ + +
+
+

Live Input Status

+

Per-session input activity and policy. Indicators update every second.

+
+
+ No active streaming sessions. +
+
    +
  • +
    +
    + {{ s.client_name || s.client_uuid || 'Unknown' }} + Owner +
    +
    + + + KB + + {{ s.policy.allow_keyboard ? 'ON' : 'OFF' }} + + + + + Mouse + + {{ s.policy.allow_mouse ? 'ON' : 'OFF' }} + + + + + Gamepad + + {{ s.policy.allow_gamepad ? 'ON' : 'OFF' }} + + +
    +
    +
  • +
+
@@ -229,6 +272,7 @@ }, data() { return { + activeSessions: [], clients: [], closeAppPressed: false, closeAppStatus: null, @@ -237,6 +281,7 @@ logs: 'Loading...', logFilter: null, logInterval: null, + sessionInterval: null, restartPressed: false, showApplyMessage: false, platform: "", @@ -389,13 +434,32 @@ this.logInterval = setInterval(() => { this.refreshLogs(); }, 5000); + this.sessionInterval = setInterval(() => { + this.refreshActiveSessions(); + }, 1000); this.refreshLogs(); this.refreshClients(); + this.refreshActiveSessions(); }, beforeDestroy() { clearInterval(this.logInterval); + clearInterval(this.sessionInterval); }, methods: { + refreshActiveSessions() { + fetch("./api/sessions/active") + .then((r) => r.json()) + .then((r) => { + if (r.status === true && r.sessions) { + this.activeSessions = r.sessions; + } else { + this.activeSessions = []; + } + }) + .catch(() => { + this.activeSessions = []; + }); + }, refreshLogs() { fetch("./api/logs",) .then((r) => r.text()) @@ -610,4 +674,21 @@ initApp(app); + +