瀏覽代碼

UI: Add pause support

Adds support for pausing recordings.  When settings are eligible for
recordings, a pause button will appear next to the recording button.  If
the settings are not eligible, it will warn the user in the output
settings that they cannot pause recordings if those settings are used.
jp9000 6 年之前
父節點
當前提交
eab10d48b2

+ 2 - 0
UI/CMakeLists.txt

@@ -235,6 +235,7 @@ set(obs_SOURCES
 	slider-ignorewheel.cpp
 	slider-ignorewheel.cpp
 	combobox-ignorewheel.cpp
 	combobox-ignorewheel.cpp
 	spinbox-ignorewheel.cpp
 	spinbox-ignorewheel.cpp
+	record-button.cpp
 	volume-control.cpp
 	volume-control.cpp
 	adv-audio-control.cpp
 	adv-audio-control.cpp
 	item-widget-helpers.cpp
 	item-widget-helpers.cpp
@@ -289,6 +290,7 @@ set(obs_HEADERS
 	focus-list.hpp
 	focus-list.hpp
 	menu-button.hpp
 	menu-button.hpp
 	mute-checkbox.hpp
 	mute-checkbox.hpp
+	record-button.hpp
 	volume-control.hpp
 	volume-control.hpp
 	adv-audio-control.hpp
 	adv-audio-control.hpp
 	item-widget-helpers.hpp
 	item-widget-helpers.hpp

+ 12 - 0
UI/api-interface.cpp

@@ -21,6 +21,7 @@ void EnumSceneCollections(function<bool(const char *, const char *)> &&cb);
 
 
 extern volatile bool streaming_active;
 extern volatile bool streaming_active;
 extern volatile bool recording_active;
 extern volatile bool recording_active;
+extern volatile bool recording_paused;
 extern volatile bool replaybuf_active;
 extern volatile bool replaybuf_active;
 
 
 /* ------------------------------------------------------------------------- */
 /* ------------------------------------------------------------------------- */
@@ -265,6 +266,17 @@ struct OBSStudioAPI : obs_frontend_callbacks {
 		return os_atomic_load_bool(&recording_active);
 		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
 	void obs_frontend_replay_buffer_start(void) override
 	{
 	{
 		QMetaObject::invokeMethod(main, "StartReplayBuffer");
 		QMetaObject::invokeMethod(main, "StartReplayBuffer");

+ 7 - 0
UI/data/locale/en-US.ini

@@ -281,6 +281,9 @@ Output.StartRecordingFailed="Failed to start recording"
 Output.StartReplayFailed="Failed to start replay buffer"
 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."
 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 connect messages
 Output.ConnectFail.Title="Failed to connect"
 Output.ConnectFail.Title="Failed to connect"
@@ -501,6 +504,8 @@ Basic.Main.StartRecording="Start Recording"
 Basic.Main.StartReplayBuffer="Start Replay Buffer"
 Basic.Main.StartReplayBuffer="Start Replay Buffer"
 Basic.Main.StartStreaming="Start Streaming"
 Basic.Main.StartStreaming="Start Streaming"
 Basic.Main.StopRecording="Stop Recording"
 Basic.Main.StopRecording="Stop Recording"
+Basic.Main.PauseRecording="Pause Recording"
+Basic.Main.UnpauseRecording="Unpause Recording"
 Basic.Main.StoppingRecording="Stopping Recording..."
 Basic.Main.StoppingRecording="Stopping Recording..."
 Basic.Main.StopReplayBuffer="Stop Replay Buffer"
 Basic.Main.StopReplayBuffer="Stop Replay Buffer"
 Basic.Main.StoppingReplayBuffer="Stopping 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.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.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.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.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="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?"
 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.NoTracksSelected="You must select at least one track"
 OutputWarnings.MultiTrackRecording="Warning: Certain formats (such as FLV) do not support multiple tracks per recording"
 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.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
 # deleting final scene
 FinalScene.Title="Delete Scene"
 FinalScene.Title="Delete Scene"

+ 4 - 0
UI/data/themes/Acri.qss

@@ -353,6 +353,10 @@ QToolButton:pressed {
 	qproperty-icon: url(./Dark/down.svg);
 	qproperty-icon: url(./Dark/down.svg);
 }
 }
 
 
+* [themeID="pauseIconSmall"] {
+	qproperty-icon: url(./Dark/media-pause.svg);
+}
+
 /* Tab Widget */
 /* Tab Widget */
 
 
 QTabWidget::pane { /* The tab widget frame */
 QTabWidget::pane { /* The tab widget frame */

+ 17 - 0
UI/data/themes/Dark.qss

@@ -253,6 +253,10 @@ QToolButton:pressed {
     qproperty-icon: url(./Dark/down.svg);
     qproperty-icon: url(./Dark/down.svg);
 }
 }
 
 
+* [themeID="pauseIconSmall"] {
+    qproperty-icon: url(./Dark/media-pause.svg);
+}
+
 
 
 /* Tab Widget */
 /* Tab Widget */
 
 
@@ -577,6 +581,19 @@ OBSHotkeyLabel[hotkeyPairHover=true] {
     color: red;
     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 */
 /* Group Collapse Checkbox */
 
 
 SourceTreeSubItemCheckBox {
 SourceTreeSubItemCheckBox {

+ 3 - 0
UI/data/themes/Dark/media-pause.svg

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

+ 4 - 0
UI/data/themes/Rachni.qss

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

+ 4 - 0
UI/data/themes/System.qss

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

+ 57 - 33
UI/forms/OBSBasic.ui

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

+ 3 - 0
UI/forms/images/media-pause.svg

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

+ 1 - 0
UI/forms/obs.qrc

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

+ 11 - 0
UI/obs-frontend-api/obs-frontend-api.cpp

@@ -227,6 +227,17 @@ bool obs_frontend_recording_active(void)
 	return !!callbacks_valid() ? c->obs_frontend_recording_active() : false;
 	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)
 void obs_frontend_replay_buffer_start(void)
 {
 {
 	if (callbacks_valid())
 	if (callbacks_valid())

+ 5 - 0
UI/obs-frontend-api/obs-frontend-api.h

@@ -44,6 +44,9 @@ enum obs_frontend_event {
 
 
 	OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP,
 	OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP,
 	OBS_FRONTEND_EVENT_FINISHED_LOADING,
 	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_start(void);
 EXPORT void obs_frontend_recording_stop(void);
 EXPORT void obs_frontend_recording_stop(void);
 EXPORT bool obs_frontend_recording_active(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_start(void);
 EXPORT void obs_frontend_replay_buffer_save(void);
 EXPORT void obs_frontend_replay_buffer_save(void);

+ 2 - 0
UI/obs-frontend-api/obs-frontend-internal.hpp

@@ -43,6 +43,8 @@ struct obs_frontend_callbacks {
 	virtual void obs_frontend_recording_start(void) = 0;
 	virtual void obs_frontend_recording_start(void) = 0;
 	virtual void obs_frontend_recording_stop(void) = 0;
 	virtual void obs_frontend_recording_stop(void) = 0;
 	virtual bool obs_frontend_recording_active(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_start(void) = 0;
 	virtual void obs_frontend_replay_buffer_save(void) = 0;
 	virtual void obs_frontend_replay_buffer_save(void) = 0;

+ 18 - 0
UI/record-button.cpp

@@ -0,0 +1,18 @@
+#include "record-button.hpp"
+#include "window-basic-main.hpp"
+
+void RecordButton::resizeEvent(QResizeEvent *event)
+{
+	OBSBasic *main = OBSBasic::Get();
+	if (!main->pause)
+		return;
+
+	QSize newSize = event->size();
+	QSize pauseSize = main->pause->size();
+	int height = main->ui->recordButton->size().height();
+
+	if (pauseSize.height() != height || pauseSize.width() != height) {
+		main->pause->setMinimumSize(height, height);
+		main->pause->setMaximumSize(height, height);
+	}
+}

+ 12 - 0
UI/record-button.hpp

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

+ 2 - 0
UI/window-basic-main-outputs.cpp

@@ -12,6 +12,7 @@ extern bool EncoderAvailable(const char *encoder);
 
 
 volatile bool streaming_active = false;
 volatile bool streaming_active = false;
 volatile bool recording_active = false;
 volatile bool recording_active = false;
+volatile bool recording_paused = false;
 volatile bool replaybuf_active = false;
 volatile bool replaybuf_active = false;
 
 
 static void OBSStreamStarting(void *data, calldata_t *params)
 static void OBSStreamStarting(void *data, calldata_t *params)
@@ -88,6 +89,7 @@ static void OBSStopRecording(void *data, calldata_t *params)
 
 
 	output->recordingActive = false;
 	output->recordingActive = false;
 	os_atomic_set_bool(&recording_active, false);
 	os_atomic_set_bool(&recording_active, false);
+	os_atomic_set_bool(&recording_paused, false);
 	QMetaObject::invokeMethod(output->main, "RecordingStop",
 	QMetaObject::invokeMethod(output->main, "RecordingStop",
 				  Q_ARG(int, code),
 				  Q_ARG(int, code),
 				  Q_ARG(QString, arg_last_error));
 				  Q_ARG(QString, arg_last_error));

+ 156 - 1
UI/window-basic-main.cpp

@@ -2112,6 +2112,17 @@ void OBSBasic::CreateHotkeys()
 	LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording",
 	LoadHotkeyPair(recordingHotkeys, "OBSBasic.StartRecording",
 		       "OBSBasic.StopRecording");
 		       "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(
 	replayBufHotkeys = obs_hotkey_pair_register_frontend(
 		"OBSBasic.StartReplayBuffer",
 		"OBSBasic.StartReplayBuffer",
 		Str("Basic.Main.StartReplayBuffer"),
 		Str("Basic.Main.StartReplayBuffer"),
@@ -2169,6 +2180,7 @@ void OBSBasic::ClearHotkeys()
 {
 {
 	obs_hotkey_pair_unregister(streamingHotkeys);
 	obs_hotkey_pair_unregister(streamingHotkeys);
 	obs_hotkey_pair_unregister(recordingHotkeys);
 	obs_hotkey_pair_unregister(recordingHotkeys);
+	obs_hotkey_pair_unregister(pauseHotkeys);
 	obs_hotkey_pair_unregister(replayBufHotkeys);
 	obs_hotkey_pair_unregister(replayBufHotkeys);
 	obs_hotkey_pair_unregister(togglePreviewHotkeys);
 	obs_hotkey_pair_unregister(togglePreviewHotkeys);
 	obs_hotkey_unregister(forceStreamingStopHotkey);
 	obs_hotkey_unregister(forceStreamingStopHotkey);
@@ -5319,6 +5331,7 @@ void OBSBasic::RecordingStart()
 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED);
 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTED);
 
 
 	OnActivate();
 	OnActivate();
+	UpdatePause();
 
 
 	blog(LOG_INFO, RECORDING_START);
 	blog(LOG_INFO, RECORDING_START);
 }
 }
@@ -5385,11 +5398,46 @@ void OBSBasic::RecordingStop(int code, QString last_error)
 		AutoRemux();
 		AutoRemux();
 
 
 	OnDeactivate();
 	OnDeactivate();
+	UpdatePause(false);
 }
 }
 
 
 #define RP_NO_HOTKEY_TITLE QTStr("Output.ReplayBuffer.NoHotkey.Title")
 #define RP_NO_HOTKEY_TITLE QTStr("Output.ReplayBuffer.NoHotkey.Title")
 #define RP_NO_HOTKEY_TEXT QTStr("Output.ReplayBuffer.NoHotkey.Msg")
 #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()
 void OBSBasic::StartReplayBuffer()
 {
 {
 	if (!outputHandler || !outputHandler->replayBuffer)
 	if (!outputHandler || !outputHandler->replayBuffer)
@@ -5423,8 +5471,12 @@ void OBSBasic::StartReplayBuffer()
 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING);
 		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING);
 
 
 	SaveProject();
 	SaveProject();
-	if (!outputHandler->StartReplayBuffer())
+
+	if (!outputHandler->StartReplayBuffer()) {
 		replayBufferButton->setChecked(false);
 		replayBufferButton->setChecked(false);
+	} else if (os_atomic_load_bool(&recording_paused)) {
+		ShowReplayBufferPauseWarning();
+	}
 }
 }
 
 
 void OBSBasic::ReplayBufferStopping()
 void OBSBasic::ReplayBufferStopping()
@@ -7295,3 +7347,106 @@ void OBSBasic::UpdatePatronJson(const QString &text, const QString &error)
 
 
 	patronJson = QT_TO_UTF8(text);
 	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();
+	}
+}

+ 11 - 2
UI/window-basic-main.hpp

@@ -123,6 +123,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class Auth;
 	friend class Auth;
 	friend class AutoConfig;
 	friend class AutoConfig;
 	friend class AutoConfigStreamPage;
 	friend class AutoConfigStreamPage;
+	friend class RecordButton;
 	friend struct OBSStudioAPI;
 	friend struct OBSStudioAPI;
 
 
 	enum class MoveDir { Up, Down, Left, Right };
 	enum class MoveDir { Up, Down, Left, Right };
@@ -204,6 +205,7 @@ private:
 
 
 	QPointer<QPushButton> transitionButton;
 	QPointer<QPushButton> transitionButton;
 	QPointer<QPushButton> replayBufferButton;
 	QPointer<QPushButton> replayBufferButton;
+	QScopedPointer<QPushButton> pause;
 
 
 	QScopedPointer<QSystemTrayIcon> trayIcon;
 	QScopedPointer<QSystemTrayIcon> trayIcon;
 	QPointer<QAction> sysTrayStream;
 	QPointer<QAction> sysTrayStream;
@@ -323,8 +325,8 @@ private:
 
 
 	int GetTopSelectedSourceItem();
 	int GetTopSelectedSourceItem();
 
 
-	obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, replayBufHotkeys,
-		togglePreviewHotkeys;
+	obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys,
+		replayBufHotkeys, togglePreviewHotkeys;
 	obs_hotkey_id forceStreamingStopHotkey;
 	obs_hotkey_id forceStreamingStopHotkey;
 
 
 	void InitDefaultTransitions();
 	void InitDefaultTransitions();
@@ -440,6 +442,7 @@ public slots:
 	void RecordStopping();
 	void RecordStopping();
 	void RecordingStop(int code, QString last_error);
 	void RecordingStop(int code, QString last_error);
 
 
+	void ShowReplayBufferPauseWarning();
 	void StartReplayBuffer();
 	void StartReplayBuffer();
 	void StopReplayBuffer();
 	void StopReplayBuffer();
 
 
@@ -465,6 +468,9 @@ public slots:
 
 
 	void UpdatePatronJson(const QString &text, const QString &error);
 	void UpdatePatronJson(const QString &text, const QString &error);
 
 
+	void PauseRecording();
+	void UnpauseRecording();
+
 private slots:
 private slots:
 	void AddSceneItem(OBSSceneItem item);
 	void AddSceneItem(OBSSceneItem item);
 	void AddScene(OBSSource source);
 	void AddScene(OBSSource source);
@@ -557,6 +563,7 @@ private:
 	static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed);
 	static void HotkeyTriggered(void *data, obs_hotkey_id id, bool pressed);
 
 
 	void AutoRemux();
 	void AutoRemux();
+	void UpdatePause(bool activate = true);
 
 
 public:
 public:
 	OBSSource GetProgramSource();
 	OBSSource GetProgramSource();
@@ -760,6 +767,8 @@ private slots:
 	void on_resetUI_triggered();
 	void on_resetUI_triggered();
 	void on_lockUI_toggled(bool lock);
 	void on_lockUI_toggled(bool lock);
 
 
+	void PauseToggled();
+
 	void logUploadFinished(const QString &text, const QString &error);
 	void logUploadFinished(const QString &text, const QString &error);
 
 
 	void updateCheckFinished();
 	void updateCheckFinished();

+ 13 - 0
UI/window-basic-settings.cpp

@@ -729,6 +729,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 		SLOT(AdvOutRecCheckWarnings()));
 		SLOT(AdvOutRecCheckWarnings()));
 	connect(ui->advOutRecFormat, SIGNAL(currentIndexChanged(int)), this,
 	connect(ui->advOutRecFormat, SIGNAL(currentIndexChanged(int)), this,
 		SLOT(AdvOutRecCheckWarnings()));
 		SLOT(AdvOutRecCheckWarnings()));
+	connect(ui->advOutRecEncoder, SIGNAL(currentIndexChanged(int)), this,
+		SLOT(AdvOutRecCheckWarnings()));
 	AdvOutRecCheckWarnings();
 	AdvOutRecCheckWarnings();
 
 
 	ui->buttonBox->button(QDialogButtonBox::Apply)->setIcon(QIcon());
 	ui->buttonBox->button(QDialogButtonBox::Apply)->setIcon(QIcon());
@@ -3929,6 +3931,13 @@ void OBSBasicSettings::AdvOutRecCheckWarnings()
 		warningMsg = QTStr("OutputWarnings.MultiTrackRecording");
 		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 ||
 	if (ui->advOutRecFormat->currentText().compare("mp4") == 0 ||
 	    ui->advOutRecFormat->currentText().compare("mov") == 0) {
 	    ui->advOutRecFormat->currentText().compare("mov") == 0) {
 		if (!warningMsg.isEmpty())
 		if (!warningMsg.isEmpty())
@@ -4387,6 +4396,10 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged()
 				warning += "\n\n";
 				warning += "\n\n";
 			warning += SIMPLE_OUTPUT_WARNING("Encoder");
 			warning += SIMPLE_OUTPUT_WARNING("Encoder");
 		}
 		}
+	} else {
+		if (!warning.isEmpty())
+			warning += "\n\n";
+		warning += SIMPLE_OUTPUT_WARNING("CannotPause");
 	}
 	}
 
 
 	if (qual != "Lossless" &&
 	if (qual != "Lossless" &&

+ 17 - 6
UI/window-basic-status-bar.cpp

@@ -241,17 +241,28 @@ void OBSBasicStatusBar::UpdateStreamTime()
 	}
 	}
 }
 }
 
 
+extern volatile bool recording_paused;
+
 void OBSBasicStatusBar::UpdateRecordTime()
 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;
 	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->setText(text);
 	recordTime->setMinimumWidth(recordTime->width());
 	recordTime->setMinimumWidth(recordTime->width());
 }
 }

+ 24 - 0
docs/sphinx/reference-frontend-api.rst

@@ -124,6 +124,18 @@ Structures/Enumerations
      the program is either about to load a new scene collection, or the
      the program is either about to load a new scene collection, or the
      program is about to exit.
      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
 .. type:: struct obs_frontend_source_list
 
 
    - DARRAY(obs_source_t*) **sources**
    - 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)
 .. function:: void obs_frontend_replay_buffer_start(void)
 
 
    Starts replay buffer.
    Starts replay buffer.