Merge pull request #1952 from obsproject/pause

Add the ability to pause and unpause recordings
This commit is contained in:
Jim 2019-07-11 19:56:41 -07:00 committed by GitHub
commit 36c8090492
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 779 additions and 73 deletions

View file

@ -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

View file

@ -21,6 +21,7 @@ void EnumSceneCollections(function<bool(const char *, const char *)> &&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");

View file

@ -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"

View file

@ -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 */

View file

@ -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 {

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" fill="#d2d2d2">
<path d="M0 0v6h2v-6h-2zm4 0v6h2v-6h-2z" transform="translate(1 1)" />
</svg>

After

Width:  |  Height:  |  Size: 154 B

View file

@ -507,6 +507,10 @@ QToolButton:pressed {
qproperty-icon: url(./Dark/down.svg);
}
* [themeID="pauseIconSmall"] {
qproperty-icon: url(./Dark/media-pause.svg);
}
/***********************/
/* --- Combo boxes --- */
/***********************/

View file

@ -39,6 +39,10 @@
qproperty-icon: url(:/res/images/down.svg);
}
* [themeID="pauseIconSmall"] {
qproperty-icon: url(:/res/images/media-pause.svg);
}
MuteCheckBox {
outline: none;
}

View file

@ -116,7 +116,7 @@
<x>0</x>
<y>0</y>
<width>1079</width>
<height>22</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menu_File">
@ -656,7 +656,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>64</width>
<width>80</width>
<height>16</height>
</rect>
</property>
@ -843,7 +843,7 @@
<string notr="true"/>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/add.png</normaloff>:/res/images/add.png</iconset>
</property>
<property name="flat">
@ -878,7 +878,7 @@
<string notr="true"/>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/list_remove.png</normaloff>:/res/images/list_remove.png</iconset>
</property>
<property name="flat">
@ -913,7 +913,7 @@
<string notr="true"/>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/configuration21_16.png</normaloff>:/res/images/configuration21_16.png</iconset>
</property>
<property name="flat">
@ -1000,7 +1000,7 @@
<attribute name="dockWidgetArea">
<number>8</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents_3">
<widget class="QWidget" name="controlsDockContents">
<layout class="QVBoxLayout" name="buttonsVLayout">
<property name="spacing">
<number>2</number>
@ -1037,29 +1037,48 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="recordButton">
<property name="enabled">
<bool>true</bool>
<layout class="QHBoxLayout" name="recordingLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<property name="leftMargin">
<number>0</number>
</property>
<property name="minimumSize">
<size>
<width>130</width>
<height>0</height>
</size>
<property name="topMargin">
<number>0</number>
</property>
<property name="text">
<string>Basic.Main.StartRecording</string>
<property name="rightMargin">
<number>0</number>
</property>
<property name="checkable">
<bool>true</bool>
<property name="bottomMargin">
<number>0</number>
</property>
</widget>
<item>
<widget class="RecordButton" name="recordButton">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>130</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Basic.Main.StartRecording</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="modeSwitch">
@ -1115,7 +1134,7 @@
</widget>
<action name="actionAddScene">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/add.png</normaloff>:/res/images/add.png</iconset>
</property>
<property name="text">
@ -1127,7 +1146,7 @@
</action>
<action name="actionAddSource">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/add.png</normaloff>:/res/images/add.png</iconset>
</property>
<property name="text">
@ -1139,7 +1158,7 @@
</action>
<action name="actionRemoveScene">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/list_remove.png</normaloff>:/res/images/list_remove.png</iconset>
</property>
<property name="text">
@ -1157,7 +1176,7 @@
</action>
<action name="actionRemoveSource">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/list_remove.png</normaloff>:/res/images/list_remove.png</iconset>
</property>
<property name="text">
@ -1178,7 +1197,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/properties.png</normaloff>:/res/images/properties.png</iconset>
</property>
<property name="text">
@ -1190,7 +1209,7 @@
</action>
<action name="actionSceneUp">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/up.png</normaloff>:/res/images/up.png</iconset>
</property>
<property name="text">
@ -1205,7 +1224,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/up.png</normaloff>:/res/images/up.png</iconset>
</property>
<property name="text">
@ -1217,7 +1236,7 @@
</action>
<action name="actionSceneDown">
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/down.png</normaloff>:/res/images/down.png</iconset>
</property>
<property name="text">
@ -1232,7 +1251,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="obs.qrc">
<iconset>
<normaloff>:/res/images/down.png</normaloff>:/res/images/down.png</iconset>
</property>
<property name="text">
@ -1733,6 +1752,11 @@
<header>window-dock.hpp</header>
<container>1</container>
</customwidget>
<customwidget>
<class>RecordButton</class>
<extends>QPushButton</extends>
<header>record-button.hpp</header>
</customwidget>
</customwidgets>
<resources>
<include location="obs.qrc"/>

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" fill="#000000">
<path d="M0 0v6h2v-6h-2zm4 0v6h2v-6h-2z" transform="translate(1 1)" />
</svg>

After

Width:  |  Height:  |  Size: 154 B

View file

@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/res">
<file>images/media-pause.svg</file>
<file>images/mute.svg</file>
<file>images/refresh.svg</file>
<file>images/no_sources.svg</file>

View file

@ -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())

View file

@ -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);

View file

@ -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;

18
UI/record-button.cpp Normal file
View file

@ -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);
}
}

12
UI/record-button.hpp Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <QPushButton>
class RecordButton : public QPushButton {
Q_OBJECT
public:
inline RecordButton(QWidget *parent = nullptr) : QPushButton(parent) {}
virtual void resizeEvent(QResizeEvent *event) override;
};

View file

@ -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));

View file

@ -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();
}
}

View file

@ -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<QPushButton> transitionButton;
QPointer<QPushButton> replayBufferButton;
QScopedPointer<QPushButton> pause;
QScopedPointer<QSystemTrayIcon> trayIcon;
QPointer<QAction> 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();

View file

@ -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" &&

View file

@ -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());
}

View file

@ -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.

View file

@ -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)
.. member:: void (*obs_output_info.unused1)(void *data)
Pauses the output (if the output supports pausing).
(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

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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 (!obs_output_valid(output, "obs_output_pause"))
return;
if (!pause->ts_end) {
pause->ts_end = ts;
pause->ts_offset += pause->ts_end - pause->ts_start;
}
}
if (output->info.pause)
output->info.pause(output->context.data);
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 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 (!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)

View file

@ -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);

View file

@ -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;

View file

@ -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 */

View file

@ -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,

View file

@ -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,