From 6f62f54573d6cafbc259a4c02133ecab6adbb046 Mon Sep 17 00:00:00 2001 From: casey broome Date: Mon, 12 Jan 2026 20:20:47 -0600 Subject: [PATCH 1/6] Add session management UI for viewing and disconnecting active streams - Add /sessions page with table showing client name, IP address, and duration - Add /api/sessions endpoint to list active streaming sessions - Add /api/sessions/disconnect endpoint to terminate sessions - Include auto-refresh (5s) and manual refresh options - Add confirmation dialog before disconnecting - Add Sessions link to navigation bar Co-Authored-By: Claude Opus 4.5 --- src/confighttp.cpp | 106 ++++++++ src/stream.cpp | 95 +++++++ src/stream.h | 23 ++ src_assets/common/assets/web/Navbar.vue | 6 + .../assets/web/public/assets/locale/en.json | 17 ++ src_assets/common/assets/web/sessions.html | 236 ++++++++++++++++++ vite.config.js | 1 + 7 files changed, 484 insertions(+) create mode 100644 src_assets/common/assets/web/sessions.html diff --git a/src/confighttp.cpp b/src/confighttp.cpp index 2f703dbca5c..a912d7dfab0 100644 --- a/src/confighttp.cpp +++ b/src/confighttp.cpp @@ -42,6 +42,7 @@ #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "stream.h" #include "utility.h" #include "uuid.h" @@ -441,6 +442,26 @@ namespace confighttp { return true; } + /** + * @brief Get the sessions page. + * @param response The HTTP response object. + * @param request The HTTP request object. + */ + void getSessionsPage(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + std::string content = file_handler::read_file(WEB_DIR "sessions.html"); + SimpleWeb::CaseInsensitiveMultimap headers; + headers.emplace("Content-Type", "text/html; charset=utf-8"); + headers.emplace("X-Frame-Options", "DENY"); + headers.emplace("Content-Security-Policy", "frame-ancestors 'none';"); + response->write(content, headers); + } + /** * @brief Get an HTML page. * @param response The HTTP response object. @@ -973,6 +994,88 @@ namespace confighttp { send_response(response, output_tree); } + /** + * @brief Get the list of active streaming sessions. + * @param response The HTTP response object. + * @param request The HTTP request object. + * + * @api_examples{/api/sessions| GET| null} + */ + void getSessions(resp_https_t response, req_https_t request) { + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + auto sessions = stream::session::get_all_sessions(); + + nlohmann::json sessions_json = nlohmann::json::array(); + auto now = std::chrono::steady_clock::now(); + + for (const auto &session : sessions) { + nlohmann::json session_json; + session_json["id"] = session.id; + session_json["client_name"] = session.client_name; + session_json["ip_address"] = session.ip_address; + + // Calculate duration in seconds + auto duration = std::chrono::duration_cast(now - session.start_time); + session_json["duration_seconds"] = duration.count(); + + sessions_json.push_back(session_json); + } + + nlohmann::json output_tree; + output_tree["sessions"] = sessions_json; + output_tree["status"] = true; + send_response(response, output_tree); + } + + /** + * @brief Disconnect an active streaming session. + * @param response The HTTP response object. + * @param request The HTTP request object. + * The body for the post request should be JSON serialized in the following format: + * @code{.json} + * { + * "session_id": "" + * } + * @endcode + * + * @api_examples{/api/sessions/disconnect| POST| {"session_id":"1234"}} + */ + void disconnectSession(resp_https_t response, req_https_t request) { + if (!check_content_type(response, request, "application/json")) { + return; + } + if (!authenticate(response, request)) { + return; + } + + print_req(request); + + std::stringstream ss; + ss << request->content.rdbuf(); + + try { + nlohmann::json output_tree; + const nlohmann::json input_tree = nlohmann::json::parse(ss); + const std::string session_id = input_tree.value("session_id", ""); + + if (session_id.empty()) { + bad_request(response, request, "Missing session_id"); + return; + } + + output_tree["status"] = stream::session::disconnect(session_id); + send_response(response, output_tree); + } catch (std::exception &e) { + BOOST_LOG(warning) << "DisconnectSession: "sv << e.what(); + bad_request(response, request, e.what()); + } + } + /** * @brief Get the configuration settings. * @param response The HTTP response object. @@ -1753,6 +1856,7 @@ namespace confighttp { server.resource["^/$"]["GET"] = page_handler("index.html"); server.resource["^/apps/?$"]["GET"] = page_handler("apps.html"); server.resource["^/clients/?$"]["GET"] = page_handler("clients.html"); + server.resource["^/sessions/?$"]["GET"] = getSessionsPage; server.resource["^/config/?$"]["GET"] = page_handler("config.html"); server.resource["^/featured/?$"]["GET"] = page_handler("featured.html"); server.resource["^/logout/?$"]["GET"] = page_handler("logout.html", false); @@ -1771,6 +1875,8 @@ namespace confighttp { server.resource["^/api/clients/unpair$"]["POST"] = unpair; server.resource["^/api/clients/unpair-all$"]["POST"] = unpairAll; server.resource["^/api/clients/update$"]["POST"] = updateClient; + server.resource["^/api/sessions$"]["GET"] = getSessions; + server.resource["^/api/sessions/disconnect$"]["POST"] = disconnectSession; server.resource["^/api/config$"]["GET"] = getConfig; server.resource["^/api/config$"]["POST"] = saveConfig; server.resource["^/api/configLocale$"]["GET"] = getLocale; diff --git a/src/stream.cpp b/src/stream.cpp index 8b303ef24da..4225e60ba83 100644 --- a/src/stream.cpp +++ b/src/stream.cpp @@ -26,6 +26,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" @@ -405,6 +406,8 @@ namespace stream { } control; std::uint32_t launch_session_id; + std::string client_unique_id; + std::chrono::steady_clock::time_point start_time; safe::mail_raw_t::event_t shutdown_event; safe::signal_t controlEnd; @@ -1985,6 +1988,7 @@ namespace stream { session.audio.peer.port(0); session.pingTimeout = std::chrono::steady_clock::now() + config::stream.ping_timeout; + session.start_time = std::chrono::steady_clock::now(); session.audioThread = std::thread {audioThread, &session}; session.videoThread = std::thread {videoThread, &session}; @@ -2009,6 +2013,7 @@ 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->config = config; @@ -2073,5 +2078,95 @@ namespace stream { return session; } + + std::vector get_all_sessions() { + std::vector result; + + // Check if any app is running before trying to access broadcast context + // This avoids triggering broadcast initialization when there's no active stream + if (proc::proc.running() == 0) { + return result; + } + + // Get the paired clients to look up names + auto clients_json = nvhttp::get_all_clients(); + + // Access the broadcast context to get sessions + auto ref = broadcast.ref(); + if (!ref) { + return result; + } + + auto lg = ref->control_server._sessions.lock(); + for (auto *session : *ref->control_server._sessions) { + if (session->state.load(std::memory_order_relaxed) != state_e::RUNNING) { + continue; + } + + session_info_t info; + + // Generate a unique ID from the session's launch_session_id + info.id = std::to_string(session->launch_session_id); + + // Look up client name from paired clients list + info.client_name = session->client_unique_id; // Default to unique_id + for (const auto &client : clients_json) { + if (client.contains("uuid") && client["uuid"] == session->client_unique_id) { + if (client.contains("name")) { + info.client_name = client["name"]; + } + break; + } + } + + // Get IP address from the control peer address + info.ip_address = session->control.expected_peer_address; + + // Get start time + info.start_time = session->start_time; + + result.push_back(std::move(info)); + } + + return result; + } + + bool disconnect(const std::string &session_id) { + // Check if any app is running before trying to access broadcast context + if (proc::proc.running() == 0) { + BOOST_LOG(warning) << "No active streaming session to disconnect"; + return false; + } + + // Convert session_id to launch_session_id + uint32_t launch_id; + try { + launch_id = std::stoul(session_id); + } catch (const std::exception &) { + BOOST_LOG(warning) << "Invalid session ID: " << session_id; + return false; + } + + // Access the broadcast context to find and stop the session + auto ref = broadcast.ref(); + if (!ref) { + return false; + } + + auto lg = ref->control_server._sessions.lock(); + for (auto *session : *ref->control_server._sessions) { + if (session->launch_session_id == launch_id) { + if (session->state.load(std::memory_order_relaxed) == state_e::RUNNING) { + BOOST_LOG(info) << "Disconnecting session " << session_id << " by admin request"; + stop(*session); + return true; + } + break; + } + } + + BOOST_LOG(warning) << "Session not found or not running: " << session_id; + return false; + } } // namespace session } // namespace stream diff --git a/src/stream.h b/src/stream.h index 53afff4fabe..810e64484fa 100644 --- a/src/stream.h +++ b/src/stream.h @@ -46,10 +46,33 @@ namespace stream { RUNNING, ///< The session is running }; + /** + * @brief Information about an active streaming session. + */ + struct session_info_t { + std::string id; ///< Unique session identifier + std::string client_name; ///< Name of the connected client + std::string ip_address; ///< Client's IP address + std::chrono::steady_clock::time_point start_time; ///< When the session started + }; + std::shared_ptr alloc(config_t &config, rtsp_stream::launch_session_t &launch_session); int start(session_t &session, const std::string &addr_string); void stop(session_t &session); void join(session_t &session); state_e state(session_t &session); + + /** + * @brief Get a list of all active streaming sessions. + * @return Vector of session information. + */ + std::vector get_all_sessions(); + + /** + * @brief Disconnect a session by its ID. + * @param session_id The unique session identifier. + * @return true if the session was found and stopped, false otherwise. + */ + bool disconnect(const std::string &session_id); } // namespace session } // namespace stream diff --git a/src_assets/common/assets/web/Navbar.vue b/src_assets/common/assets/web/Navbar.vue index a943b51ad2f..84b841c7a91 100644 --- a/src_assets/common/assets/web/Navbar.vue +++ b/src_assets/common/assets/web/Navbar.vue @@ -29,6 +29,12 @@ {{ $t('navbar.applications') }} +