119 lines
4.0 KiB
C++
119 lines
4.0 KiB
C++
/*
|
|||
|
|
streamer-tools OBS Camera Plugin - streamer-tools API client
|
||
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
||
|
|
|
||
|
|
This program is free software; you can redistribute it and/or modify
|
||
|
|
it under the terms of the GNU General Public License as published by
|
||
|
|
the Free Software Foundation; either version 2 of the License, or
|
||
|
|
(at your option) any later version.
|
||
|
|
|
||
|
|
This program is distributed in the hope that it will be useful,
|
||
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
GNU General Public License for more details.
|
||
|
|
|
||
|
|
You should have received a copy of the GNU General Public License along
|
||
|
|
with this program. If not, see <https://www.gnu.org/licenses/>
|
||
|
|
*/
|
||
|
|
|
||
|
|
#pragma once
|
||
|
|
|
||
|
|
// Client for the two read-key-scoped endpoints in
|
||
|
|
// apps/server/src/obs/plugin.routes.ts (streamer-tools repo):
|
||
|
|
//
|
||
|
|
// GET /api/obs/:slug/slots?key=<readKey>
|
||
|
|
// 200 { slots: [ { identity, displayName, live } ] }
|
||
|
|
// 404 { error: 'not found' } wrong key OR unknown room
|
||
|
|
// 503 { error: 'livekit not configured' }
|
||
|
|
//
|
||
|
|
// POST /api/obs/:slug/token?key=<readKey>
|
||
|
|
// 200 { lkToken, wsUrl, identity }
|
||
|
|
// 404 / 503 as above
|
||
|
|
//
|
||
|
|
// The server deliberately answers a wrong key and an unknown slug identically
|
||
|
|
// (404), so this client must not claim to know which it was.
|
||
|
|
|
||
|
|
#include <memory>
|
||
|
|
#include <string>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#include "stplugin/core.h"
|
||
|
|
#include "stplugin/http.h"
|
||
|
|
|
||
|
|
namespace stplugin {
|
||
|
|
|
||
|
|
enum class ApiStatus {
|
||
|
|
Ok,
|
||
|
|
/// server URL / slug / key were not all filled in
|
||
|
|
InvalidConfig,
|
||
|
|
/// request never completed (DNS, TLS, timeout, ...)
|
||
|
|
NetworkError,
|
||
|
|
/// HTTP 404: unknown room slug or wrong read key -- indistinguishable
|
||
|
|
NotFound,
|
||
|
|
/// HTTP 503: the server has no LiveKit credentials configured
|
||
|
|
Unavailable,
|
||
|
|
/// any other non-2xx status
|
||
|
|
HttpError,
|
||
|
|
/// 2xx but the body was not the JSON shape this client expects
|
||
|
|
MalformedResponse,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// A short, operator-facing description. Never includes the read key.
|
||
|
|
const char *describeApiStatus(ApiStatus status);
|
||
|
|
|
||
|
|
struct SlotInfo {
|
||
|
|
/// LiveKit participant identity -- this is what the session wrapper
|
||
|
|
/// subscribes to, and what gets persisted in the OBS source settings.
|
||
|
|
std::string identity;
|
||
|
|
/// Human label for the dropdown; the server falls back to identity.
|
||
|
|
std::string display_name;
|
||
|
|
/// Currently publishing camera video.
|
||
|
|
bool live = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
struct SlotsResult {
|
||
|
|
ApiStatus status = ApiStatus::InvalidConfig;
|
||
|
|
/// Detail for logs/UI. Never contains the read key.
|
||
|
|
std::string message;
|
||
|
|
std::vector<SlotInfo> slots;
|
||
|
|
|
||
|
|
bool ok() const { return status == ApiStatus::Ok; }
|
||
|
|
};
|
||
|
|
|
||
|
|
struct TokenResult {
|
||
|
|
ApiStatus status = ApiStatus::InvalidConfig;
|
||
|
|
std::string message;
|
||
|
|
/// LiveKit JWT for a hidden, subscribe-only participant.
|
||
|
|
std::string lk_token;
|
||
|
|
/// LiveKit websocket URL to connect to.
|
||
|
|
std::string ws_url;
|
||
|
|
/// The obs:<slug>:<nonce> identity the server minted for us.
|
||
|
|
std::string identity;
|
||
|
|
|
||
|
|
bool ok() const { return status == ApiStatus::Ok; }
|
||
|
|
};
|
||
|
|
|
||
|
|
class ApiClient {
|
||
|
|
public:
|
||
|
|
/// Takes ownership of the HTTP client, so tests can inject a fake.
|
||
|
|
explicit ApiClient(std::shared_ptr<HttpClient> http);
|
||
|
|
|
||
|
|
SlotsResult fetchSlots(const ConnectionConfig &config) const;
|
||
|
|
TokenResult requestToken(const ConnectionConfig &config) const;
|
||
|
|
|
||
|
|
/// Accepts what an operator would actually paste: a bare hostname, a URL
|
||
|
|
/// with a trailing slash, extra whitespace. Returns an empty string if
|
||
|
|
/// nothing usable is left. Defaults to https:// when no scheme is given,
|
||
|
|
/// because the read key must never be sent in the clear by accident.
|
||
|
|
static std::string normalizeServerUrl(const std::string &raw);
|
||
|
|
|
||
|
|
/// Exposed for tests and for logging: the exact URL a call will hit,
|
||
|
|
/// with the read key replaced by "***".
|
||
|
|
static std::string redactedUrl(const std::string &url);
|
||
|
|
|
||
|
|
private:
|
||
|
|
std::shared_ptr<HttpClient> http_;
|
||
|
|
};
|
||
|
|
|
||
|
|
} // namespace stplugin
|