From 8acd433ec6d217c400eec3a438e08dec66261a8c Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:20:41 -0700 Subject: [PATCH] api: notifications for video stream (#884) --- src/spice2x/api/stream_server.cpp | 32 +++++++++++++++++++++++++++ src/spice2x/api/websocket.cpp | 19 ++++++++++++---- src/spice2x/external/headsocket.h | 5 ++++- src/spice2x/overlay/notifications.cpp | 4 +++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/spice2x/api/stream_server.cpp b/src/spice2x/api/stream_server.cpp index 99d720d..bd3216c 100644 --- a/src/spice2x/api/stream_server.cpp +++ b/src/spice2x/api/stream_server.cpp @@ -14,6 +14,7 @@ #include "capture_pump.h" #include "hooks/graphics/graphics.h" +#include "overlay/notifications.h" #include "stream_format.h" #include "util/logging.h" #include "util/utils.h" @@ -178,6 +179,13 @@ namespace api { // carry it too, or the client sees an opaque failure instead of the status. constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n"; + // the port is unauthenticated, so a scanner hammering a busy/missing screen could + // otherwise flood the overlay; throttle failure toasts per distinct cause. kept under + // a second so it only swallows that, not a legitimate reconnect - substream itself + // switches screens with a 300ms gap, and only backs off to a full second once a + // retry has actually failed + constexpr double notification_throttle_seconds = 0.5; + void send_error(SOCKET socket, const char *status) { const std::string response = std::string("HTTP/1.0 ") + status + "\r\n" @@ -334,6 +342,11 @@ namespace api { if (slot < 0) { log_warning("api::stream", "client limit of {} hit", client_limit); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + "api::stream.client_limit", + notification_throttle_seconds, + fmt::format("Video stream refused: client limit reached ({})", address)); send_error(client, "503 Service Unavailable"); closesocket(client); continue; @@ -410,16 +423,32 @@ namespace api { if (!streamable(screen)) { log_warning("api::stream", "screen {} is not available, refusing {}", screen, address); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + fmt::format("api::stream.screen_unavailable.{}", screen), + notification_throttle_seconds, + fmt::format("Video stream refused: screen {} not available ({})", + screen, address)); send_error(socket, "404 Not Found"); } else if (!capture_pump::claim_screen(screen)) { log_warning("api::stream", "screen {} is already being streamed, refusing {}", screen, address); + overlay::notifications::add_throttled( + overlay::notifications::Severity::Warning, + fmt::format("api::stream.screen_claimed.{}", screen), + notification_throttle_seconds, + fmt::format("Video stream refused: screen {} already streaming ({})", + screen, address)); send_error(socket, "503 Service Unavailable"); } else { log_info("api::stream", "client connected: {} ({}, screen={}, fps={}, quality={})", address, request.path, screen, fps, quality); + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Video stream client connected ({}, screen {})", + address, screen)); const std::string header = "HTTP/1.0 200 OK\r\n" @@ -460,6 +489,9 @@ namespace api { capture_pump::release_screen(screen); log_info("api::stream", "client disconnected: {}", address); + overlay::notifications::add( + overlay::notifications::Severity::Info, + fmt::format("Video stream client disconnected ({})", address)); } } } diff --git a/src/spice2x/api/websocket.cpp b/src/spice2x/api/websocket.cpp index 18dec87..be7c67c 100644 --- a/src/spice2x/api/websocket.cpp +++ b/src/spice2x/api/websocket.cpp @@ -37,6 +37,15 @@ namespace api { private: ClientState *state = nullptr; + // headsocket doesn't expose the peer address on its own client API, but the + // sockaddr_in captured at accept time is sitting right there in the impl + std::string remote_address() const { + char address_data[INET_ADDRSTRLEN] {}; + inet_ntop(AF_INET, &this->_p->conn.impl()->from.sin_addr, + address_data, INET_ADDRSTRLEN); + return std::string(address_data); + } + protected: bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override; @@ -120,19 +129,21 @@ namespace api { srv->websocket->controller->init_state(state); // log connection - log_info("api::websocket", "client connected"); + const auto address = this->remote_address(); + log_info("api::websocket", "client connected: {}", address); overlay::notifications::add( overlay::notifications::Severity::Success, - "API websocket client connected"); + fmt::format("API websocket client connected ({})", address)); } void WebSocketClient::on_disconnect() { // log disconnection - log_info("api::websocket", "client disconnected"); + const auto address = this->remote_address(); + log_info("api::websocket", "client disconnected: {}", address); overlay::notifications::add( overlay::notifications::Severity::Info, - "API websocket client disconnected"); + fmt::format("API websocket client disconnected ({})", address)); // get pointer to server auto srv = reinterpret_cast(server().get()); diff --git a/src/spice2x/external/headsocket.h b/src/spice2x/external/headsocket.h index e96a349..fdad8c5 100644 --- a/src/spice2x/external/headsocket.h +++ b/src/spice2x/external/headsocket.h @@ -1450,7 +1450,10 @@ void basic_tcp_server::accept_thread() while (_p->isRunning) { detail::connection_impl conn_impl; - conn_impl.socket = ::accept(_p->serverSocket, reinterpret_cast(&conn_impl.from), nullptr); + // addrlen must be a valid in/out pointer or the OS leaves conn_impl.from untouched, + // so the peer address silently reads back as 0.0.0.0 + int from_len = sizeof(conn_impl.from); + conn_impl.socket = ::accept(_p->serverSocket, reinterpret_cast(&conn_impl.from), &from_len); conn_impl.id = _p->nextClientID++; if (!_p->nextClientID) diff --git a/src/spice2x/overlay/notifications.cpp b/src/spice2x/overlay/notifications.cpp index ae3402b..da3bcc3 100644 --- a/src/spice2x/overlay/notifications.cpp +++ b/src/spice2x/overlay/notifications.cpp @@ -172,7 +172,9 @@ namespace overlay::notifications { // small gutter past the accent bar, then wrapped text ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f)); ImGui::SameLine(); - ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X)); + // wrap pos is in window-local space, not screen space - adding win_pos.x here + // pushed the boundary far past the window's own width, so it never wrapped + ImGui::PushTextWrapPos(win_size.x - apply_scaling(TOAST_PAD_X)); ImGui::TextUnformatted(n.text.c_str()); ImGui::PopTextWrapPos();