fix line endings

This commit is contained in:
bicarus-dev
2026-08-16 21:23:41 -07:00
parent adf4cccd4a
commit f857926ec3
77 changed files with 25537 additions and 25495 deletions
+434 -434
View File
@@ -1,434 +1,434 @@
#include <winsock2.h>
#include "obs.h"
#include <chrono>
#include "external/easywsclient/easywsclient.hpp"
#include "external/rapidjson/document.h"
#include "external/rapidjson/stringbuffer.h"
#include "external/rapidjson/writer.h"
#include "external/hash-library/sha256.h"
#include "overlay/notifications.h"
#include "util/crypt.h"
#include "util/logging.h"
// defined in easywsclient.cpp; gates its internal diagnostic output
extern bool EASYWSCLIENT_LOGGING_ENABLED;
using easywsclient::WebSocket;
using namespace std::chrono;
// obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket):
// server -> op 0 Hello (may include an auth challenge)
// client -> op 1 Identify (answers the challenge, picks rpcVersion)
// server -> op 2 Identified (handshake done; requests may now be sent)
// server -> op 5 Event (state changes: stream/record/scene/...)
// client -> op 6 Request (e.g. GetStreamStatus, StartRecord)
// server -> op 7 RequestResponse (reply to a Request, carries responseData)
// Every message is { "op": <int>, "d": { ... } }. Event fields are nested under
// d["eventData"] and request replies under d["responseData"], not in d directly.
namespace {
int64_t now_ms() {
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// raw SHA256 digest -> base64 (obs-websocket v5 auth primitive)
std::string sha256_base64(const std::string &input) {
SHA256 hasher;
hasher.add(input.data(), input.size());
unsigned char digest[SHA256::HashBytes];
hasher.getHash(digest);
return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes);
}
// auth = base64(sha256(base64(sha256(password + salt)) + challenge))
std::string compute_auth(const std::string &password, const std::string &salt,
const std::string &challenge) {
const std::string secret = sha256_base64(password + salt);
return sha256_base64(secret + challenge);
}
std::string build_identify(int rpc_version, const std::string &authentication) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("op"); w.Int(1);
w.Key("d");
w.StartObject();
w.Key("rpcVersion"); w.Int(rpc_version);
if (!authentication.empty()) {
w.Key("authentication"); w.String(authentication.c_str());
}
w.EndObject();
w.EndObject();
return sb.GetString();
}
std::string build_request(const std::string &request_type, uint64_t request_id) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("op"); w.Int(6);
w.Key("d");
w.StartObject();
w.Key("requestType"); w.String(request_type.c_str());
w.Key("requestId"); w.String(std::to_string(request_id).c_str());
w.EndObject();
w.EndObject();
return sb.GetString();
}
// read a numeric field as int64 ms (obs sends durations as integers/doubles)
int64_t json_number(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsNumber()) {
return static_cast<int64_t>(obj[key].GetDouble());
}
return 0;
}
bool json_bool(const rapidjson::Value &obj, const char *key) {
return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool();
}
std::string json_string(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsString()) {
return obj[key].GetString();
}
return "";
}
// build the Identify (op 1) reply to a Hello (op 0), answering the auth
// challenge if the server requires one
std::string build_hello_response(const rapidjson::Value &d, const std::string &password) {
int rpc_version = 1;
if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) {
rpc_version = d["rpcVersion"].GetInt();
}
std::string auth;
if (d.HasMember("authentication") && d["authentication"].IsObject()) {
const rapidjson::Value &a = d["authentication"];
const std::string challenge = json_string(a, "challenge");
const std::string salt = json_string(a, "salt");
if (!challenge.empty()) {
auth = compute_auth(password, salt, challenge);
}
}
return build_identify(rpc_version, auth);
}
// map an obs-websocket outputState to a user notification. `label` is the
// output kind ("Streaming" or "Recording"). transitional states are ignored.
void notify_output_state(const char *label, const std::string &state) {
using overlay::notifications::Severity;
struct StateToast {
const char *state;
Severity severity;
const char *verb;
};
static const StateToast TOASTS[] = {
{ "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" },
{ "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" },
{ "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" },
{ "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" },
};
for (const auto &toast : TOASTS) {
if (state == toast.state) {
overlay::notifications::add(toast.severity,
"OBS: " + std::string(label) + " " + toast.verb);
return;
}
}
}
}
namespace overlay::windows {
// connection settings resolved at launch (see launcher.cpp)
bool OBS_CONTROL_ENABLED = false;
std::string OBS_CONTROL_HOST = "127.0.0.1";
uint16_t OBS_CONTROL_PORT = 4455;
std::string OBS_CONTROL_PASSWORD;
bool OBS_CONTROL_DEBUG = false;
void OBSControl::enqueue_request(const std::string &request_type) {
std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.push_back(request_type);
}
void OBSControl::interruptible_sleep(int total_ms) {
std::unique_lock<std::mutex> lock(this->worker_mutex);
this->worker_cv.wait_for(lock, milliseconds(total_ms),
[this] { return !this->worker_running.load(); });
}
void OBSControl::handle_message(WebSocket *ws, const std::string &message,
const std::string &password, uint64_t &request_id, bool &identified) {
rapidjson::Document doc;
if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) {
return;
}
if (!doc.HasMember("op") || !doc["op"].IsInt()
|| !doc.HasMember("d") || !doc["d"].IsObject()) {
return;
}
const int op = doc["op"].GetInt();
const rapidjson::Value &d = doc["d"];
// send an op 6 Request; each needs a unique id (we never match replies
// back, so a simple incrementing counter is enough)
const request_fn request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id));
};
switch (op) {
case 0: // Hello
// server greeted us: reply with Identify, solving the auth
// challenge inline if the server set a password
ws->send(build_hello_response(d, password));
break;
case 2: // Identified
this->handle_identified(identified, request);
break;
case 5: // Event
this->handle_event(d, request);
break;
case 7: // RequestResponse
this->handle_response(d);
break;
default:
break;
}
}
void OBSControl::handle_identified(bool &identified, const request_fn &request) {
// handshake complete: the connection is now usable for requests
identified = true;
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = true;
this->status.identifying = false;
this->status.connection_error.clear();
}
log_info("obs", "connected and identified");
// pull the current scene/stream/record state so the UI starts accurate
request("GetCurrentProgramScene");
request("GetStreamStatus");
request("GetRecordStatus");
}
void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) {
const std::string type = json_string(d, "eventType");
const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject();
if (type == "StreamStateChanged") {
if (has_data) {
notify_output_state("Streaming", json_string(d["eventData"], "outputState"));
}
request("GetStreamStatus");
} else if (type == "RecordStateChanged") {
if (has_data) {
notify_output_state("Recording", json_string(d["eventData"], "outputState"));
}
request("GetRecordStatus");
} else if (type == "CurrentProgramSceneChanged" && has_data) {
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.current_scene = json_string(d["eventData"], "sceneName");
}
}
void OBSControl::handle_response(const rapidjson::Value &d) {
const std::string type = json_string(d, "requestType");
if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) {
return;
}
const rapidjson::Value &rd = d["responseData"];
std::lock_guard<std::mutex> lock(this->status_mutex);
if (type == "GetCurrentProgramScene") {
// newer obs returns sceneName; older builds used the now-deprecated
// currentProgramSceneName, so prefer it then fall back
std::string scene = json_string(rd, "currentProgramSceneName");
if (scene.empty()) {
scene = json_string(rd, "sceneName");
}
this->status.current_scene = scene;
} else if (type == "GetStreamStatus") {
this->status.streaming = json_bool(rd, "outputActive");
this->status.stream_duration_ms = json_number(rd, "outputDuration");
this->status.stream_duration_base_tick = now_ms();
} else if (type == "GetRecordStatus") {
this->status.recording = json_bool(rd, "outputActive");
this->status.record_paused = json_bool(rd, "outputPaused");
this->status.record_duration_ms = json_number(rd, "outputDuration");
this->status.record_duration_base_tick = now_ms();
}
}
bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) {
// one iteration of a live connection: pump socket I/O, dispatch any
// inbound messages, flush queued user commands, then refresh status
bool identified = false;
// handle_identified() issues the first GetStreamStatus/GetRecordStatus on
// identify, so the periodic poll below just maintains the ~1s cadence
auto last_status_poll = steady_clock::now();
// send a request with the next sequential id
const auto request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id));
};
while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) {
ws->poll(100);
ws->dispatch([&](const std::string &message) {
this->handle_message(ws, message, password, request_id, identified);
});
if (ws->getReadyState() == WebSocket::CLOSED) {
break;
}
// nothing may be sent until the op 2 Identified handshake completes
if (!identified) {
continue;
}
// drain user commands
std::deque<std::string> pending;
{
std::lock_guard<std::mutex> lock(this->command_mutex);
pending.swap(this->command_queue);
}
for (const auto &cmd : pending) {
ws->send(build_request(cmd, ++request_id));
}
// periodic status refresh (~1s) for live duration
const auto now = steady_clock::now();
if (now - last_status_poll >= milliseconds(1000)) {
last_status_poll = now;
request("GetStreamStatus");
request("GetRecordStatus");
}
}
return identified;
}
void OBSControl::worker_main() {
// connection settings are resolved once at launch into globals
// (launcher.cpp, from the merged command-line + saved config options)
if (!OBS_CONTROL_ENABLED) {
log_info("obs", "disabled, not connecting");
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = true;
return;
}
const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
const std::string password = OBS_CONTROL_PASSWORD;
// opt easywsclient's internal diagnostics in/out per the debug option
EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG;
// winsock is reference-counted: the app performs its own WSAStartup at
// launch (which outlives this worker), so this paired Startup/Cleanup only
// bumps the refcount and the WSACleanup below never tears down winsock for
// the rest of the process
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
log_info("obs", "enabled, connecting to {}", url);
uint64_t request_id = 0;
// the reconnect loop retries every 5s; latch the auth-failure warning so a
// wrong password logs once, not on every retry. reset after any identified
// session so a later genuine failure is reported again
bool auth_warning_logged = false;
// reconnect loop: keep a session alive while enabled, retrying on drop
while (this->worker_running.load()) {
// mark "connecting" for the UI before each attempt
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = false;
this->status.connected = false;
this->status.identifying = true;
this->status.connection_error.clear();
}
// open the TCP socket and perform the WebSocket handshake; null means
// OBS is unreachable (not running / wrong port / obs-websocket off)
WebSocket::pointer ws = WebSocket::from_url(url);
if (ws == nullptr) {
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.identifying = false;
this->status.connection_error = "Unable to connect";
}
interruptible_sleep(5000);
continue;
}
// blocks here pumping the connection until it closes or we stop.
// a session that never reaches "Identified" was rejected by OBS,
// overwhelmingly because the password is wrong or missing
const bool identified = this->run_session(ws, password, request_id);
// session ended: close the socket cleanly and free it
ws->close();
ws->poll();
delete ws;
if (!identified && this->worker_running.load()) {
if (!auth_warning_logged) {
log_warning("obs", "connection closed before identify; "
"OBS likely rejected authentication (check the password)");
auth_warning_logged = true;
}
} else if (identified) {
// a good session resets the latch so a future failure logs again
auth_warning_logged = false;
}
// connection dropped: clear live state so the UI doesn't show stale
// scene/stream/record info while disconnected
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = false;
this->status.identifying = false;
if (this->status.connection_error.empty()) {
this->status.connection_error =
identified ? "Disconnected" : "Auth failed (check password)";
}
this->status.streaming = false;
this->status.recording = false;
this->status.record_paused = false;
this->status.current_scene.clear();
}
// clear any commands queued while disconnected
{
std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.clear();
}
// wait before reconnecting (interruptible)
interruptible_sleep(5000);
}
WSACleanup();
log_info("obs", "OBS overlay worker stopped");
}
}
#include <winsock2.h>
#include "obs.h"
#include <chrono>
#include "external/easywsclient/easywsclient.hpp"
#include "external/rapidjson/document.h"
#include "external/rapidjson/stringbuffer.h"
#include "external/rapidjson/writer.h"
#include "external/hash-library/sha256.h"
#include "overlay/notifications.h"
#include "util/crypt.h"
#include "util/logging.h"
// defined in easywsclient.cpp; gates its internal diagnostic output
extern bool EASYWSCLIENT_LOGGING_ENABLED;
using easywsclient::WebSocket;
using namespace std::chrono;
// obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket):
// server -> op 0 Hello (may include an auth challenge)
// client -> op 1 Identify (answers the challenge, picks rpcVersion)
// server -> op 2 Identified (handshake done; requests may now be sent)
// server -> op 5 Event (state changes: stream/record/scene/...)
// client -> op 6 Request (e.g. GetStreamStatus, StartRecord)
// server -> op 7 RequestResponse (reply to a Request, carries responseData)
// Every message is { "op": <int>, "d": { ... } }. Event fields are nested under
// d["eventData"] and request replies under d["responseData"], not in d directly.
namespace {
int64_t now_ms() {
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
}
// raw SHA256 digest -> base64 (obs-websocket v5 auth primitive)
std::string sha256_base64(const std::string &input) {
SHA256 hasher;
hasher.add(input.data(), input.size());
unsigned char digest[SHA256::HashBytes];
hasher.getHash(digest);
return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes);
}
// auth = base64(sha256(base64(sha256(password + salt)) + challenge))
std::string compute_auth(const std::string &password, const std::string &salt,
const std::string &challenge) {
const std::string secret = sha256_base64(password + salt);
return sha256_base64(secret + challenge);
}
std::string build_identify(int rpc_version, const std::string &authentication) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("op"); w.Int(1);
w.Key("d");
w.StartObject();
w.Key("rpcVersion"); w.Int(rpc_version);
if (!authentication.empty()) {
w.Key("authentication"); w.String(authentication.c_str());
}
w.EndObject();
w.EndObject();
return sb.GetString();
}
std::string build_request(const std::string &request_type, uint64_t request_id) {
rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject();
w.Key("op"); w.Int(6);
w.Key("d");
w.StartObject();
w.Key("requestType"); w.String(request_type.c_str());
w.Key("requestId"); w.String(std::to_string(request_id).c_str());
w.EndObject();
w.EndObject();
return sb.GetString();
}
// read a numeric field as int64 ms (obs sends durations as integers/doubles)
int64_t json_number(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsNumber()) {
return static_cast<int64_t>(obj[key].GetDouble());
}
return 0;
}
bool json_bool(const rapidjson::Value &obj, const char *key) {
return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool();
}
std::string json_string(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsString()) {
return obj[key].GetString();
}
return "";
}
// build the Identify (op 1) reply to a Hello (op 0), answering the auth
// challenge if the server requires one
std::string build_hello_response(const rapidjson::Value &d, const std::string &password) {
int rpc_version = 1;
if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) {
rpc_version = d["rpcVersion"].GetInt();
}
std::string auth;
if (d.HasMember("authentication") && d["authentication"].IsObject()) {
const rapidjson::Value &a = d["authentication"];
const std::string challenge = json_string(a, "challenge");
const std::string salt = json_string(a, "salt");
if (!challenge.empty()) {
auth = compute_auth(password, salt, challenge);
}
}
return build_identify(rpc_version, auth);
}
// map an obs-websocket outputState to a user notification. `label` is the
// output kind ("Streaming" or "Recording"). transitional states are ignored.
void notify_output_state(const char *label, const std::string &state) {
using overlay::notifications::Severity;
struct StateToast {
const char *state;
Severity severity;
const char *verb;
};
static const StateToast TOASTS[] = {
{ "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" },
{ "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" },
{ "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" },
{ "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" },
};
for (const auto &toast : TOASTS) {
if (state == toast.state) {
overlay::notifications::add(toast.severity,
"OBS: " + std::string(label) + " " + toast.verb);
return;
}
}
}
}
namespace overlay::windows {
// connection settings resolved at launch (see launcher.cpp)
bool OBS_CONTROL_ENABLED = false;
std::string OBS_CONTROL_HOST = "127.0.0.1";
uint16_t OBS_CONTROL_PORT = 4455;
std::string OBS_CONTROL_PASSWORD;
bool OBS_CONTROL_DEBUG = false;
void OBSControl::enqueue_request(const std::string &request_type) {
std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.push_back(request_type);
}
void OBSControl::interruptible_sleep(int total_ms) {
std::unique_lock<std::mutex> lock(this->worker_mutex);
this->worker_cv.wait_for(lock, milliseconds(total_ms),
[this] { return !this->worker_running.load(); });
}
void OBSControl::handle_message(WebSocket *ws, const std::string &message,
const std::string &password, uint64_t &request_id, bool &identified) {
rapidjson::Document doc;
if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) {
return;
}
if (!doc.HasMember("op") || !doc["op"].IsInt()
|| !doc.HasMember("d") || !doc["d"].IsObject()) {
return;
}
const int op = doc["op"].GetInt();
const rapidjson::Value &d = doc["d"];
// send an op 6 Request; each needs a unique id (we never match replies
// back, so a simple incrementing counter is enough)
const request_fn request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id));
};
switch (op) {
case 0: // Hello
// server greeted us: reply with Identify, solving the auth
// challenge inline if the server set a password
ws->send(build_hello_response(d, password));
break;
case 2: // Identified
this->handle_identified(identified, request);
break;
case 5: // Event
this->handle_event(d, request);
break;
case 7: // RequestResponse
this->handle_response(d);
break;
default:
break;
}
}
void OBSControl::handle_identified(bool &identified, const request_fn &request) {
// handshake complete: the connection is now usable for requests
identified = true;
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = true;
this->status.identifying = false;
this->status.connection_error.clear();
}
log_info("obs", "connected and identified");
// pull the current scene/stream/record state so the UI starts accurate
request("GetCurrentProgramScene");
request("GetStreamStatus");
request("GetRecordStatus");
}
void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) {
const std::string type = json_string(d, "eventType");
const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject();
if (type == "StreamStateChanged") {
if (has_data) {
notify_output_state("Streaming", json_string(d["eventData"], "outputState"));
}
request("GetStreamStatus");
} else if (type == "RecordStateChanged") {
if (has_data) {
notify_output_state("Recording", json_string(d["eventData"], "outputState"));
}
request("GetRecordStatus");
} else if (type == "CurrentProgramSceneChanged" && has_data) {
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.current_scene = json_string(d["eventData"], "sceneName");
}
}
void OBSControl::handle_response(const rapidjson::Value &d) {
const std::string type = json_string(d, "requestType");
if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) {
return;
}
const rapidjson::Value &rd = d["responseData"];
std::lock_guard<std::mutex> lock(this->status_mutex);
if (type == "GetCurrentProgramScene") {
// newer obs returns sceneName; older builds used the now-deprecated
// currentProgramSceneName, so prefer it then fall back
std::string scene = json_string(rd, "currentProgramSceneName");
if (scene.empty()) {
scene = json_string(rd, "sceneName");
}
this->status.current_scene = scene;
} else if (type == "GetStreamStatus") {
this->status.streaming = json_bool(rd, "outputActive");
this->status.stream_duration_ms = json_number(rd, "outputDuration");
this->status.stream_duration_base_tick = now_ms();
} else if (type == "GetRecordStatus") {
this->status.recording = json_bool(rd, "outputActive");
this->status.record_paused = json_bool(rd, "outputPaused");
this->status.record_duration_ms = json_number(rd, "outputDuration");
this->status.record_duration_base_tick = now_ms();
}
}
bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) {
// one iteration of a live connection: pump socket I/O, dispatch any
// inbound messages, flush queued user commands, then refresh status
bool identified = false;
// handle_identified() issues the first GetStreamStatus/GetRecordStatus on
// identify, so the periodic poll below just maintains the ~1s cadence
auto last_status_poll = steady_clock::now();
// send a request with the next sequential id
const auto request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id));
};
while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) {
ws->poll(100);
ws->dispatch([&](const std::string &message) {
this->handle_message(ws, message, password, request_id, identified);
});
if (ws->getReadyState() == WebSocket::CLOSED) {
break;
}
// nothing may be sent until the op 2 Identified handshake completes
if (!identified) {
continue;
}
// drain user commands
std::deque<std::string> pending;
{
std::lock_guard<std::mutex> lock(this->command_mutex);
pending.swap(this->command_queue);
}
for (const auto &cmd : pending) {
ws->send(build_request(cmd, ++request_id));
}
// periodic status refresh (~1s) for live duration
const auto now = steady_clock::now();
if (now - last_status_poll >= milliseconds(1000)) {
last_status_poll = now;
request("GetStreamStatus");
request("GetRecordStatus");
}
}
return identified;
}
void OBSControl::worker_main() {
// connection settings are resolved once at launch into globals
// (launcher.cpp, from the merged command-line + saved config options)
if (!OBS_CONTROL_ENABLED) {
log_info("obs", "disabled, not connecting");
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = true;
return;
}
const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
const std::string password = OBS_CONTROL_PASSWORD;
// opt easywsclient's internal diagnostics in/out per the debug option
EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG;
// winsock is reference-counted: the app performs its own WSAStartup at
// launch (which outlives this worker), so this paired Startup/Cleanup only
// bumps the refcount and the WSACleanup below never tears down winsock for
// the rest of the process
WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data);
log_info("obs", "enabled, connecting to {}", url);
uint64_t request_id = 0;
// the reconnect loop retries every 5s; latch the auth-failure warning so a
// wrong password logs once, not on every retry. reset after any identified
// session so a later genuine failure is reported again
bool auth_warning_logged = false;
// reconnect loop: keep a session alive while enabled, retrying on drop
while (this->worker_running.load()) {
// mark "connecting" for the UI before each attempt
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = false;
this->status.connected = false;
this->status.identifying = true;
this->status.connection_error.clear();
}
// open the TCP socket and perform the WebSocket handshake; null means
// OBS is unreachable (not running / wrong port / obs-websocket off)
WebSocket::pointer ws = WebSocket::from_url(url);
if (ws == nullptr) {
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.identifying = false;
this->status.connection_error = "Unable to connect";
}
interruptible_sleep(5000);
continue;
}
// blocks here pumping the connection until it closes or we stop.
// a session that never reaches "Identified" was rejected by OBS,
// overwhelmingly because the password is wrong or missing
const bool identified = this->run_session(ws, password, request_id);
// session ended: close the socket cleanly and free it
ws->close();
ws->poll();
delete ws;
if (!identified && this->worker_running.load()) {
if (!auth_warning_logged) {
log_warning("obs", "connection closed before identify; "
"OBS likely rejected authentication (check the password)");
auth_warning_logged = true;
}
} else if (identified) {
// a good session resets the latch so a future failure logs again
auth_warning_logged = false;
}
// connection dropped: clear live state so the UI doesn't show stale
// scene/stream/record info while disconnected
{
std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = false;
this->status.identifying = false;
if (this->status.connection_error.empty()) {
this->status.connection_error =
identified ? "Disconnected" : "Auth failed (check password)";
}
this->status.streaming = false;
this->status.recording = false;
this->status.record_paused = false;
this->status.current_scene.clear();
}
// clear any commands queued while disconnected
{
std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.clear();
}
// wait before reconnecting (interruptible)
interruptible_sleep(5000);
}
WSACleanup();
log_info("obs", "OBS overlay worker stopped");
}
}