From f78c78cfc85347c8d787790070d6471a2e516fc9 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Thu, 4 Sep 2025 03:04:14 +0300 Subject: [PATCH 01/11] feat(docker): Add spdlog to runtime dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 779b9f5..d3a4a2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ ENV LD_LIBRARY_PATH=/lib:/usr/lib:/usr/local/lib64 # Install runtime dependencies RUN apk update \ - && apk add --no-cache openssl libstdc++ supervisor coreutils procps net-tools sqlite sqlite-dev \ + && apk add --no-cache openssl libstdc++ supervisor coreutils procps net-tools sqlite sqlite-dev spdlog \ && rm -rf /var/cache/apk/* # Copy binaries from the builder stage From 57b6292194ef45dd7c55cc6520ff6fc848e41ad2 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Thu, 4 Sep 2025 03:06:37 +0300 Subject: [PATCH 02/11] feat(SLSManager): Add disconnect_publisher method to manage publisher disconnections --- slscore/SLSManager.cpp | 37 +++++++++++++++++++++++++++++++++++++ slscore/SLSManager.hpp | 1 + 2 files changed, 38 insertions(+) diff --git a/slscore/SLSManager.cpp b/slscore/SLSManager.cpp index 72a38df..01967d4 100644 --- a/slscore/SLSManager.cpp +++ b/slscore/SLSManager.cpp @@ -288,6 +288,43 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, return ret; } +bool CSLSManager::disconnect_publisher(const std::string& player_key) { + // Find the publisher key using the player key + char* publisher_key = find_publisher_by_player_key(const_cast(player_key.c_str())); + if (!publisher_key) { + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for player key: %s", this, player_key.c_str()); + return false; + } + + // Search for the publisher in all server instances + CSLSRole *role = nullptr; + for (int i = 0; i < m_server_count; i++) { + CSLSMapPublisher *publisher_map = &m_map_publisher[i]; + role = publisher_map->get_publisher(publisher_key); + if (role != nullptr) { + break; + } + } + + if (role == nullptr) { + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher role not found: %s (mapped from player key: %s)", + this, publisher_key, player_key.c_str()); + return false; + } + + // Disconnect the publisher + sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s (player key: %s)", + this, publisher_key, player_key.c_str()); + + // Call on_close to notify any HTTP callbacks + role->on_close(); + + // Mark the role as invalid to trigger cleanup in the next cycle + role->invalid_srt(); + + return true; +} + json CSLSManager::create_legacy_json_stats_for_publisher(CSLSRole *role, int clear) { json ret = json::object(); SRT_TRACEBSTATS stats; diff --git a/slscore/SLSManager.hpp b/slscore/SLSManager.hpp index d720084..108ad8b 100644 --- a/slscore/SLSManager.hpp +++ b/slscore/SLSManager.hpp @@ -101,6 +101,7 @@ public : json create_legacy_json_stats_for_publisher(CSLSRole *role, int clear); json create_json_stats_for_publisher(CSLSRole *role, int clear); char* find_publisher_by_player_key(char *player_key); + bool disconnect_publisher(const std::string& player_key); void get_stat_info(std::string &info); static int stat_client_callback(void *p, HTTP_CALLBACK_TYPE type, void *v, void* context); From f8e580a16f3d7810dc10866f6fd096b37f6aaf83 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Thu, 4 Sep 2025 03:07:29 +0300 Subject: [PATCH 03/11] feat(API): Add disconnect publisher endpoint and update documentation --- .vscode/settings.json | 3 ++ API.md | 58 ++++++++++++++++++++++++++++++++++ slscore/SLSApiServer.cpp | 67 ++++++++++++++++++++++++++++++++++++++++ slscore/SLSApiServer.hpp | 1 + test_disconnect_api.md | 0 5 files changed, 129 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 test_disconnect_api.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2efadef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.colorTheme": "Visual Studio Dark" +} \ No newline at end of file diff --git a/API.md b/API.md index 28aba96..f0ba5c4 100644 --- a/API.md +++ b/API.md @@ -115,6 +115,46 @@ Authorization: Bearer } ``` +### Publisher Management + +#### Disconnect Publisher + +Disconnect an active publishing operation. + +``` +DELETE /api/disconnect/{player_id} +Authorization: Bearer +``` + +**Required Permissions:** `admin` or `write` + +**Parameters:** +- `player_id` (path) - The player ID associated with the publisher to disconnect + +**Response:** +- `200 OK` - Publisher disconnected successfully + ```json + { + "status": "success", + "message": "Publisher disconnected successfully" + } + ``` +- `401 Unauthorized` - Invalid or missing API key +- `403 Forbidden` - Insufficient permissions +- `404 Not Found` - Publisher not found or not currently streaming + ```json + { + "status": "error", + "message": "Publisher not found or not currently streaming" + } + ``` + +**Example:** +```bash +curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \ + http://hostname:8080/api/disconnect/live_stream +``` + ### Statistics #### Get Publisher Statistics @@ -209,6 +249,7 @@ The API implements rate limiting to prevent abuse. Each endpoint type has its ow - GET /api/stream-ids - POST /api/stream-ids - DELETE /api/stream-ids/{player_id} + - DELETE /api/disconnect/{player_id} - **Statistics** (`stats`): 300 requests per minute per IP (configurable via `rate_limit_stats`) - GET /stats/{player_id} - **Configuration** (`config`): 20 requests per minute per IP (configurable via `rate_limit_config`) @@ -319,6 +360,10 @@ curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \ # Get statistics curl -H "Authorization: Bearer YOUR_API_KEY" \ http://hostname:8080/stats/live_stream + +# Disconnect publisher +curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \ + http://hostname:8080/api/disconnect/live_stream ``` ### Python @@ -342,6 +387,9 @@ data = { "description": "Main studio feed" } response = requests.post(f"{BASE_URL}/api/stream-ids", json=data, headers=headers) + +# Disconnect publisher +response = requests.delete(f"{BASE_URL}/api/disconnect/live_stream", headers=headers) ``` ### JavaScript/Fetch @@ -374,6 +422,16 @@ fetch(`${BASE_URL}/api/stream-ids`, { }) .then(response => response.json()) .then(data => console.log(data)); + +// Disconnect publisher +fetch(`${BASE_URL}/api/disconnect/live_stream`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${API_KEY}` + } +}) +.then(response => response.json()) +.then(data => console.log(data)); ``` ## Security Considerations diff --git a/slscore/SLSApiServer.cpp b/slscore/SLSApiServer.cpp index b93c7dc..8a276c9 100644 --- a/slscore/SLSApiServer.cpp +++ b/slscore/SLSApiServer.cpp @@ -176,6 +176,11 @@ void CSLSApiServer::setupEndpoints() { m_server.Post("/api/keys", [this](const httplib::Request& req, httplib::Response& res) { handleApiKeys(req, res); }); + + // Publisher disconnect endpoint + m_server.Delete(R"(/api/disconnect/(.+))", [this](const httplib::Request& req, httplib::Response& res) { + handleDisconnectPublisher(req, res); + }); } void CSLSApiServer::handleHealth(const httplib::Request& req, httplib::Response& res) { @@ -440,4 +445,66 @@ void CSLSApiServer::handleApiKeys(const httplib::Request& req, httplib::Response error["message"] = "Failed to create API key"; res.set_content(error.dump(), "application/json"); } +} + +void CSLSApiServer::handleDisconnectPublisher(const httplib::Request& req, httplib::Response& res) { + setCorsHeaders(res); + + // Rate limiting + if (!checkRateLimit(req.remote_addr, "api")) { + res.status = 429; + json error; + error["status"] = "error"; + error["message"] = "Rate limit exceeded"; + res.set_content(error.dump(), "application/json"); + return; + } + + // Authentication with admin or write permissions check + std::string permissions; + if (!authenticateRequest(req, res, permissions)) { + return; + } + + if (permissions != "admin" && permissions != "write") { + res.status = 403; + json error; + error["status"] = "error"; + error["message"] = "Admin or write permissions required"; + res.set_content(error.dump(), "application/json"); + CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7), + req.path, req.method, req.remote_addr, 403); + return; + } + + std::string player_id = req.matches[1]; + + if (!m_sls_manager) { + res.status = 500; + json error; + error["status"] = "error"; + error["message"] = "SLS manager not available"; + res.set_content(error.dump(), "application/json"); + return; + } + + // Attempt to disconnect the publisher + if (m_sls_manager->disconnect_publisher(player_id)) { + json response; + response["status"] = "success"; + response["message"] = "Publisher disconnected successfully"; + res.set_content(response.dump(), "application/json"); + + CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7), + req.path, req.method, req.remote_addr, 200); + } else { + res.status = 404; + json error; + error["status"] = "error"; + error["message"] = "Publisher not found or not currently streaming"; + res.set_content(error.dump(), "application/json"); + + CSLSDatabase::getInstance().logAccess(req.get_header_value("Authorization").substr(7), + req.path, req.method, req.remote_addr, 404); + } } \ No newline at end of file diff --git a/slscore/SLSApiServer.hpp b/slscore/SLSApiServer.hpp index 0f0cd3c..ccce345 100644 --- a/slscore/SLSApiServer.hpp +++ b/slscore/SLSApiServer.hpp @@ -86,6 +86,7 @@ class CSLSApiServer { void handleStats(const httplib::Request& req, httplib::Response& res); void handleConfig(const httplib::Request& req, httplib::Response& res); void handleApiKeys(const httplib::Request& req, httplib::Response& res); + void handleDisconnectPublisher(const httplib::Request& req, httplib::Response& res); }; #endif // _SLS_API_SERVER_HPP_ \ No newline at end of file diff --git a/test_disconnect_api.md b/test_disconnect_api.md new file mode 100644 index 0000000..e69de29 From cbd5b07ea9fa183faf839e13dc4d254f22cf586f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abdulkadir=20=C3=96zyurt?= <92672409+abdulkadirozyurt@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:37:58 +0300 Subject: [PATCH 04/11] Delete test_disconnect_api.md --- test_disconnect_api.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test_disconnect_api.md diff --git a/test_disconnect_api.md b/test_disconnect_api.md deleted file mode 100644 index e69de29..0000000 From 982163f2b68394ed2e8228be4ba4ecab3182eadf Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Sat, 13 Sep 2025 23:09:26 +0300 Subject: [PATCH 05/11] SLSApiServer.cpp: Add publisher disconnect API endpoint - Implemented DELETE /api/disconnect/{publisher_id} endpoint for disconnecting active publishing operations by publisher ID. - Updated handler to use publisher_id directly instead of player_id. - Improved permission checks and error handling for disconnect operation. - Refactored code for clarity and RESTful compliance. --- slscore/SLSApiServer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slscore/SLSApiServer.cpp b/slscore/SLSApiServer.cpp index 8a276c9..f7e7607 100644 --- a/slscore/SLSApiServer.cpp +++ b/slscore/SLSApiServer.cpp @@ -477,7 +477,7 @@ void CSLSApiServer::handleDisconnectPublisher(const httplib::Request& req, httpl return; } - std::string player_id = req.matches[1]; + std::string publisher_id = req.matches[1]; if (!m_sls_manager) { res.status = 500; @@ -489,7 +489,7 @@ void CSLSApiServer::handleDisconnectPublisher(const httplib::Request& req, httpl } // Attempt to disconnect the publisher - if (m_sls_manager->disconnect_publisher(player_id)) { + if (m_sls_manager->disconnect_publisher(publisher_id)) { json response; response["status"] = "success"; response["message"] = "Publisher disconnected successfully"; From aa20947aed922535fe17fdb14e7efd1b086dcf1e Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Sat, 13 Sep 2025 23:11:02 +0300 Subject: [PATCH 06/11] SLSManager.hpp: Refactor disconnect and stats API to use publisher_id - Remove all player_id and player-to-publisher mapping logic from the interface. - Update disconnect_publisher and generate_json_for_publisher signatures to use publisher_id directly. - Remove find_publisher_by_player_key declaration. - Clarify API for publisher-based operations. --- slscore/SLSManager.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slscore/SLSManager.hpp b/slscore/SLSManager.hpp index 108ad8b..1e6f551 100644 --- a/slscore/SLSManager.hpp +++ b/slscore/SLSManager.hpp @@ -100,8 +100,7 @@ public : json generate_json_for_publisher(std::string publisherName, int clear, bool legacy = false); json create_legacy_json_stats_for_publisher(CSLSRole *role, int clear); json create_json_stats_for_publisher(CSLSRole *role, int clear); - char* find_publisher_by_player_key(char *player_key); - bool disconnect_publisher(const std::string& player_key); + bool disconnect_publisher(const std::string& publisher_id); void get_stat_info(std::string &info); static int stat_client_callback(void *p, HTTP_CALLBACK_TYPE type, void *v, void* context); From c9070ec06b9438e445b01e7228877df150f25e85 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Sat, 13 Sep 2025 23:11:55 +0300 Subject: [PATCH 07/11] SLSManager.cpp: Refactor disconnect logic to use publisher_id - Remove all player_id and player-to-publisher mapping logic from implementation. - Update disconnect_publisher and generate_json_for_publisher to use publisher_id directly. - Remove find_publisher_by_player_key and related code. - Fix all syntax and brace issues from refactor. - Ensure all publisher operations are consistent and robust. --- slscore/SLSManager.cpp | 87 ++++++++---------------------------------- 1 file changed, 15 insertions(+), 72 deletions(-) diff --git a/slscore/SLSManager.cpp b/slscore/SLSManager.cpp index 01967d4..baa85c2 100644 --- a/slscore/SLSManager.cpp +++ b/slscore/SLSManager.cpp @@ -61,8 +61,8 @@ CSLSManager::CSLSManager() m_map_data = NULL; m_map_publisher = NULL; m_map_puller = NULL; - m_map_pusher = NULL; + m_map_pusher = NULL; } CSLSManager::~CSLSManager() @@ -195,62 +195,19 @@ int CSLSManager::start() } -char* CSLSManager::find_publisher_by_player_key(char *player_key) { - // First check stream ID database - std::string publisher_id = CSLSDatabase::getInstance().getPublisherFromPlayer(player_key); - if (!publisher_id.empty()) { - static thread_local char mapped_publisher[512]; - strncpy(mapped_publisher, publisher_id.c_str(), sizeof(mapped_publisher) - 1); - mapped_publisher[sizeof(mapped_publisher) - 1] = '\0'; - return mapped_publisher; - } - - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' not found in database", - this, player_key); - - // If not found in database, check if it's a direct publisher key - CSLSRole* role = nullptr; - for (int i = 0; i < m_server_count; i++) { - role = m_map_publisher[i].get_publisher(player_key); - if (role != nullptr) { - break; - } - } - - if (role != NULL) { - sls_log(SLS_LOG_INFO, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' is a publisher key", - this, player_key); - return player_key; - } - - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, no publisher found for player key '%s'", - this, player_key); - return NULL; -} - -json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, bool legacy) { +// ...existing code... +json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clear, bool legacy) { json ret; ret["status"] = "error"; // Validate input - if (playerKey.empty()) { - ret["message"] = "Player key is required"; - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty player key provided", this); + if (publisher_id.empty()) { + ret["message"] = "Publisher ID is required"; + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty publisher_id provided", this); return ret; } - // Validate player key and get mapped publisher key - char* mapped_publisher = find_publisher_by_player_key(const_cast(playerKey.c_str())); - if (mapped_publisher == NULL) { - ret["message"] = "Invalid player key"; - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, invalid player key: %s", - this, playerKey.c_str()); - return ret; - } - - std::string publisher_key(mapped_publisher); - if (legacy) { ret["publishers"] = json::object(); } @@ -261,7 +218,7 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, CSLSRole *role = nullptr; for (int i = 0; i < m_server_count; i++) { CSLSMapPublisher *publisher_map = &m_map_publisher[i]; - role = publisher_map->get_publisher(publisher_key.c_str()); + role = publisher_map->get_publisher(publisher_id.c_str()); if (role != nullptr) { break; } @@ -269,8 +226,8 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, if (role == nullptr) { ret["message"] = "Publisher is currently not streaming"; - sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s (mapped from player key: %s)", - this, publisher_key.c_str(), playerKey.c_str()); + sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s", + this, publisher_id.c_str()); return ret; } @@ -282,46 +239,32 @@ json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, } ret.erase("message"); - sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s (player key: %s)", - this, legacy ? "legacy" : "modern", publisher_key.c_str(), playerKey.c_str()); + sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s", + this, legacy ? "legacy" : "modern", publisher_id.c_str()); return ret; } bool CSLSManager::disconnect_publisher(const std::string& player_key) { - // Find the publisher key using the player key - char* publisher_key = find_publisher_by_player_key(const_cast(player_key.c_str())); - if (!publisher_key) { - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for player key: %s", this, player_key.c_str()); - return false; - } - - // Search for the publisher in all server instances + // Search for the publisher in all server instances using publisher_id CSLSRole *role = nullptr; for (int i = 0; i < m_server_count; i++) { CSLSMapPublisher *publisher_map = &m_map_publisher[i]; - role = publisher_map->get_publisher(publisher_key); + role = publisher_map->get_publisher(player_key); // player_key artık publisher_id olarak kullanılıyor if (role != nullptr) { break; } } - if (role == nullptr) { - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher role not found: %s (mapped from player key: %s)", - this, publisher_key, player_key.c_str()); + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for publisher_id: %s", this, player_key.c_str()); return false; } - // Disconnect the publisher - sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s (player key: %s)", - this, publisher_key, player_key.c_str()); - + sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s", this, player_key.c_str()); // Call on_close to notify any HTTP callbacks role->on_close(); - // Mark the role as invalid to trigger cleanup in the next cycle role->invalid_srt(); - return true; } From 07672022550f6b8f354ddc60592a5d8398f0c171 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Sat, 13 Sep 2025 23:12:15 +0300 Subject: [PATCH 08/11] API.md: Update documentation for publisher_id-based disconnect endpoint - Document new API endpoint for disconnecting publishers using publisher_id. - Remove references to player_id and player-to-publisher mapping. - Clarify usage and expected parameters for disconnect operation. - Ensure documentation matches refactored backend logic. --- API.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/API.md b/API.md index f0ba5c4..52393af 100644 --- a/API.md +++ b/API.md @@ -117,19 +117,20 @@ Authorization: Bearer ### Publisher Management + #### Disconnect Publisher -Disconnect an active publishing operation. +Disconnect an active publishing operation by publisher ID. ``` -DELETE /api/disconnect/{player_id} +DELETE /api/disconnect/{publisher_id} Authorization: Bearer ``` **Required Permissions:** `admin` or `write` **Parameters:** -- `player_id` (path) - The player ID associated with the publisher to disconnect +- `publisher_id` (path) - The publisher ID to disconnect **Response:** - `200 OK` - Publisher disconnected successfully @@ -152,7 +153,7 @@ Authorization: Bearer **Example:** ```bash curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \ - http://hostname:8080/api/disconnect/live_stream + http://hostname:8080/api/disconnect/publisher_123 ``` ### Statistics From 342de7fbc69ee6e67b79226fb625738ec2c15cd6 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Tue, 14 Oct 2025 17:46:54 +0300 Subject: [PATCH 09/11] refactor(SLSManager): update disconnect logic to use publisher_key - Rename parameter from player_key to publisher_key in disconnect_publisher method. - Update logging messages to reflect the change from player_key to publisher_key. --- .vscode/settings.json | 3 --- slscore/SLSManager.cpp | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 2efadef..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "workbench.colorTheme": "Visual Studio Dark" -} \ No newline at end of file diff --git a/slscore/SLSManager.cpp b/slscore/SLSManager.cpp index baa85c2..c955599 100644 --- a/slscore/SLSManager.cpp +++ b/slscore/SLSManager.cpp @@ -245,22 +245,22 @@ json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clea return ret; } -bool CSLSManager::disconnect_publisher(const std::string& player_key) { - // Search for the publisher in all server instances using publisher_id +bool CSLSManager::disconnect_publisher(const std::string& publisher_key) { + // Search for the publisher in all server instances using publisher_key CSLSRole *role = nullptr; for (int i = 0; i < m_server_count; i++) { CSLSMapPublisher *publisher_map = &m_map_publisher[i]; - role = publisher_map->get_publisher(player_key); // player_key artık publisher_id olarak kullanılıyor + role = publisher_map->get_publisher(publisher_key); // publisher_key is used as publisher_id if (role != nullptr) { break; } } if (role == nullptr) { - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for publisher_id: %s", this, player_key.c_str()); + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for publisher_key: %s", this, publisher_key.c_str()); return false; } // Disconnect the publisher - sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s", this, player_key.c_str()); + sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s", this, publisher_key.c_str()); // Call on_close to notify any HTTP callbacks role->on_close(); // Mark the role as invalid to trigger cleanup in the next cycle From 15154cb73430f8419e25200882fbdb684573c95a Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Tue, 14 Oct 2025 23:59:34 +0300 Subject: [PATCH 10/11] Improve config parsing robustness: handle whitespace, comments, and line endings for cross-platform compatibility - Enhance trim and replace logic to ensure config files are parsed correctly regardless of OS or editor - Improve error logging and block parsing for better maintainability - Prevent parsing errors caused by stray whitespace, tabs, or comment lines - Ensures reliable config loading in Docker and all deployment environments --- slscore/conf.cpp | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/slscore/conf.cpp b/slscore/conf.cpp index 980d180..d88e041 100644 --- a/slscore/conf.cpp +++ b/slscore/conf.cpp @@ -166,12 +166,25 @@ vector sls_conf_string_split(const string& str, const string& delim) string& trim(string &s) { - if (s.empty()) - { + if (s.empty()) { + return s; + } + + const char *whitespace = " \t\r\n"; + + size_t start = s.find_first_not_of(whitespace); + if (start == string::npos) { + s.clear(); return s; } - s.erase(0,s.find_first_not_of(" ")); - s.erase(s.find_last_not_of(" ") + 1); + if (start > 0) { + s.erase(0, start); + } + + size_t end = s.find_last_not_of(whitespace); + if (end != string::npos && end + 1 < s.size()) { + s.erase(end + 1); + } return s; } @@ -306,7 +319,7 @@ int sls_conf_parse_block(ifstream& ifs, int& line, sls_conf_base_t * b, bool& ch } } else if (line_end_flag == "}" ) { if (str_line != line_end_flag) { - sls_log(SLS_LOG_ERROR, "line:%d=‘%s’, end indicator ‘}’ with more info.", str_line.c_str(), line); + sls_log(SLS_LOG_ERROR, "line:%d=‘%s’, end indicator ‘}’ with more info.", line, str_line.c_str()); ret = SLS_ERROR; break; } @@ -316,7 +329,7 @@ int sls_conf_parse_block(ifstream& ifs, int& line, sls_conf_base_t * b, bool& ch break; } else { - sls_log(SLS_LOG_ERROR, "line:%d='%s', invalid end flag, except ';', '{', '}',", str_line.c_str(), line); + sls_log(SLS_LOG_ERROR, "line:%d='%s', invalid end flag, except ';', '{', '}',", line, str_line.c_str()); ret = SLS_ERROR; break; } From 5d44ad996e27a72ca66fa54bf0f963c43161a608 Mon Sep 17 00:00:00 2001 From: abdulkadirozyurt Date: Wed, 15 Oct 2025 00:34:57 +0300 Subject: [PATCH 11/11] feat(SLSManager): enhance publisher disconnection logic - Update disconnect logic to support both publisher ID and player ID. - Refactor generate_json_for_publisher to accept player key instead of publisher ID. - Add find_publisher_by_player_key method to resolve player keys to publisher IDs. - Improve logging for publisher disconnection and JSON generation processes. --- .gitignore | 5 +++ API.md | 6 +-- slscore/SLSManager.cpp | 94 +++++++++++++++++++++++++++++++++++------- slscore/SLSManager.hpp | 6 ++- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 27aef11..f98ceca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .DS_Store obj/*.o + +.github/copilot-instructions.md + +memory-bank +old-files \ No newline at end of file diff --git a/API.md b/API.md index 52393af..19962fc 100644 --- a/API.md +++ b/API.md @@ -120,7 +120,7 @@ Authorization: Bearer #### Disconnect Publisher -Disconnect an active publishing operation by publisher ID. +Disconnect an active publishing operation by publisher or player ID. ``` DELETE /api/disconnect/{publisher_id} @@ -130,7 +130,7 @@ Authorization: Bearer **Required Permissions:** `admin` or `write` **Parameters:** -- `publisher_id` (path) - The publisher ID to disconnect +- `publisher_id` (path) - Publisher ID **or** player ID associated with the stream **Response:** - `200 OK` - Publisher disconnected successfully @@ -153,7 +153,7 @@ Authorization: Bearer **Example:** ```bash curl -X DELETE -H "Authorization: Bearer YOUR_API_KEY" \ - http://hostname:8080/api/disconnect/publisher_123 + http://hostname:8080/api/disconnect/live_stream ``` ### Statistics diff --git a/slscore/SLSManager.cpp b/slscore/SLSManager.cpp index c955599..081b5bb 100644 --- a/slscore/SLSManager.cpp +++ b/slscore/SLSManager.cpp @@ -195,19 +195,67 @@ int CSLSManager::start() } +char* CSLSManager::find_publisher_by_player_key(char *player_key) { + if (player_key == NULL || strlen(player_key) == 0) { + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, empty player key provided", this); + return NULL; + } + + // First check stream ID database + std::string publisher_id = CSLSDatabase::getInstance().getPublisherFromPlayer(player_key); + if (!publisher_id.empty()) { + static thread_local char mapped_publisher[512]; + strncpy(mapped_publisher, publisher_id.c_str(), sizeof(mapped_publisher) - 1); + mapped_publisher[sizeof(mapped_publisher) - 1] = '\0'; + + return mapped_publisher; + } + + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' not found in database", + this, player_key); + + // If not found in database, check if it's a direct publisher key + CSLSRole* role = nullptr; + for (int i = 0; i < m_server_count; i++) { + role = m_map_publisher[i].get_publisher(player_key); + if (role != nullptr) { + break; + } + } -// ...existing code... -json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clear, bool legacy) { + if (role != NULL) { + sls_log(SLS_LOG_INFO, "[%p]CSLSManager::find_publisher_by_player_key, player key '%s' is a publisher key", + this, player_key); + return player_key; + } + + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::find_publisher_by_player_key, no publisher found for player key '%s'", + this, player_key); + return NULL; +} + +json CSLSManager::generate_json_for_publisher(std::string playerKey, int clear, bool legacy) { json ret; ret["status"] = "error"; // Validate input - if (publisher_id.empty()) { - ret["message"] = "Publisher ID is required"; - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty publisher_id provided", this); + if (playerKey.empty()) { + ret["message"] = "Player key is required"; + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, empty player key provided", this); return ret; } + // Validate player key and get mapped publisher key + char* mapped_publisher = find_publisher_by_player_key(const_cast(playerKey.c_str())); + if (mapped_publisher == NULL) { + ret["message"] = "Invalid player key"; + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::generate_json_for_publisher, invalid player key: %s", + this, playerKey.c_str()); + return ret; + } + + std::string publisher_key(mapped_publisher); + if (legacy) { ret["publishers"] = json::object(); } @@ -218,7 +266,7 @@ json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clea CSLSRole *role = nullptr; for (int i = 0; i < m_server_count; i++) { CSLSMapPublisher *publisher_map = &m_map_publisher[i]; - role = publisher_map->get_publisher(publisher_id.c_str()); + role = publisher_map->get_publisher(publisher_key.c_str()); if (role != nullptr) { break; } @@ -226,8 +274,8 @@ json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clea if (role == nullptr) { ret["message"] = "Publisher is currently not streaming"; - sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s", - this, publisher_id.c_str()); + sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, publisher not found: %s (mapped from player key: %s)", + this, publisher_key.c_str(), playerKey.c_str()); return ret; } @@ -239,28 +287,44 @@ json CSLSManager::generate_json_for_publisher(std::string publisher_id, int clea } ret.erase("message"); - sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s", - this, legacy ? "legacy" : "modern", publisher_id.c_str()); + sls_log(SLS_LOG_DEBUG, "[%p]CSLSManager::generate_json_for_publisher, returning %s stats for publisher: %s (player key: %s)", + this, legacy ? "legacy" : "modern", publisher_key.c_str(), playerKey.c_str()); return ret; } -bool CSLSManager::disconnect_publisher(const std::string& publisher_key) { - // Search for the publisher in all server instances using publisher_key +bool CSLSManager::disconnect_publisher(const std::string& key) { + if (key.empty()) { + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, empty key provided", this); + return false; + } + + char* mapped_publisher = find_publisher_by_player_key(const_cast(key.c_str())); + if (mapped_publisher == NULL) { + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, unable to resolve publisher for key: %s", this, key.c_str()); + return false; + } + + std::string publisher_key(mapped_publisher); + + // Search for the publisher in all server instances using resolved publisher key CSLSRole *role = nullptr; for (int i = 0; i < m_server_count; i++) { CSLSMapPublisher *publisher_map = &m_map_publisher[i]; - role = publisher_map->get_publisher(publisher_key); // publisher_key is used as publisher_id + role = publisher_map->get_publisher(publisher_key.c_str()); if (role != nullptr) { break; } } if (role == nullptr) { - sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for publisher_key: %s", this, publisher_key.c_str()); + sls_log(SLS_LOG_WARNING, "[%p]CSLSManager::disconnect_publisher, publisher not found for key: %s (resolved publisher: %s)", + this, key.c_str(), publisher_key.c_str()); return false; } + // Disconnect the publisher - sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s", this, publisher_key.c_str()); + sls_log(SLS_LOG_INFO, "[%p]CSLSManager::disconnect_publisher, disconnecting publisher: %s (requested key: %s)", + this, publisher_key.c_str(), key.c_str()); // Call on_close to notify any HTTP callbacks role->on_close(); // Mark the role as invalid to trigger cleanup in the next cycle diff --git a/slscore/SLSManager.hpp b/slscore/SLSManager.hpp index 1e6f551..3faa5f1 100644 --- a/slscore/SLSManager.hpp +++ b/slscore/SLSManager.hpp @@ -97,10 +97,10 @@ public : bool is_single_thread(); int check_invalid(); - json generate_json_for_publisher(std::string publisherName, int clear, bool legacy = false); + json generate_json_for_publisher(std::string playerKey, int clear, bool legacy = false); json create_legacy_json_stats_for_publisher(CSLSRole *role, int clear); json create_json_stats_for_publisher(CSLSRole *role, int clear); - bool disconnect_publisher(const std::string& publisher_id); + bool disconnect_publisher(const std::string& key); void get_stat_info(std::string &info); static int stat_client_callback(void *p, HTTP_CALLBACK_TYPE type, void *v, void* context); @@ -118,6 +118,8 @@ public : CSLSRoleList * m_list_role; CSLSGroup * m_single_group; + + char* find_publisher_by_player_key(char *player_key); };