Files
spice2x-r3d/src/spice2x/overlay/windows/obs.h
T
bicarusandGitHub 451cbec0b9 overlay: OBS control window via WebSocket API (#773)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds an overlay window widget for controlling OBS over WebSocket.

Add new lines to the FPS widget that shows recording / streaming timers.

Show notifications for major state changes (stream started/stopped,
recording started/paused/resumed/stopped)

## Testing
2026-06-24 02:44:36 -07:00

132 lines
5.0 KiB
C++

#pragma once
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include "external/rapidjson/fwd.h"
#include "overlay/window.h"
namespace easywsclient {
class WebSocket;
}
namespace overlay::windows {
// OBS WebSocket connection settings, resolved once at launch from the merged
// launcher options (command line + saved config) following the same pattern
// as the other global launch settings in launcher.cpp
extern bool OBS_CONTROL_ENABLED;
extern std::string OBS_CONTROL_HOST;
extern uint16_t OBS_CONTROL_PORT;
extern std::string OBS_CONTROL_PASSWORD;
// when true, easywsclient's internal diagnostics are routed to the logger
extern bool OBS_CONTROL_DEBUG;
// status snapshot shared between the OBS worker thread and the render thread
struct OBSStatus {
bool disabled = true;
bool connected = false;
bool identifying = false;
std::string connection_error;
// name of the active program scene (read-only, from obs-websocket)
std::string current_scene;
bool streaming = false;
bool recording = false;
bool record_paused = false;
// duration base values (milliseconds) and the local timestamp (ms since
// steady epoch) at which they were last refreshed, so the UI can tick a
// smooth timer between polls
int64_t stream_duration_ms = 0;
int64_t record_duration_ms = 0;
int64_t stream_duration_base_tick = 0;
int64_t record_duration_base_tick = 0;
};
// in-flight user action used purely for UI feedback: when the user clicks a
// control we remember what we asked for so the button can show a transitional
// label and stay disabled until the observed OBS state matches the request
// (or a short deadline lapses). owned solely by the render thread.
enum class OBSAction {
None,
StreamStart, StreamStop,
RecordStart, RecordStop,
RecordPause, RecordResume,
};
class OBSControl : public Window {
public:
OBSControl(SpiceOverlay *overlay);
~OBSControl() override;
void build_content() override;
// thread-safe snapshot of the current status for external widgets (e.g. FPS)
OBSStatus get_status();
// live (ticked) duration in ms from a base value/tick captured at last poll
static int64_t live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking);
private:
// worker thread entry + helpers (implementation owns the WebSocket)
void worker_main();
// run one connected session loop until the socket closes or we stop;
// returns true if the obs-websocket handshake reached "Identified", false
// if the socket closed first (e.g. OBS rejected our auth)
bool run_session(easywsclient::WebSocket *ws, const std::string &password,
uint64_t &request_id);
// handle a single inbound obs-websocket message (parses + dispatches)
void handle_message(easywsclient::WebSocket *ws, const std::string &message,
const std::string &password, uint64_t &request_id,
bool &identified);
// per-opcode handlers dispatched from handle_message
using request_fn = std::function<void(const char *request_type)>;
void handle_identified(bool &identified, const request_fn &request);
void handle_event(const rapidjson::Value &d, const request_fn &request);
void handle_response(const rapidjson::Value &d);
void enqueue_request(const std::string &request_type);
// sleep up to total_ms, waking early if the worker is asked to stop
void interruptible_sleep(int total_ms);
// worker thread
std::thread worker_thread;
std::atomic<bool> worker_running { false };
// wakes interruptible_sleep immediately when worker_running is cleared,
// so shutdown (and the reconnect backoff) never waits out a fixed delay
std::mutex worker_mutex;
std::condition_variable worker_cv;
// shared status (guarded by status_mutex)
std::mutex status_mutex;
OBSStatus status;
// outgoing user commands (guarded by command_mutex)
std::mutex command_mutex;
std::deque<std::string> command_queue;
// transient action feedback, touched only by the render thread (no sync):
// remembers the last start/stop/pause request per output so the button can
// show a "Starting.../Stopping..." label and stay disabled until OBS reports
// the matching state, with *_deadline as a fallback if the update is missed
OBSAction stream_pending = OBSAction::None;
OBSAction record_pending = OBSAction::None;
int64_t stream_pending_deadline = 0;
int64_t record_pending_deadline = 0;
};
}