mirror of
https://github.com/obsproject/obs-studio.git
synced 2024-07-14 23:34:08 +00:00
Merge pull request #1952 from obsproject/pause
Add the ability to pause and unpause recordings
This commit is contained in:
commit
36c8090492
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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 {
|
||||
|
|
3
UI/data/themes/Dark/media-pause.svg
Normal file
3
UI/data/themes/Dark/media-pause.svg
Normal 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 |
|
@ -507,6 +507,10 @@ QToolButton:pressed {
|
|||
qproperty-icon: url(./Dark/down.svg);
|
||||
}
|
||||
|
||||
* [themeID="pauseIconSmall"] {
|
||||
qproperty-icon: url(./Dark/media-pause.svg);
|
||||
}
|
||||
|
||||
/***********************/
|
||||
/* --- Combo boxes --- */
|
||||
/***********************/
|
||||
|
|
|
@ -39,6 +39,10 @@
|
|||
qproperty-icon: url(:/res/images/down.svg);
|
||||
}
|
||||
|
||||
* [themeID="pauseIconSmall"] {
|
||||
qproperty-icon: url(:/res/images/media-pause.svg);
|
||||
}
|
||||
|
||||
MuteCheckBox {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
3
UI/forms/images/media-pause.svg
Normal file
3
UI/forms/images/media-pause.svg
Normal 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 |
|
@ -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>
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
18
UI/record-button.cpp
Normal 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
12
UI/record-button.hpp
Normal 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;
|
||||
};
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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" &&
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
10
libobs/obs.h
10
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 */
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue