diff --git a/src/Utils.cpp b/src/Utils.cpp index 5bd2823d..ae8a2aa0 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,8 @@ with this program. If not, see <https://www.gnu.org/licenses/> #include <obs-frontend-api.h> #include <obs.hpp> +#include <util/platform.h> + #include "obs-websocket.h" #include "Utils.h" @@ -764,3 +766,51 @@ obs_data_array_t* Utils::GetSourceFiltersList(obs_source_t* source, bool include return enumParams.filters; } + +void getPauseRecordingFunctions(RecordingPausedFunction* recPausedFuncPtr, PauseRecordingFunction* pauseRecFuncPtr) +{ + void* frontendApi = os_dlopen("obs-frontend-api"); + + if (recPausedFuncPtr) { + *recPausedFuncPtr = (RecordingPausedFunction)os_dlsym(frontendApi, "obs_frontend_recording_paused"); + } + + if (pauseRecFuncPtr) { + *pauseRecFuncPtr = (PauseRecordingFunction)os_dlsym(frontendApi, "obs_frontend_recording_pause"); + } + + os_dlclose(frontendApi); +} + +bool Utils::RecordingPauseSupported() +{ + RecordingPausedFunction recordingPaused = nullptr; + PauseRecordingFunction pauseRecording = nullptr; + getPauseRecordingFunctions(&recordingPaused, &pauseRecording); + + return (recordingPaused && pauseRecording); +} + +bool Utils::RecordingPaused() +{ + RecordingPausedFunction recordingPaused = nullptr; + getPauseRecordingFunctions(&recordingPaused, nullptr); + + if (recordingPaused == nullptr) { + return false; + } + + return recordingPaused(); +} + +void Utils::PauseRecording(bool pause) +{ + PauseRecordingFunction pauseRecording = nullptr; + getPauseRecordingFunctions(nullptr, &pauseRecording); + + if (pauseRecording == nullptr) { + return; + } + + pauseRecording(pause); +} diff --git a/src/Utils.h b/src/Utils.h index 6a446a7c..6de57eec 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -31,6 +31,9 @@ with this program. If not, see <https://www.gnu.org/licenses/> #include <obs-module.h> #include <util/config-file.h> +typedef void(*PauseRecordingFunction)(bool); +typedef bool(*RecordingPausedFunction)(); + class Utils { public: static obs_data_array_t* StringListToArray(char** strings, const char* key); @@ -78,4 +81,8 @@ class Utils { static const char* GetFilenameFormatting(); static bool SetFilenameFormatting(const char* filenameFormatting); + + static bool RecordingPauseSupported(); + static bool RecordingPaused(); + static void PauseRecording(bool pause); }; diff --git a/src/WSEvents.cpp b/src/WSEvents.cpp index a5525925..a1979492 100644 --- a/src/WSEvents.cpp +++ b/src/WSEvents.cpp @@ -17,7 +17,9 @@ * with this program. If not, see <https://www.gnu.org/licenses/> */ +#include <inttypes.h> #include <util/platform.h> +#include <media-io/video-io.h> #include <QtWidgets/QPushButton> @@ -29,7 +31,7 @@ #define STATUS_INTERVAL 2000 -const char* nsToTimestamp(uint64_t ns) { +QString nsToTimestamp(uint64_t ns) { uint64_t ms = ns / 1000000ULL; uint64_t secs = ms / 1000ULL; uint64_t minutes = secs / 60ULL; @@ -39,10 +41,7 @@ const char* nsToTimestamp(uint64_t ns) { uint64_t secsPart = secs % 60ULL; uint64_t msPart = ms % 1000ULL; - char* ts = (char*)bmalloc(64); - sprintf(ts, "%02llu:%02llu:%02llu.%03llu", hoursPart, minutesPart, secsPart, msPart); - - return ts; + return QString::asprintf("%02" PRIu64 ":%02" PRIu64 ":%02" PRIu64 ".%03" PRIu64, hoursPart, minutesPart, secsPart, msPart); } const char* sourceTypeToString(obs_source_type type) { @@ -75,7 +74,8 @@ const char* calldata_get_string(const calldata_t* data, const char* name) { WSEvents::WSEvents(WSServerPtr srv) : _srv(srv), _streamStarttime(0), - _recStarttime(0), + _lastBytesSent(0), + _lastBytesSentTime(0), HeartbeatIsActive(false), pulse(false) { @@ -206,6 +206,14 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private owner->OnRecordingStopped(); break; + case OBS_FRONTEND_EVENT_RECORDING_PAUSED: + owner->OnRecordingPaused(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED: + owner->OnRecordingResumed(); + break; + case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING: owner->OnReplayStarting(); break; @@ -247,17 +255,14 @@ void WSEvents::broadcastUpdate(const char* updateType, OBSDataAutoRelease update = obs_data_create(); obs_data_set_string(update, "update-type", updateType); - const char* ts = nullptr; if (obs_frontend_streaming_active()) { - ts = nsToTimestamp(os_gettime_ns() - _streamStarttime); - obs_data_set_string(update, "stream-timecode", ts); - bfree((void*)ts); + QString streamingTimecode = getStreamingTimecode(); + obs_data_set_string(update, "stream-timecode", streamingTimecode.toUtf8().constData()); } if (obs_frontend_recording_active()) { - ts = nsToTimestamp(os_gettime_ns() - _recStarttime); - obs_data_set_string(update, "rec-timecode", ts); - bfree((void*)ts); + QString recordingTimecode = getRecordingTimecode(); + obs_data_set_string(update, "rec-timecode", recordingTimecode.toUtf8().constData()); } if (additionalFields) @@ -362,26 +367,34 @@ void WSEvents::unhookTransitionBeginEvent() { obs_frontend_source_list_free(&transitions); } -uint64_t WSEvents::GetStreamingTime() { - if (obs_frontend_streaming_active()) - return (os_gettime_ns() - _streamStarttime); - else +uint64_t getOutputRunningTime(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 (((uint64_t)totalFrames) * frameTimeNs); } -const char* WSEvents::GetStreamingTimecode() { - return nsToTimestamp(GetStreamingTime()); +uint64_t WSEvents::getStreamingTime() { + OBSOutputAutoRelease streamingOutput = obs_frontend_get_streaming_output(); + return getOutputRunningTime(streamingOutput); } -uint64_t WSEvents::GetRecordingTime() { - if (obs_frontend_recording_active()) - return (os_gettime_ns() - _recStarttime); - else - return 0; +uint64_t WSEvents::getRecordingTime() { + OBSOutputAutoRelease recordingOutput = obs_frontend_get_recording_output(); + return getOutputRunningTime(recordingOutput); } -const char* WSEvents::GetRecordingTimecode() { - return nsToTimestamp(GetRecordingTime()); +QString WSEvents::getStreamingTimecode() { + return nsToTimestamp(getStreamingTime()); +} + +QString WSEvents::getRecordingTimecode() { + return nsToTimestamp(getRecordingTime()); } /** @@ -534,6 +547,7 @@ void WSEvents::OnStreamStarting() { void WSEvents::OnStreamStarted() { _streamStarttime = os_gettime_ns(); _lastBytesSent = 0; + broadcastUpdate("StreamStarted"); } @@ -550,7 +564,6 @@ void WSEvents::OnStreamStarted() { void WSEvents::OnStreamStopping() { OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "preview-only", false); - broadcastUpdate("StreamStopping", data); } @@ -564,6 +577,7 @@ void WSEvents::OnStreamStopping() { */ void WSEvents::OnStreamStopped() { _streamStarttime = 0; + broadcastUpdate("StreamStopped"); } @@ -588,7 +602,6 @@ void WSEvents::OnRecordingStarting() { * @since 0.3 */ void WSEvents::OnRecordingStarted() { - _recStarttime = os_gettime_ns(); broadcastUpdate("RecordingStarted"); } @@ -613,10 +626,33 @@ void WSEvents::OnRecordingStopping() { * @since 0.3 */ void WSEvents::OnRecordingStopped() { - _recStarttime = 0; broadcastUpdate("RecordingStopped"); } +/** + * Current recording paused + * + * @api events + * @name RecordingPaused + * @category recording + * @since 4.7.0 + */ +void WSEvents::OnRecordingPaused() { + broadcastUpdate("RecordingPaused"); +} + +/** + * Current recording resumed + * + * @api events + * @name RecordingResumed + * @category recording + * @since 4.7.0 + */ +void WSEvents::OnRecordingResumed() { + broadcastUpdate("RecordingResumed"); +} + /** * A request to start the replay buffer has been issued. * @@ -708,6 +744,7 @@ void WSEvents::OnExit() { void WSEvents::StreamStatus() { bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); + bool recordingPaused = Utils::RecordingPaused(); bool replayBufferActive = obs_frontend_replay_buffer_active(); OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output(); @@ -734,9 +771,7 @@ void WSEvents::StreamStatus() { _lastBytesSent = bytesSent; _lastBytesSentTime = bytesSentTime; - uint64_t totalStreamTime = - (os_gettime_ns() - _streamStarttime) / 1000000000; - + uint64_t totalStreamTime = (getStreamingTime() / 1000000000ULL); int totalFrames = obs_output_get_total_frames(streamOutput); int droppedFrames = obs_output_get_frames_dropped(streamOutput); @@ -746,6 +781,7 @@ void WSEvents::StreamStatus() { obs_data_set_bool(data, "streaming", streamingActive); obs_data_set_bool(data, "recording", recordingActive); + obs_data_set_bool(data, "recording-paused", recordingPaused); obs_data_set_bool(data, "replay-buffer-active", replayBufferActive); obs_data_set_int(data, "bytes-per-sec", bytesPerSec); @@ -792,6 +828,7 @@ void WSEvents::Heartbeat() { bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); + bool recordingPaused = Utils::RecordingPaused(); OBSDataAutoRelease data = obs_data_create(); OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output(); @@ -807,16 +844,15 @@ void WSEvents::Heartbeat() { obs_data_set_bool(data, "streaming", streamingActive); if (streamingActive) { - uint64_t totalStreamTime = (os_gettime_ns() - _streamStarttime) / 1000000000; - obs_data_set_int(data, "total-stream-time", totalStreamTime); + obs_data_set_int(data, "total-stream-time", (getStreamingTime() / 1000000000ULL)); obs_data_set_int(data, "total-stream-bytes", (uint64_t)obs_output_get_total_bytes(streamOutput)); obs_data_set_int(data, "total-stream-frames", obs_output_get_total_frames(streamOutput)); } obs_data_set_bool(data, "recording", recordingActive); + obs_data_set_bool(data, "recording-paused", recordingPaused); if (recordingActive) { - uint64_t totalRecordTime = (os_gettime_ns() - _recStarttime) / 1000000000; - obs_data_set_int(data, "total-record-time", totalRecordTime); + obs_data_set_int(data, "total-record-time", (getRecordingTime() / 1000000000ULL)); obs_data_set_int(data, "total-record-bytes", (uint64_t)obs_output_get_total_bytes(recordOutput)); obs_data_set_int(data, "total-record-frames", obs_output_get_total_frames(recordOutput)); } diff --git a/src/WSEvents.h b/src/WSEvents.h index af6e2da3..85be443f 100644 --- a/src/WSEvents.h +++ b/src/WSEvents.h @@ -43,10 +43,12 @@ public: void hookTransitionBeginEvent(); void unhookTransitionBeginEvent(); - uint64_t GetStreamingTime(); - const char* GetStreamingTimecode(); - uint64_t GetRecordingTime(); - const char* GetRecordingTimecode(); + uint64_t getStreamingTime(); + uint64_t getRecordingTime(); + + QString getStreamingTimecode(); + QString getRecordingTimecode(); + obs_data_t* GetStats(); void OnBroadcastCustomMessage(QString realm, obs_data_t* data); @@ -67,7 +69,6 @@ private: bool pulse; uint64_t _streamStarttime; - uint64_t _recStarttime; uint64_t _lastBytesSent; uint64_t _lastBytesSentTime; @@ -95,6 +96,8 @@ private: void OnRecordingStarted(); void OnRecordingStopping(); void OnRecordingStopped(); + void OnRecordingPaused(); + void OnRecordingResumed(); void OnReplayStarting(); void OnReplayStarted(); diff --git a/src/WSRequestHandler.cpp b/src/WSRequestHandler.cpp index dabf30bc..2ff897fe 100644 --- a/src/WSRequestHandler.cpp +++ b/src/WSRequestHandler.cpp @@ -24,7 +24,7 @@ #include "WSRequestHandler.h" -QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageMap { +QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageMap{ { "GetVersion", WSRequestHandler::HandleGetVersion }, { "GetAuthRequired", WSRequestHandler::HandleGetAuthRequired }, { "Authenticate", WSRequestHandler::HandleAuthenticate }, @@ -57,10 +57,14 @@ QHash<QString, HandlerResponse(*)(WSRequestHandler*)> WSRequestHandler::messageM { "GetStreamingStatus", WSRequestHandler::HandleGetStreamingStatus }, { "StartStopStreaming", WSRequestHandler::HandleStartStopStreaming }, { "StartStopRecording", WSRequestHandler::HandleStartStopRecording }, + { "StartStreaming", WSRequestHandler::HandleStartStreaming }, { "StopStreaming", WSRequestHandler::HandleStopStreaming }, + { "StartRecording", WSRequestHandler::HandleStartRecording }, { "StopRecording", WSRequestHandler::HandleStopRecording }, + { "PauseRecording", WSRequestHandler::HandlePauseRecording }, + { "ResumeRecording", WSRequestHandler::HandleResumeRecording }, { "StartStopReplayBuffer", WSRequestHandler::HandleStartStopReplayBuffer }, { "StartReplayBuffer", WSRequestHandler::HandleStartReplayBuffer }, diff --git a/src/WSRequestHandler.h b/src/WSRequestHandler.h index d9265ebf..bee9566b 100644 --- a/src/WSRequestHandler.h +++ b/src/WSRequestHandler.h @@ -103,10 +103,14 @@ class WSRequestHandler : public QObject { static HandlerResponse HandleGetStreamingStatus(WSRequestHandler* req); static HandlerResponse HandleStartStopStreaming(WSRequestHandler* req); static HandlerResponse HandleStartStopRecording(WSRequestHandler* req); + static HandlerResponse HandleStartStreaming(WSRequestHandler* req); static HandlerResponse HandleStopStreaming(WSRequestHandler* req); + static HandlerResponse HandleStartRecording(WSRequestHandler* req); static HandlerResponse HandleStopRecording(WSRequestHandler* req); + static HandlerResponse HandlePauseRecording(WSRequestHandler* req); + static HandlerResponse HandleResumeRecording(WSRequestHandler* req); static HandlerResponse HandleStartStopReplayBuffer(WSRequestHandler* req); static HandlerResponse HandleStartReplayBuffer(WSRequestHandler* req); diff --git a/src/WSRequestHandler_Recording.cpp b/src/WSRequestHandler_Recording.cpp index b8600935..9164a404 100644 --- a/src/WSRequestHandler_Recording.cpp +++ b/src/WSRequestHandler_Recording.cpp @@ -1,6 +1,20 @@ +#include "WSRequestHandler.h" + +#include <util/platform.h> #include "Utils.h" -#include "WSRequestHandler.h" +HandlerResponse ifCanPause(WSRequestHandler* req, std::function<HandlerResponse()> callback) +{ + if (!obs_frontend_recording_active()) { + return req->SendErrorResponse("recording is not active"); + } + + if (!Utils::RecordingPauseSupported()) { + return req->SendErrorResponse("recording pauses are not available in this version of OBS Studio"); + } + + return callback(); +} /** * Toggle recording on or off. @@ -11,11 +25,7 @@ * @since 0.3 */ HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active()) - obs_frontend_recording_stop(); - else - obs_frontend_recording_start(); - + (obs_frontend_recording_active() ? obs_frontend_recording_stop() : obs_frontend_recording_start()); return req->SendOKResponse(); } @@ -29,12 +39,12 @@ HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req * @since 4.1.0 */ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active() == false) { - obs_frontend_recording_start(); - return req->SendOKResponse(); - } else { + if (obs_frontend_recording_active()) { return req->SendErrorResponse("recording already active"); } + + obs_frontend_recording_start(); + return req->SendOKResponse(); } /** @@ -47,12 +57,52 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { * @since 4.1.0 */ HandlerResponse WSRequestHandler::HandleStopRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active() == true) { - obs_frontend_recording_stop(); - return req->SendOKResponse(); - } else { + if (!obs_frontend_recording_active()) { return req->SendErrorResponse("recording not active"); } + + obs_frontend_recording_stop(); + return req->SendOKResponse(); +} + +/** +* Pause the current recording. +* Returns an error if recording is not active or already paused. +* +* @api requests +* @name PauseRecording +* @category recording +* @since 4.7.0 +*/ +HandlerResponse WSRequestHandler::HandlePauseRecording(WSRequestHandler* req) { + return ifCanPause(req, [req]() { + if (Utils::RecordingPaused()) { + return req->SendErrorResponse("recording already paused"); + } + + Utils::PauseRecording(true); + return req->SendOKResponse(); + }); +} + +/** +* Resume/unpause the current recording (if paused). +* Returns an error if recording is not active or not paused. +* +* @api requests +* @name ResumeRecording +* @category recording +* @since 4.7.0 +*/ +HandlerResponse WSRequestHandler::HandleResumeRecording(WSRequestHandler* req) { + return ifCanPause(req, [req]() { + if (!Utils::RecordingPaused()) { + return req->SendErrorResponse("recording is not paused"); + } + + Utils::PauseRecording(false); + return req->SendOKResponse(); + }); } /** @@ -63,7 +113,6 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { * in progress, the change won't be applied immediately and will be * effective on the next recording. * - * * @param {String} `rec-folder` Path of the recording folder. * * @api requests diff --git a/src/WSRequestHandler_Streaming.cpp b/src/WSRequestHandler_Streaming.cpp index bbbf4bb1..0daea177 100644 --- a/src/WSRequestHandler_Streaming.cpp +++ b/src/WSRequestHandler_Streaming.cpp @@ -26,19 +26,17 @@ HandlerResponse WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "streaming", obs_frontend_streaming_active()); obs_data_set_bool(data, "recording", obs_frontend_recording_active()); + obs_data_set_bool(data, "recording-paused", Utils::RecordingPaused()); obs_data_set_bool(data, "preview-only", false); - const char* tc = nullptr; if (obs_frontend_streaming_active()) { - tc = events->GetStreamingTimecode(); - obs_data_set_string(data, "stream-timecode", tc); - bfree((void*)tc); + QString streamingTimecode = events->getStreamingTimecode(); + obs_data_set_string(data, "stream-timecode", streamingTimecode.toUtf8().constData()); } if (obs_frontend_recording_active()) { - tc = events->GetRecordingTimecode(); - obs_data_set_string(data, "rec-timecode", tc); - bfree((void*)tc); + QString recordingTimecode = events->getRecordingTimecode(); + obs_data_set_string(data, "rec-timecode", recordingTimecode.toUtf8().constData()); } return req->SendOKResponse(data);