diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ca0a67d..e29d4688 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,18 @@ if(WIN32) message(FATAL_ERROR "Could not find OBS Frontend API\'s library !") endif() + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(ARCH_NAME "64bit") + set(OBS_BUILDDIR_ARCH "build64") + else() + set(ARCH_NAME "32bit") + set(OBS_BUILDDIR_ARCH "build32") + endif() + + include_directories( + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/UI" + ) + target_link_libraries(obs-websocket "${OBS_FRONTEND_LIB}") @@ -84,14 +96,6 @@ if(WIN32) # The "release" folder has a structure similar OBS' one on Windows set(RELEASE_DIR "${PROJECT_SOURCE_DIR}/release") - if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(ARCH_NAME "64bit") - set(OBS_BUILDDIR_ARCH "build64") - else() - set(ARCH_NAME "32bit") - set(OBS_BUILDDIR_ARCH "build32") - endif() - add_custom_command(TARGET obs-websocket POST_BUILD COMMAND if $==1 ( "${CMAKE_COMMAND}" -E make_directory diff --git a/Utils.cpp b/Utils.cpp index 98cca62b..1f55b50c 100644 --- a/Utils.cpp +++ b/Utils.cpp @@ -432,12 +432,12 @@ QString* Utils::ParseDataToQueryString(obs_data_t* data) { do { if (!obs_data_item_has_user_value(item)) continue; - + if (!isFirst) query->append('&'); else isFirst = false; - + const char* attrName = obs_data_item_get_name(item); query->append(attrName).append("="); @@ -473,3 +473,55 @@ QString* Utils::ParseDataToQueryString(obs_data_t* data) { } return query; } + +obs_hotkey_t* Utils::FindHotkeyByName(const char* name) { + struct current_search { + const char* query; + obs_hotkey_t* result; + }; + + current_search search; + search.query = name; + search.result = nullptr; + + obs_enum_hotkeys([](void* data, obs_hotkey_id id, obs_hotkey_t* hotkey) { + current_search* search = static_cast(data); + + const char* hk_name = obs_hotkey_get_name(hotkey); + if (strcmp(hk_name, search->query) == 0) { + search->result = hotkey; + blog(LOG_INFO, "Utils::FindHotkeyByName: found %s", hk_name); + return false; + } + return true; + }, &search); + + return search.result; +} + +bool Utils::ReplayBufferEnabled() { + config_t* profile = obs_frontend_get_profile_config(); + const char* outputMode = config_get_string(profile, "Output", "Mode"); + + if (strcmp(outputMode, "Simple") == 0) { + return config_get_bool(profile, "SimpleOutput", "RecRB"); + } + + return false; +} + +bool Utils::RPHotkeySet() { + obs_output_t* rp_output = obs_frontend_get_replay_buffer_output(); + + obs_data_t *hotkeys = obs_hotkeys_save_output(rp_output); + obs_data_array_t *bindings = obs_data_get_array(hotkeys, + "ReplayBuffer.Save"); + + size_t count = obs_data_array_count(bindings); + + obs_data_array_release(bindings); + obs_data_release(hotkeys); + obs_output_release(rp_output); + + return (count > 0); +} diff --git a/Utils.h b/Utils.h index 568fabef..6b75e9c4 100644 --- a/Utils.h +++ b/Utils.h @@ -76,8 +76,11 @@ class Utils { static QString FormatIPAddress(QHostAddress &addr); static const char* GetRecordingFolder(); static bool SetRecordingFolder(const char* path); - + static QString* ParseDataToQueryString(obs_data_t * data); + static obs_hotkey_t* FindHotkeyByName(const char* name); + static bool ReplayBufferEnabled(); + static bool RPHotkeySet(); }; #endif // UTILS_H diff --git a/WSEvents.cpp b/WSEvents.cpp index c1381da8..0e0983d3 100644 --- a/WSEvents.cpp +++ b/WSEvents.cpp @@ -164,6 +164,18 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private owner->_recording_active = false; owner->OnRecordingStopped(); } + else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) { + owner->OnReplayStarting(); + } + else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED) { + owner->OnReplayStarted(); + } + else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING) { + owner->OnReplayStopping(); + } + else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) { + owner->OnReplayStopped(); + } else if (event == OBS_FRONTEND_EVENT_EXIT) { owner->OnExit(); } @@ -500,6 +512,50 @@ void WSEvents::OnRecordingStopped() { broadcastUpdate("RecordingStopped"); } +/** +* A request to start the replay buffer has been issued. +* +* @api events +* @name ReplayStarting +* @category replay buffer +*/ +void WSEvents::OnReplayStarting() { + broadcastUpdate("ReplayStarting"); +} + +/** +* Replay Buffer started successfully +* +* @api events +* @name ReplayStarted +* @category replay buffer +*/ +void WSEvents::OnReplayStarted() { + broadcastUpdate("ReplayStarted"); +} + +/** +* A request to start the replay buffer has been issued. +* +* @api events +* @name ReplayStopping +* @category replay buffer +*/ +void WSEvents::OnReplayStopping() { + broadcastUpdate("ReplayStopping"); +} + +/** +* Replay Buffer stopped successfully +* +* @api events +* @name ReplayStopped +* @category replay buffer +*/ +void WSEvents::OnReplayStopped() { + broadcastUpdate("ReplayStopped"); +} + /** * OBS is exiting. * diff --git a/WSEvents.h b/WSEvents.h index 95e62820..f93e0a74 100644 --- a/WSEvents.h +++ b/WSEvents.h @@ -87,6 +87,11 @@ class WSEvents : public QObject { void OnRecordingStopping(); void OnRecordingStopped(); + void OnReplayStarting(); + void OnReplayStarted(); + void OnReplayStopping(); + void OnReplayStopped(); + void OnExit(); static void OnTransitionBegin(void* param, calldata_t* data); diff --git a/WSRequestHandler.cpp b/WSRequestHandler.cpp index 0a67e640..5cc9f72e 100644 --- a/WSRequestHandler.cpp +++ b/WSRequestHandler.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include "WSEvents.h" @@ -63,6 +64,11 @@ WSRequestHandler::WSRequestHandler(QWebSocket* client) : messageMap["StartRecording"] = WSRequestHandler::HandleStartRecording; messageMap["StopRecording"] = WSRequestHandler::HandleStopRecording; + messageMap["StartStopReplayBuffer"] = WSRequestHandler::HandleStartStopReplayBuffer; + messageMap["StartReplayBuffer"] = WSRequestHandler::HandleStartReplayBuffer; + messageMap["StopReplayBuffer"] = WSRequestHandler::HandleStopReplayBuffer; + messageMap["SaveReplayBuffer"] = WSRequestHandler::HandleSaveReplayBuffer; + messageMap["SetRecordingFolder"] = WSRequestHandler::HandleSetRecordingFolder; messageMap["GetRecordingFolder"] = WSRequestHandler::HandleGetRecordingFolder; @@ -184,7 +190,7 @@ void WSRequestHandler::SendResponse(obs_data_t* response) { _client->sendTextMessage(json); if (Config::Current()->DebugEnabled) blog(LOG_DEBUG, "Response << '%s'", json); - + obs_data_release(response); } @@ -505,23 +511,23 @@ void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) if (obs_frontend_streaming_active() == false) { obs_data_t* streamData = obs_data_get_obj(req->data, "stream"); obs_service_t* currentService = nullptr; - + if (streamData) { currentService = obs_frontend_get_streaming_service(); obs_service_addref(currentService); - + obs_service_t* service = _service; const char* currentServiceType = obs_service_get_type(currentService); - + const char* requestedType = obs_data_has_user_value(streamData, "type") ? obs_data_get_string(streamData, "type") : currentServiceType; const char* serviceType = service != nullptr ? obs_service_get_type(service) : currentServiceType; obs_data_t* settings = obs_data_get_obj(streamData, "settings"); - + obs_data_t* metadata = obs_data_get_obj(streamData, "metadata"); QString* query = Utils::ParseDataToQueryString(metadata); - + if (strcmp(requestedType, serviceType) != 0) { if (settings) { obs_service_release(service); @@ -534,20 +540,20 @@ void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) //if type isn't changing we should overlay the settings we got with the existing settings obs_data_t* existingSettings = obs_service_get_settings(currentService); obs_data_t* newSettings = obs_data_create(); //by doing this you can send a request to the websocket that only contains a setting you want to change instead of having to do a get and then change them - + obs_data_apply(newSettings, existingSettings); //first apply the existing settings - + obs_data_apply(newSettings, settings); //then apply the settings from the request should they exist obs_data_release(settings); - + settings = newSettings; obs_data_release(existingSettings); } - + if (!service){ service = obs_service_create(requestedType, "websocket_custom_service", settings, nullptr); } - + //Supporting adding metadata parameters to key query string if (query && query->length() > 0) { const char* key = obs_data_get_string(settings, "key"); @@ -568,7 +574,7 @@ void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) key = query->toUtf8(); obs_data_set_string(settings, "key", key); } - + obs_service_update(service, settings); obs_data_release(settings); obs_data_release(metadata); @@ -579,13 +585,13 @@ void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) obs_service_release(_service); _service = nullptr; } - + obs_frontend_streaming_start(); - + if (_service != nullptr) { obs_frontend_set_streaming_service(currentService); } - + req->SendOKResponse(); obs_service_release(currentService); } else { @@ -644,6 +650,98 @@ void WSRequestHandler::HandleStopRecording(WSRequestHandler* req) { } } +/** +* Toggle the Replay Buffer on/off. +* +* @api requests +* @name StartStopReplayBuffer +* @category replay buffer +*/ +void WSRequestHandler::HandleStartStopReplayBuffer(WSRequestHandler* req) { + if (obs_frontend_replay_buffer_active()) { + obs_frontend_replay_buffer_stop(); + } else { + if (!Utils::RPHotkeySet()) { + req->SendErrorResponse("replay buffer hotkey not set"); + return; + } + obs_frontend_replay_buffer_start(); + } + req->SendOKResponse(); +} + +/** +* Start recording into the Replay Buffer. +* Will return an `error` if the Replay Buffer is already active or if the +* "Save Replay Buffer" hotkey is not set in OBS' settings. +* Setting this hotkey is mandatory, even when triggering saves only +* through obs-websocket. +* +* @api requests +* @name StartReplayBuffer +* @category replay buffer +*/ +void WSRequestHandler::HandleStartReplayBuffer(WSRequestHandler* req) { + if (!Utils::ReplayBufferEnabled()) { + req->SendErrorResponse("replay buffer disabled in settings"); + return; + } + + if (obs_frontend_replay_buffer_active() == true) { + req->SendErrorResponse("replay buffer already active"); + return; + } + + if (!Utils::RPHotkeySet()) { + req->SendErrorResponse("replay buffer hotkey not set"); + return; + } + + obs_frontend_replay_buffer_start(); + req->SendOKResponse(); +} + +/** +* Stop recording into the Replay Buffer. +* Will return an `error` if the Replay Buffer is not active. +* +* @api requests +* @name StopReplayBuffer +* @category replay buffer +*/ +void WSRequestHandler::HandleStopReplayBuffer(WSRequestHandler* req) { + if (obs_frontend_replay_buffer_active() == true) { + obs_frontend_replay_buffer_stop(); + req->SendOKResponse(); + } else { + req->SendErrorResponse("replay buffer not active"); + } +} + +/** +* Save and flush the contents of the Replay Buffer to disk. This is +* basically the same as triggering the "Save Replay Buffer" hotkey. +* Will return an `error` if the Replay Buffer is not active. +* +* @api requests +* @name SaveReplayBuffer +* @category replay buffer +*/ +void WSRequestHandler::HandleSaveReplayBuffer(WSRequestHandler* req) { + if (!obs_frontend_replay_buffer_active()) { + req->SendErrorResponse("replay buffer not active"); + return; + } + + obs_hotkey_t* hk = Utils::FindHotkeyByName("ReplayBuffer.Save"); + if (hk) { + obs_hotkey_trigger_routed_callback(obs_hotkey_get_id(hk), true); + req->SendOKResponse(); + } else { + req->SendErrorResponse("failed to save replay buffer"); + } +} + /** * List of all transitions available in the frontend's dropdown menu. * @@ -1298,13 +1396,13 @@ void WSRequestHandler::HandleGetCurrentProfile(WSRequestHandler* req) { */ void WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) { obs_service_t* service = obs_frontend_get_streaming_service(); - + obs_data_t* settings = obs_data_get_obj(req->data, "settings"); if (!settings) { req->SendErrorResponse("'settings' are required'"); return; } - + const char* serviceType = obs_service_get_type(service); const char* requestedType = obs_data_get_string(req->data, "type"); if (requestedType != nullptr && strcmp(requestedType, serviceType) != 0) { @@ -1316,27 +1414,27 @@ void WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) { //if type isn't changing we should overlay the settings we got with the existing settings obs_data_t* existingSettings = obs_service_get_settings(service); //by doing this you can send a request to the websocket that only contains a setting you want to change instead of having to do a get and then change them - obs_data_t* newSettings = obs_data_create(); - + obs_data_t* newSettings = obs_data_create(); + obs_data_apply(newSettings, existingSettings); //first apply the existing settings obs_data_apply(newSettings, settings); //then apply the settings from the request - + obs_data_release(settings); obs_data_release(existingSettings); - + obs_service_update(service, settings); settings = newSettings; } - + //if save is specified we should immediately save the streaming service if (obs_data_get_bool(req->data, "save")) { obs_frontend_save_streaming_service(); } - + obs_data_t* response = obs_data_create(); obs_data_set_string(response, "type", requestedType); obs_data_set_obj(response, "settings", settings); - + req->SendOKResponse(response); obs_data_release(settings); obs_data_release(response); @@ -1361,11 +1459,11 @@ void WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) { obs_service_t* service = obs_frontend_get_streaming_service(); const char* serviceType = obs_service_get_type(service); obs_data_t* settings = obs_service_get_settings(service); - + obs_data_t* response = obs_data_create(); obs_data_set_string(response, "type", serviceType); obs_data_set_obj(response, "settings", settings); - + req->SendOKResponse(response); obs_data_release(settings); obs_data_release(response); @@ -1638,7 +1736,7 @@ void WSRequestHandler::HandleSetRecordingFolder(WSRequestHandler* req) { /** * Get the path of the current recording folder. * - * @return {Stsring} `rec-folder` Path of the recording folder. + * @return {String} `rec-folder` Path of the recording folder. * * @api requests * @name GetRecordingFolder @@ -1799,37 +1897,37 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { obs_data_t* settings = obs_source_get_settings(sceneItemSource); if (req->hasField("align")) { - obs_data_set_string(settings, "align", + obs_data_set_string(settings, "align", obs_data_get_string(req->data, "align")); } if (req->hasField("bk_color")) { - obs_data_set_int(settings, "bk_color", + obs_data_set_int(settings, "bk_color", obs_data_get_int(req->data, "bk_color")); } if (req->hasField("bk-opacity")) { - obs_data_set_int(settings, "bk_opacity", + obs_data_set_int(settings, "bk_opacity", obs_data_get_int(req->data, "bk_opacity")); } if (req->hasField("chatlog")) { - obs_data_set_bool(settings, "chatlog", + obs_data_set_bool(settings, "chatlog", obs_data_get_bool(req->data, "chatlog")); } - + if (req->hasField("chatlog_lines")) { obs_data_set_int(settings, "chatlog_lines", obs_data_get_int(req->data, "chatlog_lines")); } if (req->hasField("color")) { - obs_data_set_int(settings, "color", + obs_data_set_int(settings, "color", obs_data_get_int(req->data, "color")); } if (req->hasField("extents")) { - obs_data_set_bool(settings, "extents", + obs_data_set_bool(settings, "extents", obs_data_get_bool(req->data, "extents")); } @@ -1849,7 +1947,7 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } if (req->hasField("file")) { - obs_data_set_string(settings, "file", + obs_data_set_string(settings, "file", obs_data_get_string(req->data, "file")); } @@ -1869,7 +1967,7 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } if (obs_data_has_user_value(req_font_obj, "size")) { - obs_data_set_int(font_obj, "size", + obs_data_set_int(font_obj, "size", obs_data_get_int(req_font_obj, "size")); } @@ -1884,7 +1982,7 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } if (req->hasField("gradient")) { - obs_data_set_bool(settings, "gradient", + obs_data_set_bool(settings, "gradient", obs_data_get_bool(req->data, "gradient")); } @@ -1904,7 +2002,7 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } if (req->hasField("outline")) { - obs_data_set_bool(settings, "outline", + obs_data_set_bool(settings, "outline", obs_data_get_bool(req->data, "outline")); } @@ -1919,34 +2017,34 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } if (req->hasField("outline_opacity")) { - obs_data_set_int(settings, "outline_opacity", + obs_data_set_int(settings, "outline_opacity", obs_data_get_int(req->data, "outline_opacity")); } if (req->hasField("read_from_file")) { - obs_data_set_bool(settings, "read_from_file", + obs_data_set_bool(settings, "read_from_file", obs_data_get_bool(req->data, "read_from_file")); } if (req->hasField("text")) { - obs_data_set_string(settings, "text", + obs_data_set_string(settings, "text", obs_data_get_string(req->data, "text")); } if (req->hasField("valign")) { - obs_data_set_string(settings, "valign", + obs_data_set_string(settings, "valign", obs_data_get_string(req->data, "valign")); } if (req->hasField("vertical")) { - obs_data_set_bool(settings, "vertical", + obs_data_set_bool(settings, "vertical", obs_data_get_bool(req->data, "vertical")); } obs_source_update(sceneItemSource, settings); if (req->hasField("render")) { - obs_sceneitem_set_visible(sceneItem, + obs_sceneitem_set_visible(sceneItem, obs_data_get_bool(req->data, "render")); } @@ -1959,7 +2057,7 @@ void WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) { } else { req->SendErrorResponse("specified scene item doesn't exist"); } - + obs_source_release(scene); } @@ -2100,7 +2198,7 @@ void WSRequestHandler::HandleSetBrowserSourceProperties(WSRequestHandler* req) { obs_data_set_int(settings, "height", obs_data_get_int(req->data, "height")); } - + if (req->hasField("fps")) { obs_data_set_int(settings, "fps", obs_data_get_int(req->data, "fps")); diff --git a/WSRequestHandler.h b/WSRequestHandler.h index 7e86f788..c6598b74 100644 --- a/WSRequestHandler.h +++ b/WSRequestHandler.h @@ -71,6 +71,11 @@ class WSRequestHandler : public QObject { static void HandleStartRecording(WSRequestHandler* req); static void HandleStopRecording(WSRequestHandler* req); + static void HandleStartStopReplayBuffer(WSRequestHandler* req); + static void HandleStartReplayBuffer(WSRequestHandler* req); + static void HandleStopReplayBuffer(WSRequestHandler* req); + static void HandleSaveReplayBuffer(WSRequestHandler* req); + static void HandleSetRecordingFolder(WSRequestHandler* req); static void HandleGetRecordingFolder(WSRequestHandler* req);