2026-09-06 21:28:55 -07:00
|
|
|
/*
|
|
|
|
|
streamer-tools OBS Camera Plugin - streamer-tools API client
|
|
|
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
|
|
|
|
|
2026-09-07 04:44:16 -07:00
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
2026-09-06 21:28:55 -07:00
|
|
|
|
2026-09-07 04:44:16 -07:00
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
2026-09-06 21:28:55 -07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#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);
|
|
|
|
|
|
2026-09-06 21:54:11 -07:00
|
|
|
/// @param timeout_ms whole-request timeout. Kept as a parameter because
|
|
|
|
|
/// the properties dialog's "Refresh" button runs on OBS's UI thread with
|
|
|
|
|
/// an operator waiting, and must give up sooner than a background
|
|
|
|
|
/// reconnect would.
|
|
|
|
|
SlotsResult fetchSlots(const ConnectionConfig &config, int timeout_ms = 10000) const;
|
|
|
|
|
TokenResult requestToken(const ConnectionConfig &config, int timeout_ms = 10000) const;
|
2026-09-06 21:28:55 -07:00
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
|
|
2026-09-06 22:59:57 -07:00
|
|
|
/// Scrubs "access_token=<value>" and "key=<value>" out of arbitrary
|
|
|
|
|
/// text -- not necessarily a bare URL/query string -- replacing each
|
|
|
|
|
/// value with "<redacted>". Unlike redactedUrl (which only has to
|
|
|
|
|
/// handle "&"-delimited query parameters), a value here can be followed
|
|
|
|
|
/// by a quote or whitespace, because the text this scrubs is a free-form
|
|
|
|
|
/// log line that may merely *contain* a URL. Used by the OBS adapter's
|
|
|
|
|
/// LiveKit SDK log bridge: LiveKit's signaling URL carries the access
|
|
|
|
|
/// token as a query parameter, and the SDK's own log lines could
|
|
|
|
|
/// include it.
|
|
|
|
|
static std::string redactSensitiveParams(const std::string &text);
|
|
|
|
|
|
2026-09-06 21:28:55 -07:00
|
|
|
private:
|
|
|
|
|
std::shared_ptr<HttpClient> http_;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} // namespace stplugin
|