Replace master branch with local files

This commit is contained in:
doomtube 2025-08-03 21:53:15 -04:00
commit 875a53f499
60 changed files with 21637 additions and 0 deletions

View file

@ -0,0 +1,347 @@
#include "AuthService.h"
#include "DatabaseService.h"
#include "RedisHelper.h"
#include <drogon/utils/Utilities.h>
#include <regex>
#include <random>
using namespace drogon;
using namespace drogon::orm;
bool AuthService::validatePassword(const std::string& password, std::string& error) {
if (password.length() < 8) {
error = "Password must be at least 8 characters long";
return false;
}
if (!std::regex_search(password, std::regex("[0-9]"))) {
error = "Password must contain at least one number";
return false;
}
if (!std::regex_search(password, std::regex("[!@#$%^&*(),.?\":{}|<>]"))) {
error = "Password must contain at least one symbol";
return false;
}
return true;
}
void AuthService::registerUser(const std::string& username, const std::string& password,
const std::string& publicKey, const std::string& fingerprint,
std::function<void(bool, const std::string&, int64_t)> callback) {
// Validate username
if (username.length() < 3 || username.length() > 30) {
callback(false, "Username must be between 3 and 30 characters", 0);
return;
}
if (!std::regex_match(username, std::regex("^[a-zA-Z0-9_]+$"))) {
callback(false, "Username can only contain letters, numbers, and underscores", 0);
return;
}
// Validate password
std::string error;
if (!validatePassword(password, error)) {
callback(false, error, 0);
return;
}
auto dbClient = app().getDbClient();
// Check if username exists
*dbClient << "SELECT id FROM users WHERE username = $1"
<< username
>> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r) {
if (!r.empty()) {
callback(false, "Username already exists", 0);
return;
}
// Check if fingerprint exists
*dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1"
<< fingerprint
>> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r2) {
if (!r2.empty()) {
callback(false, "This PGP key is already registered", 0);
return;
}
// Hash password
std::string hash = BCrypt::generateHash(password);
// Begin transaction
auto trans = dbClient->newTransaction();
// Insert user with explicit false values for booleans
*trans << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only) VALUES ($1, $2, false, false, false) RETURNING id"
<< username << hash
>> [trans, publicKey, fingerprint, callback](const Result& r3) {
if (r3.empty()) {
callback(false, "Failed to create user", 0);
return;
}
int64_t userId = r3[0]["id"].as<int64_t>();
// Insert PGP key
*trans << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)"
<< userId << publicKey << fingerprint
>> [trans, callback, userId](const Result&) {
// Transaction commits automatically
callback(true, "", userId);
}
>> [trans, callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to insert PGP key: " << e.base().what();
callback(false, "Failed to save PGP key", 0);
};
}
>> [trans, callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to insert user: " << e.base().what();
callback(false, "Registration failed", 0);
};
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "Database error", 0);
};
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "Database error", 0);
};
}
void AuthService::loginUser(const std::string& username, const std::string& password,
std::function<void(bool, const std::string&, const UserInfo&)> callback) {
auto dbClient = app().getDbClient();
*dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at "
"FROM users WHERE username = $1"
<< username
>> [password, callback, this](const Result& r) {
if (r.empty()) {
callback(false, "", UserInfo{});
return;
}
// Check if PGP-only is enabled BEFORE password validation
bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
if (isPgpOnly) {
// Return a specific error for PGP-only accounts
callback(false, "PGP-only login enabled for this account", UserInfo{});
return;
}
std::string hash = r[0]["password_hash"].as<std::string>();
if (!BCrypt::validatePassword(password, hash)) {
callback(false, "", UserInfo{});
return;
}
UserInfo user;
user.id = r[0]["id"].as<int64_t>();
user.username = r[0]["username"].as<std::string>();
user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
user.isPgpOnly = isPgpOnly;
user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
std::string token = generateToken(user);
callback(true, token, user);
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "", UserInfo{});
};
}
void AuthService::initiatePgpLogin(const std::string& username,
std::function<void(bool, const std::string&, const std::string&)> callback) {
auto dbClient = app().getDbClient();
// Generate random challenge
auto bytes = drogon::utils::genRandomString(32);
std::string challenge = drogon::utils::base64Encode(
reinterpret_cast<const unsigned char*>(bytes.data()), bytes.length()
);
// Store challenge in Redis with 5 minute TTL
RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300,
[dbClient, username, challenge, callback](bool stored) {
if (!stored) {
callback(false, "", "");
return;
}
// Get user's latest public key
*dbClient << "SELECT pk.public_key FROM pgp_keys pk "
"JOIN users u ON pk.user_id = u.id "
"WHERE u.username = $1 "
"ORDER BY pk.created_at DESC LIMIT 1"
<< username
>> [callback, challenge](const Result& r) {
if (r.empty()) {
callback(false, "", "");
return;
}
std::string publicKey = r[0]["public_key"].as<std::string>();
callback(true, challenge, publicKey);
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "", "");
};
}
);
}
void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature,
const std::string& challenge,
std::function<void(bool, const std::string&, const UserInfo&)> callback) {
// Get stored challenge from Redis
RedisHelper::getKeyAsync("pgp_challenge:" + username,
[username, signature, challenge, callback, this](const std::string& storedChallenge) {
if (storedChallenge.empty() || storedChallenge != challenge) {
callback(false, "", UserInfo{});
return;
}
// Delete challenge after use
RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {});
// In a real implementation, you would verify the signature here
// For now, we'll trust the client-side verification
auto dbClient = app().getDbClient();
*dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at "
"FROM users WHERE username = $1"
<< username
>> [callback, this](const Result& r) {
if (r.empty()) {
callback(false, "", UserInfo{});
return;
}
UserInfo user;
user.id = r[0]["id"].as<int64_t>();
user.username = r[0]["username"].as<std::string>();
user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
std::string token = generateToken(user);
callback(true, token, user);
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "", UserInfo{});
};
}
);
}
std::string AuthService::generateToken(const UserInfo& user) {
if (jwtSecret_.empty()) {
const char* envSecret = std::getenv("JWT_SECRET");
jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret";
}
auto token = jwt::create()
.set_issuer("streaming-app")
.set_type("JWS")
.set_issued_at(std::chrono::system_clock::now())
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24))
.set_payload_claim("user_id", jwt::claim(std::to_string(user.id)))
.set_payload_claim("username", jwt::claim(user.username))
.set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin)))
.set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer)))
.sign(jwt::algorithm::hs256{jwtSecret_});
return token;
}
bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) {
if (jwtSecret_.empty()) {
const char* envSecret = std::getenv("JWT_SECRET");
jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret";
}
try {
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{jwtSecret_})
.with_issuer("streaming-app");
verifier.verify(decoded);
userInfo.id = std::stoll(decoded.get_payload_claim("user_id").as_string());
userInfo.username = decoded.get_payload_claim("username").as_string();
userInfo.isAdmin = decoded.get_payload_claim("is_admin").as_string() == "1";
userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ?
decoded.get_payload_claim("is_streamer").as_string() == "1" : false;
return true;
} catch (const std::exception& e) {
LOG_DEBUG << "Token validation failed: " << e.what();
return false;
}
}
void AuthService::updatePassword(int64_t userId, const std::string& oldPassword,
const std::string& newPassword,
std::function<void(bool, const std::string&)> callback) {
// Validate new password
std::string error;
if (!validatePassword(newPassword, error)) {
callback(false, error);
return;
}
auto dbClient = app().getDbClient();
// Verify old password
*dbClient << "SELECT password_hash FROM users WHERE id = $1"
<< userId
>> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) {
if (r.empty()) {
callback(false, "User not found");
return;
}
std::string hash = r[0]["password_hash"].as<std::string>();
if (!BCrypt::validatePassword(oldPassword, hash)) {
callback(false, "Incorrect password");
return;
}
// Update password
std::string newHash = BCrypt::generateHash(newPassword);
*dbClient << "UPDATE users SET password_hash = $1 WHERE id = $2"
<< newHash << userId
>> [callback](const Result&) {
callback(true, "");
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Failed to update password: " << e.base().what();
callback(false, "Failed to update password");
};
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "Database error");
};
}

View file

@ -0,0 +1,63 @@
#pragma once
#include <drogon/drogon.h>
#include <bcrypt/BCrypt.hpp>
#include <jwt-cpp/jwt.h>
#include <string>
#include <memory>
struct UserInfo {
int64_t id;
std::string username;
bool isAdmin;
bool isStreamer;
bool isPgpOnly;
std::string bio;
std::string avatarUrl;
std::string pgpOnlyEnabledAt;
};
class AuthService {
public:
static AuthService& getInstance() {
static AuthService instance;
return instance;
}
// User registration
void registerUser(const std::string& username, const std::string& password,
const std::string& publicKey, const std::string& fingerprint,
std::function<void(bool success, const std::string& error, int64_t userId)> callback);
// User login with password
void loginUser(const std::string& username, const std::string& password,
std::function<void(bool success, const std::string& token, const UserInfo& user)> callback);
// User login with PGP (returns challenge)
void initiatePgpLogin(const std::string& username,
std::function<void(bool success, const std::string& challenge, const std::string& publicKey)> callback);
// Verify PGP signature
void verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge,
std::function<void(bool success, const std::string& token, const UserInfo& user)> callback);
// Validate JWT token
bool validateToken(const std::string& token, UserInfo& userInfo);
// Update password
void updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword,
std::function<void(bool success, const std::string& error)> callback);
// Check password requirements
bool validatePassword(const std::string& password, std::string& error);
// Generate JWT token
std::string generateToken(const UserInfo& user);
private:
AuthService() = default;
~AuthService() = default;
AuthService(const AuthService&) = delete;
AuthService& operator=(const AuthService&) = delete;
std::string jwtSecret_;
};

View file

@ -0,0 +1,76 @@
#pragma once
#include <drogon/drogon.h>
namespace middleware {
class CorsMiddleware {
public:
struct Config {
std::vector<std::string> allowOrigins = {"*"};
std::vector<std::string> allowMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"};
std::vector<std::string> allowHeaders = {"Content-Type", "Authorization"};
bool allowCredentials = true;
int maxAge = 86400;
};
static void enable(const Config& config = {}) {
using namespace drogon;
auto cfg = std::make_shared<Config>(config);
auto addHeaders = [cfg](const HttpResponsePtr &resp, const HttpRequestPtr &req) {
std::string origin = req->getHeader("Origin");
// Check if origin is allowed
bool allowed = false;
for (const auto& allowedOrigin : cfg->allowOrigins) {
if (allowedOrigin == "*" || allowedOrigin == origin) {
allowed = true;
break;
}
}
if (allowed) {
resp->addHeader("Access-Control-Allow-Origin", origin.empty() ? "*" : origin);
resp->addHeader("Access-Control-Allow-Methods", joinStrings(cfg->allowMethods, ", "));
resp->addHeader("Access-Control-Allow-Headers", joinStrings(cfg->allowHeaders, ", "));
if (cfg->allowCredentials) {
resp->addHeader("Access-Control-Allow-Credentials", "true");
}
}
};
// Handle preflight requests
app().registerPreRoutingAdvice([cfg, addHeaders](const HttpRequestPtr &req,
AdviceCallback &&acb,
AdviceChainCallback &&accb) {
if (req->getMethod() == Options) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k204NoContent);
addHeaders(resp, req);
resp->addHeader("Access-Control-Max-Age", std::to_string(cfg->maxAge));
acb(resp);
return;
}
accb();
});
// Add CORS headers to all responses
app().registerPostHandlingAdvice([addHeaders](const HttpRequestPtr &req,
const HttpResponsePtr &resp) {
addHeaders(resp, req);
});
}
private:
static std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter) {
std::string result;
for (size_t i = 0; i < strings.size(); ++i) {
result += strings[i];
if (i < strings.size() - 1) result += delimiter;
}
return result;
}
};
} // namespace middleware

View file

@ -0,0 +1,120 @@
#include "DatabaseService.h"
#include "../services/RedisHelper.h"
#include <drogon/orm/DbClient.h>
#include <random>
#include <sstream>
#include <iomanip>
using namespace drogon;
using namespace drogon::orm;
namespace {
void storeKeyInRedis(const std::string& streamKey) {
// Store the stream key in Redis for validation (24 hour TTL)
bool stored = RedisHelper::storeKey("stream_key:" + streamKey, "1", 86400);
if (stored) {
LOG_INFO << "Stored stream key in Redis: " << streamKey;
} else {
LOG_ERROR << "Failed to store key in Redis: " << streamKey;
}
}
std::string generateStreamKey() {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 255);
std::stringstream ss;
for (int i = 0; i < 16; ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << dis(gen);
}
return ss.str();
}
}
void DatabaseService::initialize() {
LOG_INFO << "Initializing Database Service...";
}
void DatabaseService::getUserStreamKey(int64_t userId,
std::function<void(bool, const std::string&)> callback) {
auto dbClient = drogon::app().getDbClient();
*dbClient << "SELECT key FROM stream_keys WHERE user_id = $1 AND is_active = true"
<< userId
>> [callback, userId, dbClient](const Result &r) {
if (!r.empty()) {
std::string key = r[0]["key"].as<std::string>();
// Also store in Redis when retrieved
storeKeyInRedis(key);
callback(true, key);
} else {
// Generate new key for user
std::string newKey = generateStreamKey();
*dbClient << "INSERT INTO stream_keys (user_id, key, is_active) VALUES ($1, $2, true)"
<< userId << newKey
>> [callback, newKey](const Result &) {
storeKeyInRedis(newKey);
callback(true, newKey);
}
>> [callback](const DrogonDbException &e) {
LOG_ERROR << "Failed to create stream key: " << e.base().what();
callback(false, "");
};
}
}
>> [callback](const DrogonDbException &e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false, "");
};
}
void DatabaseService::updateUserStreamKey(int64_t userId,
const std::string& newKey,
std::function<void(bool)> callback) {
auto dbClient = drogon::app().getDbClient();
// Execute as separate queries instead of transaction for simplicity
*dbClient << "UPDATE stream_keys SET is_active = false WHERE user_id = $1"
<< userId
>> [dbClient, userId, newKey, callback](const Result &) {
// Insert new key
*dbClient << "INSERT INTO stream_keys (user_id, key, is_active) VALUES ($1, $2, true)"
<< userId << newKey
>> [callback, newKey](const Result &) {
// Store new key in Redis
storeKeyInRedis(newKey);
callback(true);
}
>> [callback](const DrogonDbException &e) {
LOG_ERROR << "Failed to insert new key: " << e.base().what();
callback(false);
};
}
>> [callback](const DrogonDbException &e) {
LOG_ERROR << "Failed to deactivate old keys: " << e.base().what();
callback(false);
};
}
void DatabaseService::validateStreamKey(const std::string& key,
std::function<void(bool)> callback) {
auto dbClient = drogon::app().getDbClient();
*dbClient << "SELECT 1 FROM stream_keys WHERE key = $1 AND is_active = true"
<< key
>> [callback, key](const Result &r) {
bool valid = !r.empty();
if (valid) {
// Also store in Redis when validated
storeKeyInRedis(key);
}
callback(valid);
}
>> [callback](const DrogonDbException &e) {
LOG_ERROR << "Database error: " << e.base().what();
callback(false);
};
}

View file

@ -0,0 +1,30 @@
#pragma once
#include <drogon/drogon.h>
#include <functional>
#include <memory>
class DatabaseService {
public:
static DatabaseService& getInstance() {
static DatabaseService instance;
return instance;
}
void initialize();
void getUserStreamKey(int64_t userId,
std::function<void(bool, const std::string&)> callback);
void updateUserStreamKey(int64_t userId,
const std::string& newKey,
std::function<void(bool)> callback);
void validateStreamKey(const std::string& key,
std::function<void(bool)> callback);
private:
DatabaseService() = default;
~DatabaseService() = default;
DatabaseService(const DatabaseService&) = delete;
DatabaseService& operator=(const DatabaseService&) = delete;
};

View file

@ -0,0 +1,176 @@
#pragma once
#include <drogon/HttpClient.h>
#include <drogon/utils/Utilities.h>
#include <functional>
#include <string>
// TODO: Consider implementing OME webhooks for real-time updates instead of polling
// OME supports webhooks for stream events (start/stop/etc) which would be more efficient
// than polling. See: https://airensoft.gitbook.io/ovenmediaengine/access-control/admission-webhooks
class OmeClient {
public:
static OmeClient& getInstance() {
static OmeClient instance;
return instance;
}
// Get list of active streams
void getActiveStreams(std::function<void(bool, const Json::Value&)> callback) {
auto request = createRequest(drogon::Get, "/v1/vhosts/default/apps/app/streams");
getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result == drogon::ReqResult::Ok && response && response->getStatusCode() == drogon::k200OK) {
try {
Json::Value json = *response->getJsonObject();
callback(true, json);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse OME response: " << e.what();
Json::Value empty;
callback(false, empty);
}
} else {
LOG_ERROR << "Failed to get active streams from OME";
Json::Value empty;
callback(false, empty);
}
});
}
// Get stats for a specific stream
void getStreamStats(const std::string& streamKey,
std::function<void(bool, const Json::Value&)> callback) {
std::string path = "/v1/stats/current/vhosts/default/apps/app/streams/" + streamKey;
auto request = createRequest(drogon::Get, path);
getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result == drogon::ReqResult::Ok && response) {
if (response->getStatusCode() == drogon::k200OK) {
try {
Json::Value json = *response->getJsonObject();
callback(true, json);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse stats response: " << e.what();
Json::Value empty;
callback(false, empty);
}
} else {
// Not found or error - return empty but success (stream offline)
Json::Value empty;
callback(true, empty);
}
} else {
LOG_ERROR << "Request to OME failed";
Json::Value empty;
callback(false, empty);
}
});
}
// Get detailed stream info including track metadata (resolution, codec, etc.)
void getStreamInfo(const std::string& streamKey,
std::function<void(bool, const Json::Value&)> callback) {
std::string path = "/v1/vhosts/default/apps/app/streams/" + streamKey;
auto request = createRequest(drogon::Get, path);
getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
if (result == drogon::ReqResult::Ok && response) {
if (response->getStatusCode() == drogon::k200OK) {
try {
Json::Value json = *response->getJsonObject();
callback(true, json);
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse stream info response: " << e.what();
Json::Value empty;
callback(false, empty);
}
} else {
// Stream not found or error
Json::Value empty;
callback(false, empty);
}
} else {
LOG_ERROR << "Stream info request to OME failed";
Json::Value empty;
callback(false, empty);
}
});
}
// Disconnect a stream
void disconnectStream(const std::string& streamId,
std::function<void(bool)> callback) {
std::string path = "/v1/vhosts/default/apps/app/streams/" + streamId;
auto request = createRequest(drogon::Delete, path);
getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
bool success = (result == drogon::ReqResult::Ok &&
response &&
response->getStatusCode() == drogon::k200OK);
callback(success);
});
}
private:
OmeClient() = default;
~OmeClient() = default;
OmeClient(const OmeClient&) = delete;
OmeClient& operator=(const OmeClient&) = delete;
std::string getBaseUrl() {
// Check environment variable first
const char* envUrl = std::getenv("OME_API_URL");
if (envUrl) {
return std::string(envUrl);
}
// Try to get from Drogon config
try {
const auto& config = drogon::app().getCustomConfig();
if (config.isMember("ome") && config["ome"].isMember("api_url")) {
return config["ome"]["api_url"].asString();
}
} catch (...) {
// Config not available
}
return "http://ovenmediaengine:8081"; // Default
}
std::string getApiToken() {
// Check environment variable first
const char* envToken = std::getenv("OME_API_TOKEN");
if (envToken) {
return std::string(envToken);
}
// Try to get from Drogon config
try {
const auto& config = drogon::app().getCustomConfig();
if (config.isMember("ome") && config["ome"].isMember("api_token")) {
return config["ome"]["api_token"].asString();
}
} catch (...) {
// Config not available
}
return "your-api-token"; // Default
}
drogon::HttpClientPtr getClient() {
return drogon::HttpClient::newHttpClient(getBaseUrl());
}
drogon::HttpRequestPtr createRequest(drogon::HttpMethod method, const std::string& path) {
auto request = drogon::HttpRequest::newHttpRequest();
request->setMethod(method);
request->setPath(path);
// Add authorization header (OME uses Basic auth with token as username)
const auto token = getApiToken();
const auto b64 = drogon::utils::base64Encode(token);
request->addHeader("Authorization", std::string("Basic ") + b64);
return request;
}
};

View file

@ -0,0 +1,311 @@
#include "RedisHelper.h"
#include <chrono>
#include <cstdlib>
#include <thread>
#include <algorithm>
#include <cstring>
namespace services {
RedisHelper &RedisHelper::instance() {
static RedisHelper inst;
return inst;
}
RedisHelper::RedisHelper() : _initialized(false) {
LOG_INFO << "RedisHelper created (connection will be established on first use)";
}
RedisHelper::~RedisHelper() = default;
void RedisHelper::ensureConnected() {
if (_initialized) return;
std::lock_guard<std::mutex> lock(_initMutex);
if (_initialized) return; // Double-check
try {
sw::redis::ConnectionOptions opts;
opts.host = getRedisHost();
opts.port = getRedisPort();
const char* envPass = std::getenv("REDIS_PASS");
if (envPass && strlen(envPass) > 0) {
opts.password = envPass;
}
opts.socket_timeout = std::chrono::milliseconds(1000);
opts.connect_timeout = std::chrono::milliseconds(1000);
LOG_INFO << "Connecting to Redis at " << opts.host << ":" << opts.port;
_redis = std::make_unique<sw::redis::Redis>(opts);
_redis->ping();
_initialized = true;
LOG_INFO << "Redis connection established successfully";
} catch (const sw::redis::Error& e) {
LOG_ERROR << "Failed to connect to Redis: " << e.what();
throw;
}
}
std::string RedisHelper::getRedisHost() const {
const char* envHost = std::getenv("REDIS_HOST");
if (envHost) return std::string(envHost);
try {
const auto& config = drogon::app().getCustomConfig();
if (config.isMember("redis") && config["redis"].isMember("host")) {
return config["redis"]["host"].asString();
}
} catch (...) {}
return "redis";
}
int RedisHelper::getRedisPort() const {
const char* envPort = std::getenv("REDIS_PORT");
if (envPort) {
try {
return std::stoi(envPort);
} catch (...) {}
}
try {
const auto& config = drogon::app().getCustomConfig();
if (config.isMember("redis") && config["redis"].isMember("port")) {
return config["redis"]["port"].asInt();
}
} catch (...) {}
return 6379;
}
void RedisHelper::executeInThreadPool(std::function<void()> task) {
auto loop = drogon::app().getLoop();
if (!loop) {
LOG_ERROR << "Event loop not available, executing task synchronously";
try {
task();
} catch (const std::exception& e) {
LOG_ERROR << "Error executing task: " << e.what();
}
return;
}
loop->queueInLoop([task = std::move(task)]() {
std::thread([task]() {
try {
task();
} catch (const std::exception& e) {
LOG_ERROR << "Error in thread pool task: " << e.what();
}
}).detach();
});
}
// Define a macro to generate async methods
#define REDIS_ASYNC_IMPL(method, return_type, operation) \
void RedisHelper::method##Async(const std::string &key, \
std::function<void(return_type)> callback) { \
executeAsync<return_type>( \
[this, key]() { \
return _redis->operation; \
}, \
std::move(callback) \
); \
}
// Specialized async methods using the template
void RedisHelper::setexAsync(const std::string &key,
const std::string &value,
long ttlSeconds,
std::function<void(bool)> callback) {
executeAsync<bool>(
[this, key, value, ttlSeconds]() {
_redis->setex(key, ttlSeconds, value);
return true;
},
std::move(callback)
);
}
void RedisHelper::getAsync(const std::string &key,
std::function<void(sw::redis::OptionalString)> callback) {
executeAsync<sw::redis::OptionalString>(
[this, key]() {
return _redis->get(key);
},
std::move(callback)
);
}
void RedisHelper::delAsync(const std::string &key,
std::function<void(bool)> callback) {
executeAsync<bool>(
[this, key]() {
return _redis->del(key) > 0;
},
std::move(callback)
);
}
void RedisHelper::saddAsync(const std::string &setName,
const std::string &value,
std::function<void(bool)> callback) {
executeAsync<bool>(
[this, setName, value]() {
return _redis->sadd(setName, value) > 0;
},
std::move(callback)
);
}
void RedisHelper::sremAsync(const std::string &setName,
const std::string &value,
std::function<void(bool)> callback) {
executeAsync<bool>(
[this, setName, value]() {
return _redis->srem(setName, value) > 0;
},
std::move(callback)
);
}
void RedisHelper::smembersAsync(const std::string &setName,
std::function<void(std::vector<std::string>)> callback) {
executeAsync<std::vector<std::string>>(
[this, setName]() {
std::vector<std::string> members;
_redis->smembers(setName, std::back_inserter(members));
return members;
},
std::move(callback)
);
}
void RedisHelper::keysAsync(const std::string &pattern,
std::function<void(std::vector<std::string>)> callback) {
executeAsync<std::vector<std::string>>(
[this, pattern]() {
std::vector<std::string> keys;
_redis->keys(pattern, std::back_inserter(keys));
return keys;
},
std::move(callback)
);
}
void RedisHelper::expireAsync(const std::string &key,
long ttlSeconds,
std::function<void(bool)> callback) {
executeAsync<bool>(
[this, key, ttlSeconds]() {
return _redis->expire(key, ttlSeconds);
},
std::move(callback)
);
}
// Sync versions for compatibility
std::unique_ptr<sw::redis::Redis> RedisHelper::getConnection() {
ensureConnected();
sw::redis::ConnectionOptions opts;
opts.host = getRedisHost();
opts.port = getRedisPort();
const char* envPass = std::getenv("REDIS_PASS");
if (envPass && strlen(envPass) > 0) {
opts.password = envPass;
}
opts.socket_timeout = std::chrono::milliseconds(200);
opts.connect_timeout = std::chrono::milliseconds(200);
return std::make_unique<sw::redis::Redis>(opts);
}
bool RedisHelper::storeKey(const std::string &key, const std::string &value, int ttl) {
try {
ensureConnected();
if (ttl > 0) {
_redis->setex(key, ttl, value);
} else {
_redis->set(key, value);
}
return true;
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis SET error: " << e.what();
return false;
}
}
std::string RedisHelper::getKey(const std::string &key) {
try {
ensureConnected();
auto val = _redis->get(key);
return val.has_value() ? val.value() : "";
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis GET error: " << e.what();
return "";
}
}
bool RedisHelper::deleteKey(const std::string &key) {
try {
ensureConnected();
return _redis->del(key) > 0;
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis DEL error: " << e.what();
return false;
}
}
bool RedisHelper::addToSet(const std::string &setName, const std::string &value) {
try {
ensureConnected();
return _redis->sadd(setName, value) > 0;
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis SADD error: " << e.what();
return false;
}
}
bool RedisHelper::removeFromSet(const std::string &setName, const std::string &value) {
try {
ensureConnected();
return _redis->srem(setName, value) > 0;
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis SREM error: " << e.what();
return false;
}
}
// Deprecated command executor - simplified
void RedisHelper::executeAsync(const std::string &command,
std::function<void(bool, const std::string&)> callback) {
// For the single use case in the code (EXPIRE), handle it directly
std::istringstream iss(command);
std::string op, key;
long ttl;
iss >> op >> key >> ttl;
if (op == "EXPIRE" || op == "expire") {
expireAsync(key, ttl, [callback](bool success) {
callback(success, success ? "1" : "0");
});
} else {
if (auto loop = drogon::app().getLoop()) {
loop->queueInLoop([callback]() {
callback(false, "Unsupported command in executeAsync. Use specific async methods.");
});
} else {
callback(false, "Unsupported command in executeAsync. Use specific async methods.");
}
}
}
} // namespace services

View file

@ -0,0 +1,190 @@
#pragma once
#include <sw/redis++/redis++.h>
#include <memory>
#include <string>
#include <functional>
#include <mutex>
#include <drogon/drogon.h>
namespace services {
class RedisHelper {
public:
// Singleton accessor
static RedisHelper &instance();
// Generic async execute wrapper
template<typename Result, typename Callback>
void executeAsync(std::function<Result()> redisOp, Callback&& callback) {
executeInThreadPool([this, redisOp = std::move(redisOp),
callback = std::forward<Callback>(callback)]() {
try {
ensureConnected();
auto result = redisOp();
if (auto loop = drogon::app().getLoop()) {
loop->queueInLoop([callback, result = std::move(result)]() {
callback(std::move(result));
});
} else {
callback(std::move(result));
}
} catch (const sw::redis::Error &e) {
LOG_ERROR << "Redis operation error: " << e.what();
if (auto loop = drogon::app().getLoop()) {
loop->queueInLoop([callback]() {
callback(Result{});
});
} else {
callback(Result{});
}
}
});
}
// Async SETEX
void setexAsync(const std::string &key,
const std::string &value,
long ttlSeconds,
std::function<void(bool)> callback);
// Async GET
void getAsync(const std::string &key,
std::function<void(sw::redis::OptionalString)> callback);
// Async DEL
void delAsync(const std::string &key,
std::function<void(bool)> callback);
// Async SADD
void saddAsync(const std::string &setName,
const std::string &value,
std::function<void(bool)> callback);
// Async SREM
void sremAsync(const std::string &setName,
const std::string &value,
std::function<void(bool)> callback);
// Async SMEMBERS
void smembersAsync(const std::string &setName,
std::function<void(std::vector<std::string>)> callback);
// Async KEYS
void keysAsync(const std::string &pattern,
std::function<void(std::vector<std::string>)> callback);
// Async EXPIRE
void expireAsync(const std::string &key,
long ttlSeconds,
std::function<void(bool)> callback);
// Sync versions for compatibility
std::unique_ptr<sw::redis::Redis> getConnection();
bool storeKey(const std::string &key, const std::string &value, int ttl = 0);
std::string getKey(const std::string &key);
bool deleteKey(const std::string &key);
bool addToSet(const std::string &setName, const std::string &value);
bool removeFromSet(const std::string &setName, const std::string &value);
// Compatibility wrappers - keep for backward compatibility
static void storeKeyAsync(const std::string &key, const std::string &value, int ttl,
std::function<void(bool)> callback) {
instance().setexAsync(key, value, ttl, callback);
}
static void getKeyAsync(const std::string &key,
std::function<void(const std::string&)> callback) {
instance().getAsync(key, [callback](sw::redis::OptionalString val) {
callback(val.has_value() ? val.value() : "");
});
}
static void deleteKeyAsync(const std::string &key,
std::function<void(bool)> callback) {
instance().delAsync(key, callback);
}
// Execute arbitrary command asynchronously (deprecated)
void executeAsync(const std::string &command,
std::function<void(bool, const std::string&)> callback);
private:
RedisHelper();
~RedisHelper();
RedisHelper(const RedisHelper &) = delete;
RedisHelper &operator=(const RedisHelper &) = delete;
void ensureConnected();
void executeInThreadPool(std::function<void()> task);
std::string getRedisHost() const;
int getRedisPort() const;
std::unique_ptr<sw::redis::Redis> _redis;
bool _initialized;
std::mutex _initMutex;
};
} // namespace services
// Compatibility layer for existing code
class RedisHelper {
public:
using RedisConnectionPtr = std::unique_ptr<sw::redis::Redis>;
static RedisConnectionPtr getConnection() {
return services::RedisHelper::instance().getConnection();
}
static std::string getRedisHost() {
const char* envHost = std::getenv("REDIS_HOST");
return envHost ? std::string(envHost) : "redis";
}
static int getRedisPort() {
const char* envPort = std::getenv("REDIS_PORT");
return envPort ? std::stoi(envPort) : 6379;
}
static bool storeKey(const std::string& key, const std::string& value, int ttl = 0) {
return services::RedisHelper::instance().storeKey(key, value, ttl);
}
static std::string getKey(const std::string& key) {
return services::RedisHelper::instance().getKey(key);
}
static bool deleteKey(const std::string& key) {
return services::RedisHelper::instance().deleteKey(key);
}
static bool addToSet(const std::string& setName, const std::string& value) {
return services::RedisHelper::instance().addToSet(setName, value);
}
static bool removeFromSet(const std::string& setName, const std::string& value) {
return services::RedisHelper::instance().removeFromSet(setName, value);
}
static void storeKeyAsync(const std::string& key, const std::string& value, int ttl,
std::function<void(bool)> callback) {
services::RedisHelper::storeKeyAsync(key, value, ttl, callback);
}
static void getKeyAsync(const std::string& key,
std::function<void(const std::string&)> callback) {
services::RedisHelper::getKeyAsync(key, callback);
}
static void deleteKeyAsync(const std::string& key,
std::function<void(bool)> callback) {
services::RedisHelper::deleteKeyAsync(key, callback);
}
static void executeAsync(const std::string& command,
std::function<void(bool, const std::string&)> callback) {
services::RedisHelper::instance().executeAsync(command, callback);
}
};

View file

@ -0,0 +1,356 @@
#include "StatsService.h"
#include "../controllers/StreamController.h"
#include "../services/RedisHelper.h"
#include "../services/OmeClient.h"
#include <drogon/HttpClient.h>
#include <drogon/utils/Utilities.h>
using namespace drogon;
// Macro to simplify JSON integer assignments
#define JSON_INT(json, field, value) json[field] = static_cast<Json::Int64>(value)
StatsService::~StatsService() {
shutdown();
}
void StatsService::initialize() {
LOG_INFO << "Initializing Stats Service...";
running_ = true;
drogon::app().registerBeginningAdvice([this]() {
LOG_INFO << "Starting stats polling timer...";
if (auto loop = drogon::app().getLoop()) {
try {
timerId_ = loop->runEvery(
pollInterval_.count(),
[this]() {
if (!running_) return;
try {
pollOmeStats();
} catch (const std::exception& e) {
LOG_ERROR << "Error in stats polling: " << e.what();
}
}
);
LOG_INFO << "Stats polling timer started with " << pollInterval_.count() << "s interval";
} catch (const std::exception& e) {
LOG_ERROR << "Failed to create stats timer: " << e.what();
}
}
});
}
void StatsService::shutdown() {
LOG_INFO << "Shutting down Stats Service...";
running_ = false;
if (timerId_.has_value()) {
if (auto loop = drogon::app().getLoop()) {
loop->invalidateTimer(timerId_.value());
}
timerId_.reset();
}
}
int64_t StatsService::getUniqueViewerCount(const std::string& streamKey) {
try {
auto redis = services::RedisHelper::instance().getConnection();
if (!redis) return 0;
std::vector<std::string> keys;
redis->keys("viewer_token:*", std::back_inserter(keys));
return std::count_if(keys.begin(), keys.end(), [&redis, &streamKey](const auto& tokenKey) {
auto storedKey = redis->get(tokenKey);
return storedKey.has_value() && storedKey.value() == streamKey;
});
} catch (const std::exception& e) {
LOG_ERROR << "Error getting unique viewer count: " << e.what();
return 0;
}
}
void StatsService::pollOmeStats() {
// Get active streams from OME
OmeClient::getInstance().getActiveStreams([this](bool success, const Json::Value& json) {
if (success && json["response"].isArray()) {
for (const auto& stream : json["response"]) {
if (stream.isString()) {
updateStreamStats(stream.asString());
}
}
}
});
// Poll known stream keys from Redis
services::RedisHelper::instance().keysAsync("stream_key:*",
[this](const std::vector<std::string>& keys) {
for (const auto& key : keys) {
if (auto pos = key.find(':'); pos != std::string::npos) {
updateStreamStats(key.substr(pos + 1));
}
}
}
);
}
void StatsService::updateStreamStats(const std::string& streamKey) {
fetchStatsFromOme(streamKey, [this, streamKey](bool success, const StreamStats& stats) {
if (success) {
StreamStats updatedStats = stats;
updatedStats.uniqueViewers = getUniqueViewerCount(streamKey);
storeStatsInRedis(streamKey, updatedStats);
// Update realm in database
updateRealmLiveStatus(streamKey, updatedStats);
// Only broadcast if stream has meaningful data
if (updatedStats.isLive || updatedStats.totalBytesIn > 0 || updatedStats.uniqueViewers > 0) {
Json::Value msg;
msg["type"] = "stats_update";
msg["stream_key"] = streamKey;
auto& s = msg["stats"];
JSON_INT(s, "connections", updatedStats.uniqueViewers);
JSON_INT(s, "raw_connections", updatedStats.currentConnections);
s["bitrate"] = updatedStats.bitrate;
s["resolution"] = updatedStats.resolution;
s["fps"] = updatedStats.fps;
s["codec"] = updatedStats.codec;
s["is_live"] = updatedStats.isLive;
JSON_INT(s, "bytes_in", updatedStats.totalBytesIn);
JSON_INT(s, "bytes_out", updatedStats.totalBytesOut);
// Protocol breakdown
auto& pc = s["protocol_connections"];
JSON_INT(pc, "webrtc", updatedStats.protocolConnections.webrtc);
JSON_INT(pc, "hls", updatedStats.protocolConnections.hls);
JSON_INT(pc, "llhls", updatedStats.protocolConnections.llhls);
JSON_INT(pc, "dash", updatedStats.protocolConnections.dash);
StreamWebSocketController::broadcastStatsUpdate(msg);
}
}
});
}
void StatsService::updateRealmLiveStatus(const std::string& streamKey, const StreamStats& stats) {
auto dbClient = app().getDbClient();
// Update realm's live status and viewer count
*dbClient << "UPDATE realms SET is_live = $1, viewer_count = $2 WHERE stream_key = $3"
<< stats.isLive << stats.uniqueViewers << streamKey
>> [streamKey, stats](const orm::Result&) {
LOG_DEBUG << "Updated realm status for stream " << streamKey
<< " - Live: " << stats.isLive
<< ", Viewers: " << stats.uniqueViewers;
}
>> [streamKey](const orm::DrogonDbException& e) {
LOG_ERROR << "Failed to update realm status for " << streamKey
<< ": " << e.base().what();
};
}
void StatsService::fetchStatsFromOme(const std::string& streamKey,
std::function<void(bool, const StreamStats&)> callback) {
LOG_DEBUG << "Fetching stats for stream: " << streamKey;
OmeClient::getInstance().getStreamStats(streamKey, [this, callback, streamKey](bool success, const Json::Value& json) {
StreamStats stats;
if (success && json.isMember("response") && !json["response"].isNull()) {
try {
const auto& data = json["response"];
// Parse connections
if (data.isMember("connections")) {
const auto& conns = data["connections"];
int64_t totalConns = 0;
for (const auto& protocolName : conns.getMemberNames()) {
int64_t count = conns[protocolName].asInt64();
auto& pc = stats.protocolConnections;
if (protocolName == "webrtc") pc.webrtc = count;
else if (protocolName == "hls") pc.hls = count;
else if (protocolName == "llhls") pc.llhls = count;
else if (protocolName == "dash") pc.dash = count;
totalConns += count;
}
stats.currentConnections = totalConns;
stats.totalConnections = totalConns;
}
// Bitrate
stats.bitrate = data.isMember("lastThroughputIn") ?
data["lastThroughputIn"].asDouble() :
(data.isMember("avgThroughputIn") ? data["avgThroughputIn"].asDouble() : 0);
// Byte counters
if (data.isMember("totalBytesIn")) stats.totalBytesIn = data["totalBytesIn"].asInt64();
if (data.isMember("totalBytesOut")) stats.totalBytesOut = data["totalBytesOut"].asInt64();
stats.isLive = (stats.bitrate > 0 || stats.currentConnections > 0);
LOG_DEBUG << "OME stats response: " << json.toStyledString();
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse stats: " << e.what();
stats.isLive = false;
}
} else {
stats.isLive = false;
}
stats.lastUpdated = std::chrono::system_clock::now();
// Now fetch stream info for resolution/codec/fps
OmeClient::getInstance().getStreamInfo(streamKey, [callback, stats](bool infoSuccess, const Json::Value& infoJson) mutable {
// Parse stream metadata if available
if (infoSuccess && infoJson.isMember("response") && infoJson["response"].isMember("tracks")) {
try {
for (const auto& track : infoJson["response"]["tracks"]) {
if (track["type"].asString() == "video") {
if (track.isMember("codec")) {
stats.codec = track["codec"].asString();
}
if (track.isMember("width") && track.isMember("height")) {
stats.resolution = std::to_string(track["width"].asInt()) + "x" +
std::to_string(track["height"].asInt());
}
if (track.isMember("framerate")) {
stats.fps = track["framerate"].asDouble();
}
break;
}
}
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse stream info: " << e.what();
}
}
callback(true, stats);
});
});
}
void StatsService::storeStatsInRedis(const std::string& streamKey, const StreamStats& stats) {
Json::Value json;
JSON_INT(json, "connections", stats.currentConnections);
JSON_INT(json, "unique_viewers", stats.uniqueViewers);
JSON_INT(json, "total_connections", stats.totalConnections);
JSON_INT(json, "bytes_in", stats.totalBytesIn);
JSON_INT(json, "bytes_out", stats.totalBytesOut);
json["bitrate"] = stats.bitrate;
json["codec"] = stats.codec;
json["resolution"] = stats.resolution;
json["fps"] = stats.fps;
json["is_live"] = stats.isLive;
JSON_INT(json, "last_updated",
std::chrono::duration_cast<std::chrono::seconds>(
stats.lastUpdated.time_since_epoch()
).count()
);
// Protocol connections
Json::Value pc;
JSON_INT(pc, "webrtc", stats.protocolConnections.webrtc);
JSON_INT(pc, "hls", stats.protocolConnections.hls);
JSON_INT(pc, "llhls", stats.protocolConnections.llhls);
JSON_INT(pc, "dash", stats.protocolConnections.dash);
json["protocol_connections"] = pc;
// Store connection drop timestamp if recent
auto timeSinceDrop = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now() - stats.lastConnectionDrop).count();
if (timeSinceDrop < 60) {
JSON_INT(json, "last_connection_drop",
std::chrono::duration_cast<std::chrono::seconds>(
stats.lastConnectionDrop.time_since_epoch()
).count()
);
}
RedisHelper::storeKey("stream_stats:" + streamKey, Json::FastWriter().write(json), 10);
}
void StatsService::getStreamStats(const std::string& streamKey,
std::function<void(bool, const StreamStats&)> callback) {
std::string jsonStr = RedisHelper::getKey("stream_stats:" + streamKey);
if (jsonStr.empty()) {
// Fetch fresh stats from OME and populate uniqueViewers
LOG_DEBUG << "No cached stats, fetching from OME for " << streamKey;
fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) {
if (success) {
StreamStats updatedStats = stats;
// FIX: Set uniqueViewers on cache miss!
updatedStats.uniqueViewers = getUniqueViewerCount(streamKey);
callback(true, updatedStats);
} else {
callback(false, stats);
}
});
return;
}
try {
Json::Value json;
Json::Reader reader;
if (reader.parse(jsonStr, json)) {
StreamStats stats;
stats.currentConnections = json["connections"].asInt64();
stats.uniqueViewers = json["unique_viewers"].asInt64();
stats.totalConnections = json["total_connections"].asInt64();
stats.totalBytesIn = json["bytes_in"].asInt64();
stats.totalBytesOut = json["bytes_out"].asInt64();
stats.bitrate = json["bitrate"].asDouble();
stats.codec = json["codec"].asString();
stats.resolution = json["resolution"].asString();
stats.fps = json["fps"].asDouble();
stats.isLive = json["is_live"].asBool();
// Parse protocol connections
if (json.isMember("protocol_connections")) {
const auto& pc = json["protocol_connections"];
stats.protocolConnections.webrtc = pc["webrtc"].asInt64();
stats.protocolConnections.hls = pc["hls"].asInt64();
stats.protocolConnections.llhls = pc["llhls"].asInt64();
stats.protocolConnections.dash = pc["dash"].asInt64();
}
stats.lastUpdated = std::chrono::system_clock::time_point(
std::chrono::seconds(json["last_updated"].asInt64())
);
callback(true, stats);
LOG_DEBUG << "Retrieved cached stats for " << streamKey;
} else {
// Fallback to fresh fetch if cached data is corrupted
fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) {
if (success) {
StreamStats updatedStats = stats;
updatedStats.uniqueViewers = getUniqueViewerCount(streamKey);
callback(true, updatedStats);
} else {
callback(false, stats);
}
});
}
} catch (const std::exception& e) {
LOG_ERROR << "Failed to parse cached stats: " << e.what();
// Fallback to fresh fetch
fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) {
if (success) {
StreamStats updatedStats = stats;
updatedStats.uniqueViewers = getUniqueViewerCount(streamKey);
callback(true, updatedStats);
} else {
callback(false, stats);
}
});
}
}

View file

@ -0,0 +1,72 @@
#pragma once
#include <drogon/drogon.h>
#include <trantor/net/EventLoop.h>
#include <memory>
#include <string>
#include <chrono>
#include <atomic>
#include <optional>
struct StreamStats {
int64_t currentConnections = 0; // Raw connection count from OME
int64_t uniqueViewers = 0; // Unique viewer tokens
int64_t totalConnections = 0;
int64_t totalBytesIn = 0;
int64_t totalBytesOut = 0;
double bitrate = 0.0;
std::string codec;
std::string resolution;
double fps = 0.0;
bool isLive = false;
std::chrono::system_clock::time_point lastUpdated;
// Protocol-specific connections
struct ProtocolConnections {
int64_t webrtc = 0;
int64_t hls = 0;
int64_t llhls = 0;
int64_t dash = 0;
} protocolConnections;
// Connection history for deduplication
std::chrono::system_clock::time_point lastConnectionDrop;
int64_t previousTotalConnections = 0;
};
class StatsService {
public:
static StatsService& getInstance() {
static StatsService instance;
return instance;
}
void initialize();
void shutdown();
// Get cached stats from Redis
void getStreamStats(const std::string& streamKey,
std::function<void(bool, const StreamStats&)> callback);
// Force update stats for a specific stream
void updateStreamStats(const std::string& streamKey);
// Get unique viewer count for a stream
int64_t getUniqueViewerCount(const std::string& streamKey);
private:
StatsService() = default;
~StatsService();
StatsService(const StatsService&) = delete;
StatsService& operator=(const StatsService&) = delete;
bool getPreviousStats(const std::string& streamKey, StreamStats& stats);
void pollOmeStats();
void storeStatsInRedis(const std::string& streamKey, const StreamStats& stats);
void fetchStatsFromOme(const std::string& streamKey,
std::function<void(bool, const StreamStats&)> callback);
void updateRealmLiveStatus(const std::string& streamKey, const StreamStats& stats);
std::atomic<bool> running_{false};
std::optional<trantor::TimerId> timerId_;
std::chrono::seconds pollInterval_{2}; // Poll every 2 seconds
};