base: Pause requests and events during start, SC change, and shutdown

This implements the functionality described by the new NotReady request
status. Behavior should now be *much* more reliable.
This commit is contained in:
tt2468 2023-05-09 22:48:07 -07:00
parent e3d0751385
commit a0ffe16e91
7 changed files with 98 additions and 132 deletions

View File

@ -20,11 +20,6 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "EventHandler.h" #include "EventHandler.h"
EventHandler::EventHandler() EventHandler::EventHandler()
: _obsLoaded(false),
_inputVolumeMetersRef(0),
_inputActiveStateChangedRef(0),
_inputShowStateChangedRef(0),
_sceneItemTransformChangedRef(0)
{ {
blog_debug("[EventHandler::EventHandler] Setting up..."); blog_debug("[EventHandler::EventHandler] Setting up...");
@ -67,9 +62,9 @@ void EventHandler::SetBroadcastCallback(EventHandler::BroadcastCallback cb)
_broadcastCallback = cb; _broadcastCallback = cb;
} }
void EventHandler::SetObsLoadedCallback(EventHandler::ObsLoadedCallback cb) void EventHandler::SetObsReadyCallback(EventHandler::ObsReadyCallback cb)
{ {
_obsLoadedCallback = cb; _obsReadyCallback = cb;
} }
// Function to increment refcounts for high volume event subscriptions // Function to increment refcounts for high volume event subscriptions
@ -261,9 +256,6 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
{ {
auto eventHandler = static_cast<EventHandler *>(private_data); auto eventHandler = static_cast<EventHandler *>(private_data);
if (!eventHandler->_obsLoaded.load() && event != OBS_FRONTEND_EVENT_FINISHED_LOADING)
return;
switch (event) { switch (event) {
// General // General
case OBS_FRONTEND_EVENT_FINISHED_LOADING: case OBS_FRONTEND_EVENT_FINISHED_LOADING:
@ -283,7 +275,11 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
} }
obs_frontend_source_list_free(&transitions); obs_frontend_source_list_free(&transitions);
} }
// Before ready update to allow event to broadcast
eventHandler->HandleCurrentSceneCollectionChanging(); eventHandler->HandleCurrentSceneCollectionChanging();
eventHandler->_obsReady = false;
if (eventHandler->_obsReadyCallback)
eventHandler->_obsReadyCallback(false);
break; break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED: { case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED: {
obs_frontend_source_list transitions = {}; obs_frontend_source_list transitions = {};
@ -294,6 +290,9 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
} }
obs_frontend_source_list_free(&transitions); obs_frontend_source_list_free(&transitions);
} }
eventHandler->_obsReady = true;
if (eventHandler->_obsReadyCallback)
eventHandler->_obsReadyCallback(true);
eventHandler->HandleCurrentSceneCollectionChanged(); eventHandler->HandleCurrentSceneCollectionChanged();
break; break;
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED: case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED:
@ -430,30 +429,6 @@ void EventHandler::FrontendFinishedLoadingMultiHandler()
blog_debug( blog_debug(
"[EventHandler::FrontendFinishedLoadingMultiHandler] OBS has finished loading. Connecting final handlers and enabling events..."); "[EventHandler::FrontendFinishedLoadingMultiHandler] OBS has finished loading. Connecting final handlers and enabling events...");
// Connect source signals and enable events only after OBS has fully loaded (to reduce extra logging).
_obsLoaded.store(true);
// In the case that plugins become hotloadable, this will have to go back into `EventHandler::EventHandler()`
// Enumerate inputs and connect each one
{
auto enumInputs = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler *>(param);
eventHandler->ConnectSourceSignals(source);
return true;
};
obs_enum_sources(enumInputs, this);
}
// Enumerate scenes and connect each one
{
auto enumScenes = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler *>(param);
eventHandler->ConnectSourceSignals(source);
return true;
};
obs_enum_scenes(enumScenes, this);
}
// Enumerate all scene transitions and connect each one // Enumerate all scene transitions and connect each one
{ {
obs_frontend_source_list transitions = {}; obs_frontend_source_list transitions = {};
@ -465,41 +440,23 @@ void EventHandler::FrontendFinishedLoadingMultiHandler()
obs_frontend_source_list_free(&transitions); obs_frontend_source_list_free(&transitions);
} }
blog_debug("[EventHandler::FrontendFinishedLoadingMultiHandler] Finished."); _obsReady = true;
if (_obsReadyCallback)
_obsReadyCallback(true);
if (_obsLoadedCallback) blog_debug("[EventHandler::FrontendFinishedLoadingMultiHandler] Finished.");
_obsLoadedCallback();
} }
void EventHandler::FrontendExitMultiHandler() void EventHandler::FrontendExitMultiHandler()
{ {
HandleExitStarted();
blog_debug("[EventHandler::FrontendExitMultiHandler] OBS is unloading. Disabling events..."); blog_debug("[EventHandler::FrontendExitMultiHandler] OBS is unloading. Disabling events...");
HandleExitStarted();
// Disconnect source signals and disable events when OBS starts unloading (to reduce extra logging). // Disconnect source signals and disable events when OBS starts unloading (to reduce extra logging).
_obsLoaded.store(false); _obsReady = false;
if (_obsReadyCallback)
// In the case that plugins become hotloadable, this will have to go back into `EventHandler::~EventHandler()` _obsReadyCallback(false);
// Enumerate inputs and disconnect each one
{
auto enumInputs = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler *>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
};
obs_enum_sources(enumInputs, this);
}
// Enumerate scenes and disconnect each one
{
auto enumScenes = [](void *param, obs_source_t *source) {
auto eventHandler = static_cast<EventHandler *>(param);
eventHandler->DisconnectSourceSignals(source);
return true;
};
obs_enum_scenes(enumScenes, this);
}
// Enumerate all scene transitions and disconnect each one // Enumerate all scene transitions and disconnect each one
{ {
@ -520,10 +477,6 @@ void EventHandler::SourceCreatedMultiHandler(void *param, calldata_t *data)
{ {
auto eventHandler = static_cast<EventHandler *>(param); auto eventHandler = static_cast<EventHandler *>(param);
// Don't react to signals until OBS has finished loading
if (!eventHandler->_obsLoaded.load())
return;
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source"); obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source) if (!source)
return; return;
@ -556,10 +509,6 @@ void EventHandler::SourceDestroyedMultiHandler(void *param, calldata_t *data)
// Disconnect all signals from the source // Disconnect all signals from the source
eventHandler->DisconnectSourceSignals(source); eventHandler->DisconnectSourceSignals(source);
// Don't react to signals if OBS is unloading
if (!eventHandler->_obsLoaded.load())
return;
switch (obs_source_get_type(source)) { switch (obs_source_get_type(source)) {
case OBS_SOURCE_TYPE_INPUT: case OBS_SOURCE_TYPE_INPUT:
// Only emit removed if the input has not already been removed. This is the case when removing the last scene item of an input. // Only emit removed if the input has not already been removed. This is the case when removing the last scene item of an input.
@ -582,9 +531,6 @@ void EventHandler::SourceRemovedMultiHandler(void *param, calldata_t *data)
{ {
auto eventHandler = static_cast<EventHandler *>(param); auto eventHandler = static_cast<EventHandler *>(param);
if (!eventHandler->_obsLoaded.load())
return;
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source"); obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source) if (!source)
return; return;
@ -605,9 +551,6 @@ void EventHandler::SourceRenamedMultiHandler(void *param, calldata_t *data)
{ {
auto eventHandler = static_cast<EventHandler *>(param); auto eventHandler = static_cast<EventHandler *>(param);
if (!eventHandler->_obsLoaded.load())
return;
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source"); obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
if (!source) if (!source)
return; return;

View File

@ -34,25 +34,26 @@ public:
EventHandler(); EventHandler();
~EventHandler(); ~EventHandler();
typedef std::function<void(uint64_t, std::string, json, uint8_t)> BroadcastCallback; typedef std::function<void(uint64_t, std::string, json, uint8_t)>
BroadcastCallback; // uint64_t requiredIntent, std::string eventType, json eventData, uint8_t rpcVersion
void SetBroadcastCallback(BroadcastCallback cb); void SetBroadcastCallback(BroadcastCallback cb);
typedef std::function<void()> ObsLoadedCallback; typedef std::function<void(bool)> ObsReadyCallback; // bool ready
void SetObsLoadedCallback(ObsLoadedCallback cb); void SetObsReadyCallback(ObsReadyCallback cb);
void ProcessSubscription(uint64_t eventSubscriptions); void ProcessSubscription(uint64_t eventSubscriptions);
void ProcessUnsubscription(uint64_t eventSubscriptions); void ProcessUnsubscription(uint64_t eventSubscriptions);
private: private:
BroadcastCallback _broadcastCallback; BroadcastCallback _broadcastCallback;
ObsLoadedCallback _obsLoadedCallback; ObsReadyCallback _obsReadyCallback;
std::atomic<bool> _obsLoaded; std::atomic<bool> _obsReady = false;
std::unique_ptr<Utils::Obs::VolumeMeter::Handler> _inputVolumeMetersHandler; std::unique_ptr<Utils::Obs::VolumeMeter::Handler> _inputVolumeMetersHandler;
std::atomic<uint64_t> _inputVolumeMetersRef; std::atomic<uint64_t> _inputVolumeMetersRef = 0;
std::atomic<uint64_t> _inputActiveStateChangedRef; std::atomic<uint64_t> _inputActiveStateChangedRef = 0;
std::atomic<uint64_t> _inputShowStateChangedRef; std::atomic<uint64_t> _inputShowStateChangedRef = 0;
std::atomic<uint64_t> _sceneItemTransformChangedRef; std::atomic<uint64_t> _sceneItemTransformChangedRef = 0;
void ConnectSourceSignals(obs_source_t *source); void ConnectSourceSignals(obs_source_t *source);
void DisconnectSourceSignals(obs_source_t *source); void DisconnectSourceSignals(obs_source_t *source);

View File

@ -89,7 +89,24 @@ bool obs_module_load(void)
return true; return true;
} }
void obs_module_unload() #ifdef PLUGIN_TESTS
void test_register_vendor();
#endif
void obs_module_post_load(void)
{
#ifdef PLUGIN_TESTS
test_register_vendor();
#endif
// Server will accept clients, but requests and events will not be served until FINISHED_LOADING occurs
if (_config->ServerEnabled) {
blog(LOG_INFO, "[obs_module_post_load] WebSocket server is enabled, starting...");
_webSocketServer->Start();
}
}
void obs_module_unload(void)
{ {
blog(LOG_INFO, "[obs_module_unload] Shutting down..."); blog(LOG_INFO, "[obs_module_unload] Shutting down...");
@ -193,18 +210,18 @@ static void test_vendor_request_cb(obs_data_t *requestData, obs_data_t *response
obs_websocket_vendor_emit_event(priv_data, "TestEvent", requestData); obs_websocket_vendor_emit_event(priv_data, "TestEvent", requestData);
} }
void obs_module_post_load() void test_register_vendor()
{ {
blog(LOG_INFO, "[obs_module_post_load] Post load started."); blog(LOG_INFO, "[test_register_vendor] Registering test vendor...");
// Test plugin API version fetch // Test plugin API version fetch
uint apiVersion = obs_websocket_get_api_version(); uint apiVersion = obs_websocket_get_api_version();
blog(LOG_INFO, "[obs_module_post_load] obs-websocket plugin API version: %u", apiVersion); blog(LOG_INFO, "[test_register_vendor] obs-websocket plugin API version: %u", apiVersion);
// Test calling obs-websocket requests // Test calling obs-websocket requests
struct obs_websocket_request_response *response = obs_websocket_call_request("GetVersion"); struct obs_websocket_request_response *response = obs_websocket_call_request("GetVersion");
if (response) { if (response) {
blog(LOG_INFO, "[obs_module_post_load] Called GetVersion. Status Code: %u | Comment: %s | Response Data: %s", blog(LOG_INFO, "[test_register_vendor] Called GetVersion. Status Code: %u | Comment: %s | Response Data: %s",
response->status_code, response->comment, response->response_data); response->status_code, response->comment, response->response_data);
obs_websocket_request_response_free(response); obs_websocket_request_response_free(response);
} }
@ -212,17 +229,17 @@ void obs_module_post_load()
// Test vendor creation // Test vendor creation
auto vendor = obs_websocket_register_vendor("obs-websocket-test"); auto vendor = obs_websocket_register_vendor("obs-websocket-test");
if (!vendor) { if (!vendor) {
blog(LOG_WARNING, "[obs_module_post_load] Failed to create vendor!"); blog(LOG_WARNING, "[test_register_vendor] Failed to create vendor!");
return; return;
} }
// Test vendor request registration // Test vendor request registration
if (!obs_websocket_vendor_register_request(vendor, "TestRequest", test_vendor_request_cb, vendor)) { if (!obs_websocket_vendor_register_request(vendor, "TestRequest", test_vendor_request_cb, vendor)) {
blog(LOG_WARNING, "[obs_module_post_load] Failed to register vendor request!"); blog(LOG_WARNING, "[test_register_vendor] Failed to register vendor request!");
return; return;
} }
blog(LOG_INFO, "[obs_module_post_load] Post load completed."); blog(LOG_INFO, "[test_register_vendor] Post load completed.");
} }
#endif #endif

View File

@ -31,17 +31,13 @@ struct SerialFrameBatch {
json &variables; json &variables;
bool haltOnFailure; bool haltOnFailure;
size_t frameCount; size_t frameCount = 0;
size_t sleepUntilFrame; size_t sleepUntilFrame = 0;
std::mutex conditionMutex; std::mutex conditionMutex;
std::condition_variable condition; std::condition_variable condition;
SerialFrameBatch(RequestHandler &requestHandler, json &variables, bool haltOnFailure) SerialFrameBatch(RequestHandler &requestHandler, json &variables, bool haltOnFailure)
: requestHandler(requestHandler), : requestHandler(requestHandler), variables(variables), haltOnFailure(haltOnFailure)
variables(variables),
haltOnFailure(haltOnFailure),
frameCount(0),
sleepUntilFrame(0)
{ {
} }
}; };

View File

@ -31,7 +31,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "../utils/Platform.h" #include "../utils/Platform.h"
#include "../utils/Compat.h" #include "../utils/Compat.h"
WebSocketServer::WebSocketServer() : QObject(nullptr), _sessions() WebSocketServer::WebSocketServer() : QObject(nullptr)
{ {
_server.get_alog().clear_channels(websocketpp::log::alevel::all); _server.get_alog().clear_channels(websocketpp::log::alevel::all);
_server.get_elog().clear_channels(websocketpp::log::elevel::all); _server.get_elog().clear_channels(websocketpp::log::elevel::all);
@ -52,7 +52,7 @@ WebSocketServer::WebSocketServer() : QObject(nullptr), _sessions()
eventHandler->SetBroadcastCallback(std::bind(&WebSocketServer::BroadcastEvent, this, std::placeholders::_1, eventHandler->SetBroadcastCallback(std::bind(&WebSocketServer::BroadcastEvent, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
eventHandler->SetObsLoadedCallback(std::bind(&WebSocketServer::onObsLoaded, this)); eventHandler->SetObsReadyCallback(std::bind(&WebSocketServer::onObsReady, this, std::placeholders::_1));
} }
WebSocketServer::~WebSocketServer() WebSocketServer::~WebSocketServer()
@ -205,18 +205,9 @@ std::vector<WebSocketServer::WebSocketSessionState> WebSocketServer::GetWebSocke
return webSocketSessions; return webSocketSessions;
} }
void WebSocketServer::onObsLoaded() void WebSocketServer::onObsReady(bool ready)
{ {
auto conf = GetConfig(); _obsReady = ready;
if (!conf) {
blog(LOG_ERROR, "[WebSocketServer::onObsLoaded] Unable to retreive config!");
return;
}
if (conf->ServerEnabled) {
blog(LOG_INFO, "[WebSocketServer::onObsLoaded] WebSocket server is enabled, starting...");
Start();
}
} }
bool WebSocketServer::onValidate(websocketpp::connection_hdl hdl) bool WebSocketServer::onValidate(websocketpp::connection_hdl hdl)

View File

@ -77,7 +77,7 @@ private:
void ServerRunner(); void ServerRunner();
void onObsLoaded(); void onObsReady(bool loaded);
bool onValidate(websocketpp::connection_hdl hdl); bool onValidate(websocketpp::connection_hdl hdl);
void onOpen(websocketpp::connection_hdl hdl); void onOpen(websocketpp::connection_hdl hdl);
void onClose(websocketpp::connection_hdl hdl); void onClose(websocketpp::connection_hdl hdl);
@ -96,4 +96,6 @@ private:
std::mutex _sessionMutex; std::mutex _sessionMutex;
std::map<websocketpp::connection_hdl, SessionPtr, std::owner_less<websocketpp::connection_hdl>> _sessions; std::map<websocketpp::connection_hdl, SessionPtr, std::owner_less<websocketpp::connection_hdl>> _sessions;
std::atomic<bool> _obsReady = false;
}; };

View File

@ -209,13 +209,17 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
return; return;
} }
RequestHandler requestHandler(session);
std::string requestType = payloadData["requestType"]; std::string requestType = payloadData["requestType"];
json requestData = payloadData["requestData"]; RequestResult requestResult;
Request request(requestType, requestData); if (_obsReady) {
json requestData = payloadData["requestData"];
Request request(requestType, requestData);
RequestResult requestResult = requestHandler.ProcessRequest(request); RequestHandler requestHandler(session);
requestResult = requestHandler.ProcessRequest(request);
} else {
requestResult = RequestResult::Error(RequestStatus::NotReady, "OBS is not ready to perform the request.");
}
json resultPayloadData; json resultPayloadData;
resultPayloadData["requestType"] = requestType; resultPayloadData["requestType"] = requestType;
@ -303,22 +307,34 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
} }
std::vector<json> requests = payloadData["requests"]; std::vector<json> requests = payloadData["requests"];
std::vector<RequestResult> resultsVector;
if (_obsReady) {
std::vector<RequestBatchRequest> requestsVector;
for (auto &requestJson : requests) {
if (!requestJson["requestType"].is_string())
requestJson["requestType"] =
""; // Workaround for what would otherwise be extensive additional logic for a rare edge case
std::string requestType = requestJson["requestType"];
json requestData = requestJson["requestData"];
json inputVariables = requestJson["inputVariables"];
json outputVariables = requestJson["outputVariables"];
requestsVector.emplace_back(requestType, requestData, executionType, inputVariables,
outputVariables);
}
std::vector<RequestBatchRequest> requestsVector; resultsVector = RequestBatchHandler::ProcessRequestBatch(
for (auto &requestJson : requests) { _threadPool, session, executionType, requestsVector, payloadData["variables"], haltOnFailure);
if (!requestJson["requestType"].is_string()) } else {
requestJson["requestType"] = // I lowkey hate this, but whatever
""; // Workaround for what would otherwise be extensive additional logic for a rare edge case if (haltOnFailure) {
std::string requestType = requestJson["requestType"]; resultsVector.emplace_back(RequestStatus::NotReady, "OBS is not ready to perform the request.");
json requestData = requestJson["requestData"]; } else {
json inputVariables = requestJson["inputVariables"]; for (size_t i = 0; i < requests.size(); i++)
json outputVariables = requestJson["outputVariables"]; resultsVector.emplace_back(RequestStatus::NotReady,
requestsVector.emplace_back(requestType, requestData, executionType, inputVariables, outputVariables); "OBS is not ready to perform the request.");
}
} }
auto resultsVector = RequestBatchHandler::ProcessRequestBatch(_threadPool, session, executionType, requestsVector,
payloadData["variables"], haltOnFailure);
size_t i = 0; size_t i = 0;
std::vector<json> results; std::vector<json> results;
for (auto &requestResult : resultsVector) { for (auto &requestResult : resultsVector) {
@ -342,7 +358,7 @@ void WebSocketServer::ProcessMessage(SessionPtr session, WebSocketServer::Proces
void WebSocketServer::BroadcastEvent(uint64_t requiredIntent, const std::string &eventType, const json &eventData, void WebSocketServer::BroadcastEvent(uint64_t requiredIntent, const std::string &eventType, const json &eventData,
uint8_t rpcVersion) uint8_t rpcVersion)
{ {
if (!_server.is_listening()) if (!_server.is_listening() || !_obsReady)
return; return;
_threadPool.start(Utils::Compat::CreateFunctionRunnable([=]() { _threadPool.start(Utils::Compat::CreateFunctionRunnable([=]() {