diff --git a/CMakeLists.txt b/CMakeLists.txt index fa9f2e6d..d023baa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,7 @@ set(obs-websocket_SOURCES src/requesthandler/RequestHandler_Sources.cpp src/requesthandler/RequestHandler_Scenes.cpp src/requesthandler/RequestHandler_Inputs.cpp + src/requesthandler/RequestHandler_Stream.cpp src/requesthandler/rpc/Request.cpp src/requesthandler/rpc/RequestResult.cpp src/forms/SettingsDialog.cpp diff --git a/src/requesthandler/RequestHandler.cpp b/src/requesthandler/RequestHandler.cpp index 8bc50269..8e2fcfc3 100644 --- a/src/requesthandler/RequestHandler.cpp +++ b/src/requesthandler/RequestHandler.cpp @@ -22,6 +22,8 @@ const std::map RequestHandler::_handlerMap {"SetProfileParameter", &RequestHandler::SetProfileParameter}, {"GetVideoSettings", &RequestHandler::GetVideoSettings}, {"SetVideoSettings", &RequestHandler::SetVideoSettings}, + {"GetStreamServiceSettings", &RequestHandler::GetStreamServiceSettings}, + {"SetStreamServiceSettings", &RequestHandler::SetStreamServiceSettings}, // Sources {"GetSourceActive", &RequestHandler::GetSourceActive}, @@ -51,6 +53,11 @@ const std::map RequestHandler::_handlerMap {"SetInputVolume", &RequestHandler::SetInputVolume}, {"SetInputName", &RequestHandler::SetInputName}, {"CreateInput", &RequestHandler::CreateInput}, + + // Stream + {"GetStreamStatus", &RequestHandler::GetStreamStatus}, + {"StartStream", &RequestHandler::StartStream}, + {"StopStream", &RequestHandler::StopStream}, }; RequestResult RequestHandler::ProcessRequest(const Request& request) diff --git a/src/requesthandler/RequestHandler.h b/src/requesthandler/RequestHandler.h index 6499c82d..46cd542a 100644 --- a/src/requesthandler/RequestHandler.h +++ b/src/requesthandler/RequestHandler.h @@ -37,6 +37,8 @@ class RequestHandler { RequestResult SetProfileParameter(const Request&); RequestResult GetVideoSettings(const Request&); RequestResult SetVideoSettings(const Request&); + RequestResult GetStreamServiceSettings(const Request&); + RequestResult SetStreamServiceSettings(const Request&); // Sources RequestResult GetSourceActive(const Request&); @@ -67,5 +69,10 @@ class RequestHandler { RequestResult SetInputName(const Request&); RequestResult CreateInput(const Request&); + // Stream + RequestResult GetStreamStatus(const Request&); + RequestResult StartStream(const Request&); + RequestResult StopStream(const Request&); + static const std::map _handlerMap; }; diff --git a/src/requesthandler/RequestHandler_Config.cpp b/src/requesthandler/RequestHandler_Config.cpp index fd063964..37dd7ef4 100644 --- a/src/requesthandler/RequestHandler_Config.cpp +++ b/src/requesthandler/RequestHandler_Config.cpp @@ -175,3 +175,55 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request) return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify at least one video-changing pair."); } + +RequestResult RequestHandler::GetStreamServiceSettings(const Request& request) +{ + json responseData; + + OBSService service = obs_frontend_get_streaming_service(); + responseData["streamServiceType"] = obs_service_get_type(service); + OBSDataAutoRelease serviceSettings = obs_service_get_settings(service); + responseData["streamServiceSettings"] = Utils::Json::ObsDataToJson(serviceSettings, true); + + return RequestResult::Success(responseData); +} + +RequestResult RequestHandler::SetStreamServiceSettings(const Request& request) +{ + if (obs_frontend_streaming_active()) + return RequestResult::Error(RequestStatus::StreamRunning); + + RequestStatus::RequestStatus statusCode; + std::string comment; + if (!(request.ValidateString("streamServiceType", statusCode, comment) && request.ValidateObject("streamServiceSettings", statusCode, comment))) + return RequestResult::Error(statusCode, comment); + + OBSService currentStreamService = obs_frontend_get_streaming_service(); + + std::string streamServiceType = obs_service_get_type(currentStreamService); + std::string requestedStreamServiceType = request.RequestData["streamServiceType"]; + OBSDataAutoRelease requestedStreamServiceSettings = Utils::Json::JsonToObsData(request.RequestData["streamServiceSettings"]); + + // Don't create a new service if the current service is the same type. + if (streamServiceType == requestedStreamServiceType) { + OBSDataAutoRelease currentStreamServiceSettings = obs_service_get_settings(currentStreamService); + + OBSDataAutoRelease newStreamServiceSettings = obs_data_create(); + obs_data_apply(newStreamServiceSettings, currentStreamServiceSettings); + obs_data_apply(newStreamServiceSettings, requestedStreamServiceSettings); + + obs_service_update(currentStreamService, newStreamServiceSettings); + } else { + // TODO: This leaks memory. I have no idea why. + OBSService newStreamService = obs_service_create(requestedStreamServiceType.c_str(), "obs_websocket_custom_service", requestedStreamServiceSettings, NULL); + // TODO: Check service type here, instead of relying on service creation to fail. + if (!newStreamService) + return RequestResult::Error(RequestStatus::StreamServiceCreationFailed, "Creating the stream service with the requested streamServiceType failed. It may be an invalid type."); + + obs_frontend_set_streaming_service(newStreamService); + } + + obs_frontend_save_streaming_service(); + + return RequestResult::Success(); +} diff --git a/src/requesthandler/RequestHandler_Stream.cpp b/src/requesthandler/RequestHandler_Stream.cpp new file mode 100644 index 00000000..63f028c6 --- /dev/null +++ b/src/requesthandler/RequestHandler_Stream.cpp @@ -0,0 +1,36 @@ +#include "RequestHandler.h" +#include "../plugin-macros.generated.h" + +RequestResult RequestHandler::GetStreamStatus(const Request& request) +{ + json responseData; + + OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output(); + responseData["outputActive"] = obs_output_active(streamOutput); + responseData["outputTimecode"] = Utils::Obs::StringHelper::GetOutputTimecodeString(streamOutput); + responseData["outputDuration"] = Utils::Obs::NumberHelper::GetOutputDuration(streamOutput); + + return RequestResult::Success(responseData); +} + +RequestResult RequestHandler::StartStream(const Request& request) +{ + if (obs_frontend_streaming_active()) + return RequestResult::Error(RequestStatus::StreamRunning); + + // TODO: Call signal directly to perform blocking wait + obs_frontend_streaming_start(); + + return RequestResult::Success(); +} + +RequestResult RequestHandler::StopStream(const Request& request) +{ + if (!obs_frontend_streaming_active()) + return RequestResult::Error(RequestStatus::StreamNotRunning); + + // TODO: Call signal directly to perform blocking wait + obs_frontend_streaming_stop(); + + return RequestResult::Success(); +} diff --git a/src/requesthandler/rpc/RequestStatus.h b/src/requesthandler/rpc/RequestStatus.h index f0d2ac92..abbc4066 100644 --- a/src/requesthandler/rpc/RequestStatus.h +++ b/src/requesthandler/rpc/RequestStatus.h @@ -118,5 +118,7 @@ namespace RequestStatus { DirectoryCreationFailed = 706, // The combination of request parameters cannot be used to perform an action CannotAct = 707, + // Creation of a new stream service failed + StreamServiceCreationFailed = 708, }; }; diff --git a/src/utils/Obs.cpp b/src/utils/Obs.cpp index f7b144fe..de5f7fdc 100644 --- a/src/utils/Obs.cpp +++ b/src/utils/Obs.cpp @@ -1,4 +1,9 @@ +#include +#include +#include +#include #include +#include #include "Utils.h" #include "../obs-websocket.h" @@ -6,6 +11,17 @@ #define CASE(x) case x: return #x; +template +std::string string_format( const std::string& format, Args ... args ) +{ + int size_s = std::snprintf( nullptr, 0, format.c_str(), args ... ) + 1; + if( size_s <= 0 ){ throw std::runtime_error( "Error during formatting." ); } + auto size = static_cast( size_s ); + auto buf = std::make_unique( size ); + std::snprintf( buf.get(), size, format.c_str(), args ... ); + return std::string( buf.get(), buf.get() + size - 1 ); // We don't want the '\0' inside +} + std::vector ConvertStringArray(char **array) { std::vector ret; @@ -107,6 +123,40 @@ std::string Utils::Obs::StringHelper::GetLastReplayBufferFilePath() return ret; } +std::string Utils::Obs::StringHelper::GetOutputTimecodeString(obs_output_t *output) +{ + if (!output || !obs_output_active(output)) + return "00:00:00.000"; + + video_t* video = obs_output_video(output); + uint64_t frameTimeNs = video_output_get_frame_time(video); + int totalFrames = obs_output_get_total_frames(output); + + uint64_t ms = (((uint64_t)totalFrames) * frameTimeNs) / 1000000ULL; + uint64_t secs = ms / 1000ULL; + uint64_t minutes = secs / 60ULL; + + uint64_t hoursPart = minutes / 60ULL; + uint64_t minutesPart = minutes % 60ULL; + uint64_t secsPart = secs % 60ULL; + uint64_t msPart = ms % 1000ULL; + + return string_format("%02" PRIu64 ":%02" PRIu64 ":%02" PRIu64 ".%03" PRIu64, hoursPart, minutesPart, secsPart, msPart); +} + +uint64_t Utils::Obs::NumberHelper::GetOutputDuration(obs_output_t *output) +{ + if (!output || !obs_output_active(output)) + return 0; + + video_t* video = obs_output_video(output); + uint64_t frameTimeNs = video_output_get_frame_time(video); + int totalFrames = obs_output_get_total_frames(output); + + return util_mul_div64(totalFrames, frameTimeNs, 1000000ULL); + //return (((uint64_t)totalFrames) * frameTimeNs) / ; +} + std::vector Utils::Obs::ListHelper::GetSceneCollectionList() { char** sceneCollections = obs_frontend_get_scene_collections(); diff --git a/src/utils/Utils.h b/src/utils/Utils.h index 4e0b1463..b3925d29 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -38,6 +38,11 @@ namespace Utils { std::string GetInputMonitorTypeString(obs_source_t *input); std::string GetMediaInputStateString(obs_source_t *input); std::string GetLastReplayBufferFilePath(); + std::string GetOutputTimecodeString(obs_output_t *output); + } + + namespace NumberHelper { + uint64_t GetOutputDuration(obs_output_t *output); } namespace ListHelper {