#pragma once #include #include #include #include #include #include #include #include #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 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 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 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; }; }