diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index ac5411056edcf44e198f4966fba2771b4ae4b5b8..1ed137860cb04ff5e4070a93d8477f8f6183e143 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -235,6 +235,7 @@ set(obs_SOURCES slider-ignorewheel.cpp combobox-ignorewheel.cpp spinbox-ignorewheel.cpp + record-button.cpp volume-control.cpp adv-audio-control.cpp item-widget-helpers.cpp @@ -289,6 +290,7 @@ set(obs_HEADERS focus-list.hpp menu-button.hpp mute-checkbox.hpp + record-button.hpp volume-control.hpp adv-audio-control.hpp item-widget-helpers.hpp diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index f57f3700d13dddf72d11ce40250669791dffbc1b..0d928ac76de39d1b23aa97a45ddee86bbfd3ebb1 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -21,6 +21,7 @@ void EnumSceneCollections(function &&cb); extern volatile bool streaming_active; extern volatile bool recording_active; +extern volatile bool recording_paused; extern volatile bool replaybuf_active; /* ------------------------------------------------------------------------- */ @@ -265,6 +266,17 @@ struct OBSStudioAPI : obs_frontend_callbacks { return os_atomic_load_bool(&recording_active); } + void obs_frontend_recording_pause(bool pause) override + { + QMetaObject::invokeMethod(main, pause ? "PauseRecording" + : "UnpauseRecording"); + } + + bool obs_frontend_recording_paused(void) override + { + return os_atomic_load_bool(&recording_paused); + } + void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 2400d1fb0e7dd02cdf5fb9a13a93fd4611fb4129..ec9ef1924e11ac90ce5d51da374900966f880134 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -281,6 +281,9 @@ Output.StartRecordingFailed="Failed to start recording" Output.StartReplayFailed="Failed to start replay buffer" Output.StartFailedGeneric="Starting the output failed. Please check the log for details.\n\nNote: If you are using the NVENC or AMD encoders, make sure your video drivers are up to date." +# replay buffer + pause warning message +Output.ReplayBuffer.PauseWarning.Title="Cannot save replays while paused" +Output.ReplayBuffer.PauseWarning.Text="Warning: Replays cannot be saved while recording is paused." # output connect messages Output.ConnectFail.Title="Failed to connect" @@ -501,6 +504,8 @@ Basic.Main.StartRecording="Start Recording" Basic.Main.StartReplayBuffer="Start Replay Buffer" Basic.Main.StartStreaming="Start Streaming" Basic.Main.StopRecording="Stop Recording" +Basic.Main.PauseRecording="Pause Recording" +Basic.Main.UnpauseRecording="Unpause Recording" Basic.Main.StoppingRecording="Stopping Recording..." Basic.Main.StopReplayBuffer="Stop Replay Buffer" Basic.Main.StoppingReplayBuffer="Stopping Replay Buffer..." @@ -678,6 +683,7 @@ Basic.Settings.Output.Simple.RecordingQuality.HQ="Indistinguishable Quality, Lar Basic.Settings.Output.Simple.RecordingQuality.Lossless="Lossless Quality, Tremendously Large File Size" Basic.Settings.Output.Simple.Warn.VideoBitrate="Warning: The streaming video bitrate will be set to %1, which is the upper limit for the current streaming service. If you're sure you want to go above %1, enable advanced encoder options and uncheck \"Enforce streaming service bitrate limits\"." Basic.Settings.Output.Simple.Warn.AudioBitrate="Warning: The streaming audio bitrate will be set to %1, which is the upper limit for the current streaming service. If you're sure you want to go above %1, enable advanced encoder options and uncheck \"Enforce streaming service bitrate limits\"." +Basic.Settings.Output.Simple.Warn.CannotPause="Warning: Recordings cannot be paused if the recording quality is set to \"Same as stream\"." Basic.Settings.Output.Simple.Warn.Encoder="Warning: Recording with a software encoder at a different quality than the stream will require extra CPU usage if you stream and record at the same time." Basic.Settings.Output.Simple.Warn.Lossless="Warning: Lossless quality generates tremendously large file sizes! Lossless quality can use upward of 7 gigabytes of disk space per minute at high resolutions and framerates. Lossless is not recommended for long recordings unless you have a very large amount of disk space available." Basic.Settings.Output.Simple.Warn.Lossless.Msg="Are you sure you want to use lossless quality?" @@ -909,6 +915,7 @@ SceneItemHide="Hide '%1'" OutputWarnings.NoTracksSelected="You must select at least one track" OutputWarnings.MultiTrackRecording="Warning: Certain formats (such as FLV) do not support multiple tracks per recording" OutputWarnings.MP4Recording="Warning: Recordings saved to MP4/MOV will be unrecoverable if the file cannot be finalized (e.g. as a result of BSODs, power losses, etc.). If you want to record multiple audio tracks consider using MKV and remux the recording to MP4/MOV after it is finished (File → Remux Recordings)" +OutputWarnings.CannotPause="Warning: Recordings cannot be paused if the recording encoder is set to \"(Use stream encoder)\"" # deleting final scene FinalScene.Title="Delete Scene" diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index ae2ef8c9a920c44e3c2b898f7368c8e7d7141218..e98cd8b3daf470adbac1a5d7b5de7017478b5516 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -353,6 +353,10 @@ QToolButton:pressed { qproperty-icon: url(./Dark/down.svg); } +* [themeID="pauseIconSmall"] { + qproperty-icon: url(./Dark/media-pause.svg); +} + /* Tab Widget */ QTabWidget::pane { /* The tab widget frame */ diff --git a/UI/data/themes/Dark.qss b/UI/data/themes/Dark.qss index 693a5561fb306de1e7729af38e796b287d6bc41a..9a7c65e743198d93860e52817bb460cbd3f67431 100644 --- a/UI/data/themes/Dark.qss +++ b/UI/data/themes/Dark.qss @@ -253,6 +253,10 @@ QToolButton:pressed { qproperty-icon: url(./Dark/down.svg); } +* [themeID="pauseIconSmall"] { + qproperty-icon: url(./Dark/media-pause.svg); +} + /* Tab Widget */ @@ -577,6 +581,19 @@ OBSHotkeyLabel[hotkeyPairHover=true] { color: red; } +/* Pause */ +PauseCheckBox { + outline: none; +} + +PauseCheckBox::indicator:checked { + image: url(:/res/images/media-pause.svg); +} + +PauseCheckBox::indicator:unchecked { + image: url(:/res/images/media-play.svg); +} + /* Group Collapse Checkbox */ SourceTreeSubItemCheckBox { diff --git a/UI/data/themes/Dark/media-pause.svg b/UI/data/themes/Dark/media-pause.svg new file mode 100644 index 0000000000000000000000000000000000000000..6050b94722f53c79a7299fbab09a52ed69dbbfe6 --- /dev/null +++ b/UI/data/themes/Dark/media-pause.svg @@ -0,0 +1,3 @@ + + + diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 88bde15e7dfef0afae14696959f3cb0b8a5bbae4..1e6a697f2c3c86cc1fc5f047035fe99775961909 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -507,6 +507,10 @@ QToolButton:pressed { qproperty-icon: url(./Dark/down.svg); } +* [themeID="pauseIconSmall"] { + qproperty-icon: url(./Dark/media-pause.svg); +} + /***********************/ /* --- Combo boxes --- */ /***********************/ diff --git a/UI/data/themes/System.qss b/UI/data/themes/System.qss index c68e0c462daf1c18929c653dfc08fd2282df87d0..0eb01cf1993888c7cbb7bf734d5b006d1eedf4b8 100644 --- a/UI/data/themes/System.qss +++ b/UI/data/themes/System.qss @@ -39,6 +39,10 @@ qproperty-icon: url(:/res/images/down.svg); } +* [themeID="pauseIconSmall"] { + qproperty-icon: url(:/res/images/media-pause.svg); +} + MuteCheckBox { outline: none; } diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index b082729f87f82aecb0b41f357c67d61f96e8e34c..3b72c629589bf76399088e2c77e820b7945ccf56 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -116,7 +116,7 @@ 0 0 1079 - 22 + 21 @@ -656,7 +656,7 @@ 0 0 - 64 + 80 16 @@ -843,7 +843,7 @@ - + :/res/images/add.png:/res/images/add.png @@ -878,7 +878,7 @@ - + :/res/images/list_remove.png:/res/images/list_remove.png @@ -913,7 +913,7 @@ - + :/res/images/configuration21_16.png:/res/images/configuration21_16.png @@ -1000,7 +1000,7 @@ 8 - + 2 @@ -1037,29 +1037,48 @@ - - - true + + + 2 - - - 0 - 0 - + + 0 - - - 130 - 0 - + + 0 - - Basic.Main.StartRecording + + 0 - - true + + 0 - + + + + true + + + + 0 + 0 + + + + + 130 + 0 + + + + Basic.Main.StartRecording + + + true + + + + @@ -1115,7 +1134,7 @@ - + :/res/images/add.png:/res/images/add.png @@ -1127,7 +1146,7 @@ - + :/res/images/add.png:/res/images/add.png @@ -1139,7 +1158,7 @@ - + :/res/images/list_remove.png:/res/images/list_remove.png @@ -1157,7 +1176,7 @@ - + :/res/images/list_remove.png:/res/images/list_remove.png @@ -1178,7 +1197,7 @@ true - + :/res/images/properties.png:/res/images/properties.png @@ -1190,7 +1209,7 @@ - + :/res/images/up.png:/res/images/up.png @@ -1205,7 +1224,7 @@ true - + :/res/images/up.png:/res/images/up.png @@ -1217,7 +1236,7 @@ - + :/res/images/down.png:/res/images/down.png @@ -1232,7 +1251,7 @@ true - + :/res/images/down.png:/res/images/down.png @@ -1733,6 +1752,11 @@
window-dock.hpp
1 + + RecordButton + QPushButton +
record-button.hpp
+
diff --git a/UI/forms/images/media-pause.svg b/UI/forms/images/media-pause.svg new file mode 100644 index 0000000000000000000000000000000000000000..8e45ee21f139806d88c9bfa15884174018c19fd7 --- /dev/null +++ b/UI/forms/images/media-pause.svg @@ -0,0 +1,3 @@ + + + diff --git a/UI/forms/obs.qrc b/UI/forms/obs.qrc index 7c647c7b7c615b1aabbbba6e38b5ee1b1c1ffd62..87e11e359ac08bb32fda54bd5f1754a40dc23f49 100644 --- a/UI/forms/obs.qrc +++ b/UI/forms/obs.qrc @@ -1,5 +1,6 @@ + images/media-pause.svg images/mute.svg images/refresh.svg images/no_sources.svg diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp index 0181a87ab42f38a74053b1795cc09250f4c5e874..c5fafeb0c366b3eb238b17ae8ecfb1a5bb9879b9 100644 --- a/UI/obs-frontend-api/obs-frontend-api.cpp +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -227,6 +227,17 @@ bool obs_frontend_recording_active(void) return !!callbacks_valid() ? c->obs_frontend_recording_active() : false; } +void obs_frontend_recording_pause(bool pause) +{ + if (!!callbacks_valid()) + c->obs_frontend_recording_pause(pause); +} + +bool obs_frontend_recording_paused(void) +{ + return !!callbacks_valid() ? c->obs_frontend_recording_paused() : false; +} + void obs_frontend_replay_buffer_start(void) { if (callbacks_valid()) diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h index fb0d8b686fce1fc4538c06270acbda112e9ff4f5..68fa917fd99e1216d9deb60e712ea7f4588578b5 100644 --- a/UI/obs-frontend-api/obs-frontend-api.h +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -44,6 +44,9 @@ enum obs_frontend_event { OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP, OBS_FRONTEND_EVENT_FINISHED_LOADING, + + OBS_FRONTEND_EVENT_RECORDING_PAUSED, + OBS_FRONTEND_EVENT_RECORDING_UNPAUSED, }; /* ------------------------------------------------------------------------- */ @@ -152,6 +155,8 @@ EXPORT bool obs_frontend_streaming_active(void); EXPORT void obs_frontend_recording_start(void); EXPORT void obs_frontend_recording_stop(void); EXPORT bool obs_frontend_recording_active(void); +EXPORT void obs_frontend_recording_pause(bool pause); +EXPORT bool obs_frontend_recording_paused(void); EXPORT void obs_frontend_replay_buffer_start(void); EXPORT void obs_frontend_replay_buffer_save(void); diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp index 617b842fcd51efb68e06470641e6f34abd57092c..80051ca79ca7f7ae2e089b437bed07f17856ac86 100644 --- a/UI/obs-frontend-api/obs-frontend-internal.hpp +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -43,6 +43,8 @@ struct obs_frontend_callbacks { virtual void obs_frontend_recording_start(void) = 0; virtual void obs_frontend_recording_stop(void) = 0; virtual bool obs_frontend_recording_active(void) = 0; + virtual void obs_frontend_recording_pause(bool pause) = 0; + virtual bool obs_frontend_recording_paused(void) = 0; virtual void obs_frontend_replay_buffer_start(void) = 0; virtual void obs_frontend_replay_buffer_save(void) = 0; diff --git a/UI/record-button.cpp b/UI/record-button.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8b82c2b0fd82c88f612b0b83297958f46c184aee --- /dev/null +++ b/UI/record-button.cpp @@ -0,0 +1,18 @@ +#include "record-button.hpp" +#include "window-basic-main.hpp" + +void RecordButton::resizeEvent(QResizeEvent *event) +{ + OBSBasic *main = OBSBasic::Get(); + if (!main->pause) + return; + + QSize newSize = event->size(); + QSize pauseSize = main->pause->size(); + int height = main->ui->recordButton->size().height(); + + if (pauseSize.height() != height || pauseSize.width() != height) { + main->pause->setMinimumSize(height, height); + main->pause->setMaximumSize(height, height); + } +} diff --git a/UI/record-button.hpp b/UI/record-button.hpp new file mode 100644 index 0000000000000000000000000000000000000000..c1782aafa26a2319e5e39e6e9bf14fe64dba5d7a --- /dev/null +++ b/UI/record-button.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +class RecordButton : public QPushButton { + Q_OBJECT + +public: + inline RecordButton(QWidget *parent = nullptr) : QPushButton(parent) {} + + virtual void resizeEvent(QResizeEvent *event) override; +}; diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index f15bca4ef626e8cfe8b26aee7290957bb92f5730..93cd37c7c5ce6780b652298f767db3d8df153bf7 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -12,6 +12,7 @@ extern bool EncoderAvailable(const char *encoder); volatile bool streaming_active = false; volatile bool recording_active = false; +volatile bool recording_paused = false; volatile bool replaybuf_active = false; static void OBSStreamStarting(void *data, calldata_t *params) @@ -88,6 +89,7 @@ static void OBSStopRecording(void *data, calldata_t *params) output->recordingActive = false; os_atomic_set_bool(&recording_active, false); + os_atomic_set_bool(&recording_paused, false); QMetaObject::invokeMethod(output->main, "RecordingStop", Q_ARG(int, code), Q_ARG(QString, arg_last_error)); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index dabc30a537f0ea7bcb6f1d0ceb5238e9a96aa4ba..f482e33a44879c67640799dfd29c8912e4d56d70 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2112,6 +2112,17 @@ void OBSBasic::CreateHotkeys() LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording", "OBSBasic.StopRecording"); + pauseHotkeys = obs_hotkey_pair_register_frontend( + "OBSBasic.PauseRecording", Str("Basic.Main.PauseRecording"), + "OBSBasic.UnpauseRecording", Str("Basic.Main.UnpauseRecording"), + MAKE_CALLBACK(basic.pause && !basic.pause->isChecked(), + basic.PauseRecording, "Pausing recording"), + MAKE_CALLBACK(basic.pause && basic.pause->isChecked(), + basic.UnpauseRecording, "Unpausing recording"), + this, this); + LoadHotkeyPair(pauseHotkeys, "OBSBasic.PauseRecording", + "OBSBasic.UnpauseRecording"); + replayBufHotkeys = obs_hotkey_pair_register_frontend( "OBSBasic.StartReplayBuffer", Str("Basic.Main.StartReplayBuffer"), @@ -2169,6 +2180,7 @@ void OBSBasic::ClearHotkeys() { obs_hotkey_pair_unregister(streamingHotkeys); obs_hotkey_pair_unregister(recordingHotkeys); + obs_hotkey_pair_unregister(pauseHotkeys); obs_hotkey_pair_unregister(replayBufHotkeys); obs_hotkey_pair_unregister(togglePreviewHotkeys); obs_hotkey_unregister(forceStreamingStopHotkey); @@ -5319,6 +5331,7 @@ void OBSBasic::RecordingStart() api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED); OnActivate(); + UpdatePause(); blog(LOG_INFO, RECORDING_START); } @@ -5385,11 +5398,46 @@ void OBSBasic::RecordingStop(int code, QString last_error) AutoRemux(); OnDeactivate(); + UpdatePause(false); } #define RP_NO_HOTKEY_TITLE QTStr("Output.ReplayBuffer.NoHotkey.Title") #define RP_NO_HOTKEY_TEXT QTStr("Output.ReplayBuffer.NoHotkey.Msg") +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; + +void OBSBasic::ShowReplayBufferPauseWarning() +{ + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle(QTStr("Output.ReplayBuffer." + "PauseWarning.Title")); + msgbox.setText(QTStr("Output.ReplayBuffer." + "PauseWarning.Text")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GlobalConfig(), "General", + "WarnedAboutReplayBufferPausing", true); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GlobalConfig(), "General", + "WarnedAboutReplayBufferPausing"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, + Q_ARG(VoidFunc, msgBox)); + } +} + void OBSBasic::StartReplayBuffer() { if (!outputHandler || !outputHandler->replayBuffer) @@ -5423,8 +5471,12 @@ void OBSBasic::StartReplayBuffer() api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING); SaveProject(); - if (!outputHandler->StartReplayBuffer()) + + if (!outputHandler->StartReplayBuffer()) { replayBufferButton->setChecked(false); + } else if (os_atomic_load_bool(&recording_paused)) { + ShowReplayBufferPauseWarning(); + } } void OBSBasic::ReplayBufferStopping() @@ -7295,3 +7347,106 @@ void OBSBasic::UpdatePatronJson(const QString &text, const QString &error) patronJson = QT_TO_UTF8(text); } + +void OBSBasic::PauseRecording() +{ + if (!pause || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, true)) { + pause->setChecked(true); + os_atomic_set_bool(&recording_paused, true); + + if (api) + api->on_event(OBS_FRONTEND_EVENT_RECORDING_PAUSED); + + if (os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } +} + +void OBSBasic::UnpauseRecording() +{ + if (!pause || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + + if (obs_output_pause(output, false)) { + pause->setChecked(false); + os_atomic_set_bool(&recording_paused, false); + + if (api) + api->on_event(OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + } +} + +void OBSBasic::PauseToggled() +{ + if (!pause || !outputHandler || !outputHandler->fileOutput) + return; + + obs_output_t *output = outputHandler->fileOutput; + bool enable = !obs_output_paused(output); + + if (obs_output_pause(output, enable)) { + os_atomic_set_bool(&recording_paused, enable); + + if (api) + api->on_event( + enable ? OBS_FRONTEND_EVENT_RECORDING_PAUSED + : OBS_FRONTEND_EVENT_RECORDING_UNPAUSED); + + if (enable && os_atomic_load_bool(&replaybuf_active)) + ShowReplayBufferPauseWarning(); + } else { + pause->setChecked(!enable); + } +} + +void OBSBasic::UpdatePause(bool activate) +{ + if (!activate || !outputHandler || !outputHandler->RecordingActive()) { + pause.reset(); + return; + } + + const char *mode = config_get_string(basicConfig, "Output", "Mode"); + bool adv = astrcmpi(mode, "Advanced") == 0; + bool shared; + + if (adv) { + const char *recType = + config_get_string(basicConfig, "AdvOut", "RecType"); + + if (astrcmpi(recType, "FFmpeg") == 0) { + shared = config_get_bool(basicConfig, "AdvOut", + "FFOutputToFile"); + } else { + const char *recordEncoder = config_get_string( + basicConfig, "AdvOut", "RecEncoder"); + shared = astrcmpi(recordEncoder, "none") == 0; + } + } else { + const char *quality = config_get_string( + basicConfig, "SimpleOutput", "RecQuality"); + shared = strcmp(quality, "Stream") == 0; + } + + if (!shared) { + pause.reset(new QPushButton()); + pause->setAccessibleName(QTStr("Basic.Main.PauseRecording")); + pause->setToolTip(QTStr("Basic.Main.PauseRecording")); + pause->setCheckable(true); + pause->setChecked(false); + pause->setProperty("themeID", + QVariant(QStringLiteral("pauseIconSmall"))); + connect(pause.data(), &QAbstractButton::clicked, this, + &OBSBasic::PauseToggled); + ui->recordingLayout->addWidget(pause.data()); + } else { + pause.reset(); + } +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 62d65c7ddf5056494e1fd1a606c275169b2def9e..287b4957ad28d60809e83849234709d06877e81a 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -123,6 +123,7 @@ class OBSBasic : public OBSMainWindow { friend class Auth; friend class AutoConfig; friend class AutoConfigStreamPage; + friend class RecordButton; friend struct OBSStudioAPI; enum class MoveDir { Up, Down, Left, Right }; @@ -204,6 +205,7 @@ private: QPointer transitionButton; QPointer replayBufferButton; + QScopedPointer pause; QScopedPointer trayIcon; QPointer sysTrayStream; @@ -323,8 +325,8 @@ private: int GetTopSelectedSourceItem(); - obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, replayBufHotkeys, - togglePreviewHotkeys; + obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys, + replayBufHotkeys, togglePreviewHotkeys; obs_hotkey_id forceStreamingStopHotkey; void InitDefaultTransitions(); @@ -440,6 +442,7 @@ public slots: void RecordStopping(); void RecordingStop(int code, QString last_error); + void ShowReplayBufferPauseWarning(); void StartReplayBuffer(); void StopReplayBuffer(); @@ -465,6 +468,9 @@ public slots: void UpdatePatronJson(const QString &text, const QString &error); + void PauseRecording(); + void UnpauseRecording(); + private slots: void AddSceneItem(OBSSceneItem item); void AddScene(OBSSource source); @@ -557,6 +563,7 @@ private: static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed); void AutoRemux(); + void UpdatePause(bool activate = true); public: OBSSource GetProgramSource(); @@ -760,6 +767,8 @@ private slots: void on_resetUI_triggered(); void on_lockUI_toggled(bool lock); + void PauseToggled(); + void logUploadFinished(const QString &text, const QString &error); void updateCheckFinished(); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 2785b13810ee00abaff36a950e1385883b778583..95f8cfa25b05e7fe3e8f341a030efc4214d2558f 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -729,6 +729,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) SLOT(AdvOutRecCheckWarnings())); connect(ui->advOutRecFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(AdvOutRecCheckWarnings())); + connect(ui->advOutRecEncoder, SIGNAL(currentIndexChanged(int)), this, + SLOT(AdvOutRecCheckWarnings())); AdvOutRecCheckWarnings(); ui->buttonBox->button(QDialogButtonBox::Apply)->setIcon(QIcon()); @@ -3929,6 +3931,13 @@ void OBSBasicSettings::AdvOutRecCheckWarnings() warningMsg = QTStr("OutputWarnings.MultiTrackRecording"); } + bool useStreamEncoder = ui->advOutRecEncoder->currentIndex() == 0; + if (useStreamEncoder) { + if (!warningMsg.isEmpty()) + warningMsg += "\n\n"; + warningMsg += QTStr("OutputWarnings.CannotPause"); + } + if (ui->advOutRecFormat->currentText().compare("mp4") == 0 || ui->advOutRecFormat->currentText().compare("mov") == 0) { if (!warningMsg.isEmpty()) @@ -4387,6 +4396,10 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() warning += "\n\n"; warning += SIMPLE_OUTPUT_WARNING("Encoder"); } + } else { + if (!warning.isEmpty()) + warning += "\n\n"; + warning += SIMPLE_OUTPUT_WARNING("CannotPause"); } if (qual != "Lossless" && diff --git a/UI/window-basic-status-bar.cpp b/UI/window-basic-status-bar.cpp index b94a8c0bb161fa7c70561d8844194dff517634ce..74dde3a3cd1ea46683d3dd5c848ddb27dda097d2 100644 --- a/UI/window-basic-status-bar.cpp +++ b/UI/window-basic-status-bar.cpp @@ -241,17 +241,28 @@ void OBSBasicStatusBar::UpdateStreamTime() } } +extern volatile bool recording_paused; + void OBSBasicStatusBar::UpdateRecordTime() { - totalRecordSeconds++; + bool paused = os_atomic_load_bool(&recording_paused); - int seconds = totalRecordSeconds % 60; - int totalMinutes = totalRecordSeconds / 60; - int minutes = totalMinutes % 60; - int hours = totalMinutes / 60; + if (!paused) + totalRecordSeconds++; QString text; - text.sprintf("REC: %02d:%02d:%02d", hours, minutes, seconds); + + if (paused) { + text = QStringLiteral("REC: PAUSED"); + } else { + int seconds = totalRecordSeconds % 60; + int totalMinutes = totalRecordSeconds / 60; + int minutes = totalMinutes % 60; + int hours = totalMinutes / 60; + + text.sprintf("REC: %02d:%02d:%02d", hours, minutes, seconds); + } + recordTime->setText(text); recordTime->setMinimumWidth(recordTime->width()); } diff --git a/docs/sphinx/reference-frontend-api.rst b/docs/sphinx/reference-frontend-api.rst index 3b5b86db6f7b85695af9204f8269c6aa27f96a2f..46df5bd63ba0a164d558eec597bec2b03b98e935 100644 --- a/docs/sphinx/reference-frontend-api.rst +++ b/docs/sphinx/reference-frontend-api.rst @@ -124,6 +124,18 @@ Structures/Enumerations the program is either about to load a new scene collection, or the program is about to exit. + - **OBS_FRONTEND_FINISHED_LOADING** + + Triggered when the program has finished loading. + + - **OBS_FRONTEND_EVENT_RECORDING_PAUSED** + + Triggered when the recording has been paused. + + - **OBS_FRONTEND_EVENT_RECORDING_UNPAUSED** + + Triggered when the recording has been unpaused. + .. type:: struct obs_frontend_source_list - DARRAY(obs_source_t*) **sources** @@ -402,6 +414,18 @@ Functions --------------------------------------- +.. function:: void obs_frontend_recording_pause(bool pause) + + :pause: *true* to pause recording, *false* to unpause. + +--------------------------------------- + +.. function:: bool obs_frontend_recording_paused(void) + + :return: *true* if recording paused, *false* otherwise. + +--------------------------------------- + .. function:: void obs_frontend_replay_buffer_start(void) Starts replay buffer. diff --git a/docs/sphinx/reference-outputs.rst b/docs/sphinx/reference-outputs.rst index e7c65b6cd1e3a8601af8b5b585b87917a7676781..b0d397d0556444b0516e36d4010a35fd6dc46935 100644 --- a/docs/sphinx/reference-outputs.rst +++ b/docs/sphinx/reference-outputs.rst @@ -66,6 +66,14 @@ Output Definition Structure (obs_output_info) When this capability flag is used, specifies that this output supports multiple encoded audio tracks simultaneously. + - **OBS_OUTPUT_CAN_PAUSE** - Output supports pausing. + + When this capability flag is used, the output supports pausing. + When an output is paused, raw or encoded audio/video data will be + halted when paused down to the exact point to the closest video + frame. Audio data will be correctly truncated down to the exact + audio sample according to that video frame timing. + .. member:: const char *(*obs_output_info.get_name)(void *type_data) Get the translated name of the output type. @@ -170,13 +178,9 @@ Output Definition Structure (obs_output_info) :return: The properties of the output -.. member:: void (*obs_output_info.pause)(void *data) - - Pauses the output (if the output supports pausing). +.. member:: void (*obs_output_info.unused1)(void *data) - (Author's note: This is currently unimplemented) - - (Optional) + This callback is no longer used. .. member:: uint64_t (*obs_output_info.get_total_bytes)(void *data) @@ -257,6 +261,14 @@ Output Signals | OBS_OUTPUT_NO_SPACE - Ran out of disk space | OBS_OUTPUT_ENCODE_ERROR - Encoder error +**pause** (ptr output) + + Called when the output has been paused. + +**unpause** (ptr output) + + Called when the output has been unpaused. + **starting** (ptr output) Called when the output is starting. @@ -444,11 +456,18 @@ General Output Functions --------------------- -.. function:: void obs_output_pause(obs_output_t *output) +.. function:: bool obs_output_pause(obs_output_t *output, bool pause) Pause an output (if supported by the output). - (Author's Note: Not yet implemented) + :return: *true* if the output was paused successfuly, *false* + otherwise + +--------------------- + +.. function:: bool obs_output_paused(const obs_output_t *output) + + :return: *true* if the output is paused, *false* otherwise --------------------- @@ -808,6 +827,14 @@ Functions used by outputs | OBS_OUTPUT_UNSUPPORTED - The settings, video/audio format, or codecs are unsupported by this output | OBS_OUTPUT_NO_SPACE - Ran out of disk space +--------------------- + +.. function:: uint64_t obs_output_get_pause_offset(obs_output_t *output) + + Returns the current pause offset of the output. Used with raw + outputs to calculate system timestamps when using calculated + timestamps (see FFmpeg output for an example). + .. --------------------------------------------------------------------------- .. _libobs/obs-output.h: https://github.com/jp9000/obs-studio/blob/master/libobs/obs-output.h diff --git a/libobs/obs-encoder.c b/libobs/obs-encoder.c index 404c76a455f86158a03f7cc2747eeb04ec276aaa..a0237172809ef107182174d9f8d5b7eb1f71cb3d 100644 --- a/libobs/obs-encoder.c +++ b/libobs/obs-encoder.c @@ -48,6 +48,7 @@ static bool init_encoder(struct obs_encoder *encoder, const char *name, pthread_mutex_init_value(&encoder->init_mutex); pthread_mutex_init_value(&encoder->callbacks_mutex); pthread_mutex_init_value(&encoder->outputs_mutex); + pthread_mutex_init_value(&encoder->pause.mutex); if (pthread_mutexattr_init(&attr) != 0) return false; @@ -62,6 +63,8 @@ static bool init_encoder(struct obs_encoder *encoder, const char *name, return false; if (pthread_mutex_init(&encoder->outputs_mutex, NULL) != 0) return false; + if (pthread_mutex_init(&encoder->pause.mutex, NULL) != 0) + return false; if (encoder->orig_info.get_defaults) encoder->orig_info.get_defaults(encoder->context.settings); @@ -264,6 +267,7 @@ static void obs_encoder_actually_destroy(obs_encoder_t *encoder) pthread_mutex_destroy(&encoder->init_mutex); pthread_mutex_destroy(&encoder->callbacks_mutex); pthread_mutex_destroy(&encoder->outputs_mutex); + pthread_mutex_destroy(&encoder->pause.mutex); obs_context_data_free(&encoder->context); if (encoder->owns_info_id) bfree((void *)encoder->info.id); @@ -529,6 +533,16 @@ get_callback_idx(const struct obs_encoder *encoder, return DARRAY_INVALID; } +void pause_reset(struct pause_data *pause) +{ + pthread_mutex_lock(&pause->mutex); + pause->last_video_ts = 0; + pause->ts_start = 0; + pause->ts_end = 0; + pause->ts_offset = 0; + pthread_mutex_unlock(&pause->mutex); +} + static inline void obs_encoder_start_internal( obs_encoder_t *encoder, void (*new_packet)(void *param, struct encoder_packet *packet), @@ -551,6 +565,9 @@ static inline void obs_encoder_start_internal( pthread_mutex_unlock(&encoder->callbacks_mutex); if (first) { + os_atomic_set_bool(&encoder->paused, false); + pause_reset(&encoder->pause); + encoder->cur_pts = 0; add_connection(encoder); } @@ -906,6 +923,10 @@ void send_off_encoder_packet(obs_encoder_t *encoder, bool success, packet_dts_usec(pkt) - encoder->offset_usec; pkt->sys_dts_usec = pkt->dts_usec; + pthread_mutex_lock(&encoder->pause.mutex); + pkt->sys_dts_usec += encoder->pause.ts_offset / 1000; + pthread_mutex_unlock(&encoder->pause.mutex); + pthread_mutex_lock(&encoder->callbacks_mutex); for (size_t i = encoder->callbacks.num; i > 0; i--) { @@ -946,6 +967,39 @@ bool do_encode(struct obs_encoder *encoder, struct encoder_frame *frame) return success; } +static inline bool video_pause_check_internal(struct pause_data *pause, + uint64_t ts) +{ + pause->last_video_ts = ts; + if (!pause->ts_start) { + return false; + } + + if (ts == pause->ts_start) { + return true; + + } else if (ts == pause->ts_end) { + pause->ts_start = 0; + pause->ts_end = 0; + } else { + + return true; + } + + return false; +} + +bool video_pause_check(struct pause_data *pause, uint64_t timestamp) +{ + bool ignore_frame; + + pthread_mutex_lock(&pause->mutex); + ignore_frame = video_pause_check_internal(pause, timestamp); + pthread_mutex_unlock(&pause->mutex); + + return ignore_frame; +} + static const char *receive_video_name = "receive_video"; static void receive_video(void *param, struct video_data *frame) { @@ -962,6 +1016,9 @@ static void receive_video(void *param, struct video_data *frame) } } + if (video_pause_check(&encoder->pause, frame->timestamp)) + goto wait_for_audio; + memset(&enc_frame, 0, sizeof(struct encoder_frame)); for (size_t i = 0; i < MAX_AV_PLANES; i++) { @@ -1110,20 +1167,90 @@ static bool send_audio_data(struct obs_encoder *encoder) return true; } +static void pause_audio(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + uint64_t cutoff_frames = pause->ts_start - data->timestamp; + cutoff_frames = ns_to_audio_frames(sample_rate, cutoff_frames); + + data->frames = (uint32_t)cutoff_frames; +} + +static void unpause_audio(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + uint64_t cutoff_frames = pause->ts_end - data->timestamp; + cutoff_frames = ns_to_audio_frames(sample_rate, cutoff_frames); + + data->timestamp = pause->ts_start; + data->frames = data->frames - (uint32_t)cutoff_frames; + pause->ts_start = 0; + pause->ts_end = 0; +} + +static inline bool audio_pause_check_internal(struct pause_data *pause, + struct audio_data *data, + size_t sample_rate) +{ + uint64_t end_ts; + + if (!pause->ts_start) { + return false; + } + + end_ts = + data->timestamp + audio_frames_to_ns(sample_rate, data->frames); + + if (pause->ts_start >= data->timestamp) { + if (pause->ts_start <= end_ts) { + pause_audio(pause, data, sample_rate); + return !data->frames; + } + + } else { + if (pause->ts_end >= data->timestamp && + pause->ts_end <= end_ts) { + unpause_audio(pause, data, sample_rate); + return !data->frames; + } + + return true; + } + + return false; +} + +bool audio_pause_check(struct pause_data *pause, struct audio_data *data, + size_t sample_rate) +{ + bool ignore_audio; + + pthread_mutex_lock(&pause->mutex); + ignore_audio = audio_pause_check_internal(pause, data, sample_rate); + data->timestamp -= pause->ts_offset; + pthread_mutex_unlock(&pause->mutex); + + return ignore_audio; +} + static const char *receive_audio_name = "receive_audio"; -static void receive_audio(void *param, size_t mix_idx, struct audio_data *data) +static void receive_audio(void *param, size_t mix_idx, struct audio_data *in) { profile_start(receive_audio_name); struct obs_encoder *encoder = param; + struct audio_data audio = *in; if (!encoder->first_received) { - encoder->first_raw_ts = data->timestamp; + encoder->first_raw_ts = audio.timestamp; encoder->first_received = true; clear_audio(encoder); } - if (!buffer_audio(encoder, data)) + if (audio_pause_check(&encoder->pause, &audio, encoder->samplerate)) + goto end; + + if (!buffer_audio(encoder, &audio)) goto end; while (encoder->audio_input_buffer[0].size >= @@ -1331,3 +1458,10 @@ uint32_t obs_encoder_get_caps(const obs_encoder_t *encoder) ? encoder->orig_info.caps : 0; } + +bool obs_encoder_paused(const obs_encoder_t *encoder) +{ + return obs_encoder_valid(encoder, "obs_encoder_paused") + ? os_atomic_load_bool(&encoder->paused) + : false; +} diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 4b8b2ae2ec2500bd955dbd73b78e8ba008c4d4f7..06377c25f614d4aa8c2664273788a85a721f8fa7 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -840,6 +840,19 @@ struct caption_text { struct caption_text *next; }; +struct pause_data { + pthread_mutex_t mutex; + uint64_t last_video_ts; + uint64_t ts_start; + uint64_t ts_end; + uint64_t ts_offset; +}; + +extern bool video_pause_check(struct pause_data *pause, uint64_t timestamp); +extern bool audio_pause_check(struct pause_data *pause, struct audio_data *data, + size_t sample_rate); +extern void pause_reset(struct pause_data *pause); + struct obs_output { struct obs_context_data context; struct obs_output_info info; @@ -878,6 +891,7 @@ struct obs_output { int total_frames; volatile bool active; + volatile bool paused; video_t *video; audio_t *audio; obs_encoder_t *video_encoder; @@ -885,6 +899,8 @@ struct obs_output { obs_service_t *service; size_t mixer_mask; + struct pause_data pause; + struct circlebuf audio_buffer[MAX_AUDIO_MIXES][MAX_AV_PLANES]; uint64_t audio_start_ts; uint64_t video_start_ts; @@ -988,6 +1004,7 @@ struct obs_encoder { enum video_format preferred_format; volatile bool active; + volatile bool paused; bool initialized; /* indicates ownership of the info.id buffer */ @@ -1023,6 +1040,8 @@ struct obs_encoder { pthread_mutex_t callbacks_mutex; DARRAY(struct encoder_callback) callbacks; + struct pause_data pause; + const char *profile_encoder_encode_name; }; diff --git a/libobs/obs-output.c b/libobs/obs-output.c index 6446bec19c83f1f9a4e071aef27dcf2cddda72c6..03f11bfe1c47268705aa01e93aa552d15b4e51db 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -74,6 +74,8 @@ const char *obs_output_get_display_name(const char *id) static const char *output_signals[] = { "void start(ptr output)", "void stop(ptr output, int code)", + "void pause(ptr output)", + "void unpause(ptr output)", "void starting(ptr output)", "void stopping(ptr output)", "void activate(ptr output)", @@ -105,6 +107,7 @@ obs_output_t *obs_output_create(const char *id, const char *name, pthread_mutex_init_value(&output->interleaved_mutex); pthread_mutex_init_value(&output->delay_mutex); pthread_mutex_init_value(&output->caption_mutex); + pthread_mutex_init_value(&output->pause.mutex); if (pthread_mutex_init(&output->interleaved_mutex, NULL) != 0) goto fail; @@ -112,6 +115,8 @@ obs_output_t *obs_output_create(const char *id, const char *name, goto fail; if (pthread_mutex_init(&output->caption_mutex, NULL) != 0) goto fail; + if (pthread_mutex_init(&output->pause.mutex, NULL) != 0) + goto fail; if (os_event_init(&output->stopping_event, OS_EVENT_TYPE_MANUAL) != 0) goto fail; if (!init_output_handlers(output, name, settings, hotkey_data)) @@ -214,6 +219,7 @@ void obs_output_destroy(obs_output_t *output) clear_audio_buffers(output); os_event_destroy(output->stopping_event); + pthread_mutex_destroy(&output->pause.mutex); pthread_mutex_destroy(&output->caption_mutex); pthread_mutex_destroy(&output->interleaved_mutex); pthread_mutex_destroy(&output->delay_mutex); @@ -353,6 +359,9 @@ void obs_output_actual_stop(obs_output_t *output, bool force, uint64_t ts) if (stopping(output) && !force) return; + + obs_output_pause(output, false); + os_event_reset(output->stopping_event); was_reconnecting = reconnecting(output) && !delay_active(output); @@ -517,17 +526,148 @@ obs_data_t *obs_output_get_settings(const obs_output_t *output) bool obs_output_can_pause(const obs_output_t *output) { return obs_output_valid(output, "obs_output_can_pause") - ? (output->info.pause != NULL) + ? !!(output->info.flags & OBS_OUTPUT_CAN_PAUSE) : false; } -void obs_output_pause(obs_output_t *output) +static inline void end_pause(struct pause_data *pause, uint64_t ts) +{ + if (!pause->ts_end) { + pause->ts_end = ts; + pause->ts_offset += pause->ts_end - pause->ts_start; + } +} + +static inline uint64_t get_closest_v_ts(struct pause_data *pause) +{ + uint64_t interval = obs->video.video_frame_interval_ns; + uint64_t ts = os_gettime_ns(); + + return pause->last_video_ts + + ((ts - pause->last_video_ts + interval) / interval) * interval; +} + +static bool obs_encoded_output_pause(obs_output_t *output, bool pause) { + obs_encoder_t *venc; + obs_encoder_t *aenc[MAX_AUDIO_MIXES]; + uint64_t closest_v_ts; + + venc = output->video_encoder; + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) + aenc[i] = output->audio_encoders[i]; + + pthread_mutex_lock(&venc->pause.mutex); + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + pthread_mutex_lock(&aenc[i]->pause.mutex); + } + } + + /* ---------------------------- */ + + closest_v_ts = get_closest_v_ts(&venc->pause); + + if (pause) { + os_atomic_set_bool(&venc->paused, true); + venc->pause.ts_start = closest_v_ts; + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + os_atomic_set_bool(&aenc[i]->paused, true); + aenc[i]->pause.ts_start = closest_v_ts; + } + } + } else { + os_atomic_set_bool(&venc->paused, false); + end_pause(&venc->pause, closest_v_ts); + + for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) { + if (aenc[i]) { + os_atomic_set_bool(&aenc[i]->paused, false); + end_pause(&aenc[i]->pause, closest_v_ts); + } + } + } + + /* ---------------------------- */ + + for (size_t i = MAX_AUDIO_MIXES; i > 0; i--) { + if (aenc[i - 1]) { + pthread_mutex_unlock(&aenc[i - 1]->pause.mutex); + } + } + pthread_mutex_unlock(&venc->pause.mutex); + + return true; +} + +static bool obs_raw_output_pause(obs_output_t *output, bool pause) +{ + bool success; + uint64_t closest_v_ts; + + pthread_mutex_lock(&output->pause.mutex); + closest_v_ts = get_closest_v_ts(&output->pause); + if (pause) { + success = !output->pause.ts_start; + if (success) + output->pause.ts_start = closest_v_ts; + } else { + success = !!output->pause.ts_start; + if (success) + end_pause(&output->pause, closest_v_ts); + } + pthread_mutex_unlock(&output->pause.mutex); + + return success; +} + +bool obs_output_pause(obs_output_t *output, bool pause) +{ + bool success; + if (!obs_output_valid(output, "obs_output_pause")) - return; + return false; + if ((output->info.flags & OBS_OUTPUT_CAN_PAUSE) == 0) + return false; + if (!os_atomic_load_bool(&output->active)) + return false; + if (os_atomic_load_bool(&output->paused) == pause) + return true; + + success = ((output->info.flags & OBS_OUTPUT_ENCODED) != 0) + ? obs_encoded_output_pause(output, pause) + : obs_raw_output_pause(output, pause); + if (success) { + os_atomic_set_bool(&output->paused, pause); + do_output_signal(output, pause ? "pause" : "unpause"); + + blog(LOG_INFO, "output %s %spaused", output->context.name, + pause ? "" : "un"); + } + return success; +} + +bool obs_output_paused(const obs_output_t *output) +{ + return obs_output_valid(output, "obs_output_paused") + ? os_atomic_load_bool(&output->paused) + : false; +} + +uint64_t obs_output_get_pause_offset(obs_output_t *output) +{ + uint64_t offset; - if (output->info.pause) - output->info.pause(output->context.data); + if (!obs_output_valid(output, "obs_output_get_pause_offset")) + return 0; + + pthread_mutex_lock(&output->pause.mutex); + offset = output->pause.ts_offset; + pthread_mutex_unlock(&output->pause.mutex); + + return offset; } signal_handler_t *obs_output_get_signal_handler(const obs_output_t *output) @@ -1532,18 +1672,29 @@ static void default_encoded_callback(void *param, struct encoder_packet *packet) static void default_raw_video_callback(void *param, struct video_data *frame) { struct obs_output *output = param; + + if (video_pause_check(&output->pause, frame->timestamp)) + return; + if (data_active(output)) output->info.raw_video(output->context.data, frame); output->total_frames++; - - if (!output->video_start_ts) { - output->video_start_ts = frame->timestamp; - } } static bool prepare_audio(struct obs_output *output, const struct audio_data *old, struct audio_data *new) { + if (!output->video_start_ts) { + pthread_mutex_lock(&output->pause.mutex); + output->video_start_ts = output->pause.last_video_ts; + pthread_mutex_unlock(&output->pause.mutex); + } + + if (!output->video_start_ts) + return false; + + /* ------------------ */ + *new = *old; if (old->timestamp < output->video_start_ts) { @@ -1580,10 +1731,10 @@ static void default_raw_audio_callback(void *param, size_t mix_idx, /* -------------- */ - if (!output->video_start_ts) - return; if (!prepare_audio(output, in, &out)) return; + if (audio_pause_check(&output->pause, &out, output->sample_rate)) + return; if (!output->audio_start_ts) { output->audio_start_ts = out.timestamp; } @@ -1610,6 +1761,10 @@ static void default_raw_audio_callback(void *param, size_t mix_idx, audio_frames_to_ns(output->sample_rate, output->total_audio_frames); + pthread_mutex_lock(&output->pause.mutex); + out.timestamp += output->pause.ts_offset; + pthread_mutex_unlock(&output->pause.mutex); + output->total_audio_frames += AUDIO_OUTPUT_FRAMES; if (output->info.raw_audio2) @@ -1906,6 +2061,8 @@ static void reset_raw_output(obs_output_t *output) output->planes = get_audio_planes(info.format, info.speakers); output->total_audio_frames = 0; output->audio_size = get_audio_size(info.format, info.speakers, 1); + + pause_reset(&output->pause); } bool obs_output_begin_data_capture(obs_output_t *output, uint32_t flags) diff --git a/libobs/obs-output.h b/libobs/obs-output.h index b1a5f44c4c9978c1c3ffe1bd3c4d07b4dcd0c4b3..51093f49074502836ef0d0a453521c5edbcd472a 100644 --- a/libobs/obs-output.h +++ b/libobs/obs-output.h @@ -27,6 +27,7 @@ extern "C" { #define OBS_OUTPUT_ENCODED (1 << 2) #define OBS_OUTPUT_SERVICE (1 << 3) #define OBS_OUTPUT_MULTI_TRACK (1 << 4) +#define OBS_OUTPUT_CAN_PAUSE (1 << 5) struct encoder_packet; @@ -56,7 +57,7 @@ struct obs_output_info { obs_properties_t *(*get_properties)(void *data); - void (*pause)(void *data); + void (*unused1)(void *data); uint64_t (*get_total_bytes)(void *data); diff --git a/libobs/obs-video-gpu-encode.c b/libobs/obs-video-gpu-encode.c index 221501b30e9c282fcbbad89b456ab251dd3d2754..808b55c3ce66499281cd6747f411d28629e1118c 100644 --- a/libobs/obs-video-gpu-encode.c +++ b/libobs/obs-video-gpu-encode.c @@ -86,6 +86,9 @@ static void *gpu_encode_thread(void *unused) } } + if (video_pause_check(&encoder->pause, timestamp)) + continue; + if (!encoder->start_ts) encoder->start_ts = timestamp; diff --git a/libobs/obs.h b/libobs/obs.h index 90e464fce17329b36bbd4d7fafd6ebfa93f61aa5..58e862a0dc4969d5d3700a104a23cd0d09113758 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1692,7 +1692,10 @@ EXPORT void obs_output_update(obs_output_t *output, obs_data_t *settings); EXPORT bool obs_output_can_pause(const obs_output_t *output); /** Pauses the output (if the functionality is allowed by the output */ -EXPORT void obs_output_pause(obs_output_t *output); +EXPORT bool obs_output_pause(obs_output_t *output, bool pause); + +/** Returns whether output is paused */ +EXPORT bool obs_output_paused(const obs_output_t *output); /* Gets the current output settings string */ EXPORT obs_data_t *obs_output_get_settings(const obs_output_t *output); @@ -1867,6 +1870,8 @@ EXPORT void obs_output_end_data_capture(obs_output_t *output); */ EXPORT void obs_output_signal_stop(obs_output_t *output, int code); +EXPORT uint64_t obs_output_get_pause_offset(obs_output_t *output); + /* ------------------------------------------------------------------------- */ /* Encoders */ @@ -2031,6 +2036,9 @@ EXPORT void obs_encoder_packet_release(struct encoder_packet *packet); EXPORT void *obs_encoder_create_rerouted(obs_encoder_t *encoder, const char *reroute_id); +/** Returns whether encoder is paused */ +EXPORT bool obs_encoder_paused(const obs_encoder_t *output); + /* ------------------------------------------------------------------------- */ /* Stream Services */ diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index 5f456a34c29d5ef8261ea83193f13bdd401b4ccc..f2d3f9b91bfcde532707dc8187a1346c5f14c60b 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -515,7 +515,8 @@ static uint64_t ffmpeg_mux_total_bytes(void *data) struct obs_output_info ffmpeg_muxer = { .id = "ffmpeg_muxer", - .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK, + .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | + OBS_OUTPUT_CAN_PAUSE, .get_name = ffmpeg_mux_getname, .create = ffmpeg_mux_create, .destroy = ffmpeg_mux_destroy, @@ -545,8 +546,17 @@ static void replay_buffer_hotkey(void *data, obs_hotkey_id id, return; struct ffmpeg_muxer *stream = data; - if (os_atomic_load_bool(&stream->active)) + + if (os_atomic_load_bool(&stream->active)) { + obs_encoder_t *vencoder = + obs_output_get_video_encoder(stream->output); + if (obs_encoder_paused(vencoder)) { + info("Could not save buffer because encoders paused"); + return; + } + stream->save_ts = os_gettime_ns() / 1000LL; + } } static void save_replay_proc(void *data, calldata_t *cd) @@ -876,7 +886,8 @@ static void replay_buffer_defaults(obs_data_t *s) struct obs_output_info replay_buffer = { .id = "replay_buffer", - .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK, + .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | + OBS_OUTPUT_CAN_PAUSE, .get_name = replay_buffer_getname, .create = replay_buffer_create, .destroy = replay_buffer_destroy, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.c b/plugins/obs-ffmpeg/obs-ffmpeg-output.c index 5c56a9b18718ad4c492d1fe077adaa574231612e..c9a31f74279d475db287ec5a015b881f8a3ad200 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.c @@ -960,6 +960,7 @@ static uint64_t get_packet_sys_dts(struct ffmpeg_output *output, AVPacket *packet) { struct ffmpeg_data *data = &output->ff_data; + uint64_t pause_offset = obs_output_get_pause_offset(output->output); uint64_t start_ts; AVRational time_base; @@ -972,8 +973,9 @@ static uint64_t get_packet_sys_dts(struct ffmpeg_output *output, start_ts = output->audio_start_ts; } - return start_ts + (uint64_t)av_rescale_q(packet->dts, time_base, - (AVRational){1, 1000000000}); + return start_ts + pause_offset + + (uint64_t)av_rescale_q(packet->dts, time_base, + (AVRational){1, 1000000000}); } static int process_packet(struct ffmpeg_output *output) @@ -1247,7 +1249,8 @@ static uint64_t ffmpeg_output_total_bytes(void *data) struct obs_output_info ffmpeg_output = { .id = "ffmpeg_output", - .flags = OBS_OUTPUT_AUDIO | OBS_OUTPUT_VIDEO | OBS_OUTPUT_MULTI_TRACK, + .flags = OBS_OUTPUT_AUDIO | OBS_OUTPUT_VIDEO | OBS_OUTPUT_MULTI_TRACK | + OBS_OUTPUT_CAN_PAUSE, .get_name = ffmpeg_output_getname, .create = ffmpeg_output_create, .destroy = ffmpeg_output_destroy,