浏览代码

UI: Separate replay buffer from recording

Replay buffer and recording should be separate in case the user wants to
start recording from a specific point rather being forced to reconfigure
for regular recording.

Creates a new button on the main window below the recording button for
turning on/off the replay buffer.
jp9000 8 年之前
父节点
当前提交
f790d0fe08

+ 22 - 0
UI/api-interface.cpp

@@ -234,6 +234,21 @@ struct OBSStudioAPI : obs_frontend_callbacks {
 		return main->outputHandler->RecordingActive();
 	}
 
+	void obs_frontend_replay_buffer_start(void) override
+	{
+		QMetaObject::invokeMethod(main, "StartReplayBuffer");
+	}
+
+	void obs_frontend_replay_buffer_stop(void) override
+	{
+		QMetaObject::invokeMethod(main, "StopReplayBuffer");
+	}
+
+	bool obs_frontend_replay_buffer_active(void) override
+	{
+		return main->outputHandler->ReplayBufferActive();
+	}
+
 	void *obs_frontend_add_tools_menu_qaction(const char *name) override
 	{
 		main->ui->menuTools->setEnabled(true);
@@ -286,6 +301,13 @@ struct OBSStudioAPI : obs_frontend_callbacks {
 		return out;
 	}
 
+	obs_output_t *obs_frontend_get_replay_buffer_output(void) override
+	{
+		OBSOutput out = main->outputHandler->replayBuffer;
+		obs_output_addref(out);
+		return out;
+	}
+
 	config_t *obs_frontend_get_profile_config(void) override
 	{
 		return main->basicConfig;

+ 3 - 5
UI/data/locale/en-US.ini

@@ -420,12 +420,14 @@ Basic.Settings.Output.Mode="Output Mode"
 Basic.Settings.Output.Mode.Simple="Simple"
 Basic.Settings.Output.Mode.Adv="Advanced"
 Basic.Settings.Output.Mode.FFmpeg="FFmpeg Output"
-Basic.Settings.Output.UseReplayBuffer="Replay Buffer Mode"
+Basic.Settings.Output.UseReplayBuffer="Enable Replay Buffer"
 Basic.Settings.Output.ReplayBuffer.SecondsMax="Maximum Replay Time (Seconds)"
 Basic.Settings.Output.ReplayBuffer.MegabytesMax="Maximum Memory (Megabytes)"
 Basic.Settings.Output.ReplayBuffer.Estimate="Estimated memory usage: %1 MB"
 Basic.Settings.Output.ReplayBuffer.EstimateUnknown="Cannot estimate memory usage.  Please set maximum memory limit."
 Basic.Settings.Output.ReplayBuffer.HotkeyMessage="(Note: Make sure to set a hotkey for the replay buffer in the hotkeys section)"
+Basic.Settings.Output.ReplayBuffer.Prefix="Replay Buffer Filename Prefix"
+Basic.Settings.Output.ReplayBuffer.Suffix="Suffix"
 Basic.Settings.Output.Simple.SavePath="Recording Path"
 Basic.Settings.Output.Simple.RecordingQuality="Recording Quality"
 Basic.Settings.Output.Simple.RecordingQuality.Stream="Same as stream"
@@ -572,10 +574,6 @@ Basic.Settings.Hotkeys="Hotkeys"
 Basic.Settings.Hotkeys.Pair="Key combinations shared with '%1' act as toggles"
 
 # basic mode hotkeys
-Basic.Hotkeys.StartStreaming="Start Streaming"
-Basic.Hotkeys.StopStreaming="Stop Streaming"
-Basic.Hotkeys.StartRecording="Start Recording/Replay Buffer"
-Basic.Hotkeys.StopRecording="Stop Recording/Replay Buffer"
 Basic.Hotkeys.SelectScene="Switch to scene"
 
 # system tray

+ 6 - 6
UI/forms/OBSBasicSettings.ui

@@ -943,17 +943,17 @@
                   </property>
                  </widget>
                 </item>
-                <item row="3" column="1">
-                 <widget class="QLabel" name="simpleRBEstimate">
+                <item row="2" column="1">
+                 <widget class="QLabel" name="label_45">
                   <property name="text">
-                   <string notr="true"/>
+                   <string>Basic.Settings.Output.ReplayBuffer.HotkeyMessage</string>
                   </property>
                  </widget>
                 </item>
-                <item row="2" column="1">
-                 <widget class="QLabel" name="label_45">
+                <item row="3" column="1">
+                 <widget class="QLabel" name="simpleRBEstimate">
                   <property name="text">
-                   <string>Basic.Settings.Output.ReplayBuffer.HotkeyMessage</string>
+                   <string notr="true"/>
                   </property>
                  </widget>
                 </item>

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

@@ -205,6 +205,23 @@ bool obs_frontend_recording_active(void)
 		: false;
 }
 
+void obs_frontend_replay_buffer_start(void)
+{
+	if (callbacks_valid()) c->obs_frontend_replay_buffer_start();
+}
+
+void obs_frontend_replay_buffer_stop(void)
+{
+	if (callbacks_valid()) c->obs_frontend_replay_buffer_stop();
+}
+
+bool obs_frontend_replay_buffer_active(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_replay_buffer_active()
+		: false;
+}
+
 void *obs_frontend_add_tools_menu_qaction(const char *name)
 {
 	return !!callbacks_valid()
@@ -248,6 +265,13 @@ obs_output_t *obs_frontend_get_recording_output(void)
 		: nullptr;
 }
 
+obs_output_t *obs_frontend_get_replay_buffer_output(void)
+{
+	return !!callbacks_valid()
+		? c->obs_frontend_get_replay_buffer_output()
+		: nullptr;
+}
+
 config_t *obs_frontend_get_profile_config(void)
 {
 	return !!callbacks_valid()

+ 11 - 1
UI/obs-frontend-api/obs-frontend-api.h

@@ -70,6 +70,10 @@ 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_replay_buffer_start(void);
+EXPORT void obs_frontend_replay_buffer_stop(void);
+EXPORT bool obs_frontend_replay_buffer_active(void);
+
 typedef void (*obs_frontend_cb)(void *private_data);
 
 EXPORT void *obs_frontend_add_tools_menu_qaction(const char *name);
@@ -94,7 +98,12 @@ enum obs_frontend_event {
 	OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED,
 	OBS_FRONTEND_EVENT_PROFILE_CHANGED,
 	OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED,
-	OBS_FRONTEND_EVENT_EXIT
+	OBS_FRONTEND_EVENT_EXIT,
+
+	OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING,
+	OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED,
+	OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING,
+	OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED
 };
 
 typedef void (*obs_frontend_event_cb)(enum obs_frontend_event event,
@@ -116,6 +125,7 @@ EXPORT void obs_frontend_remove_save_callback(obs_frontend_save_cb callback,
 
 EXPORT obs_output_t *obs_frontend_get_streaming_output(void);
 EXPORT obs_output_t *obs_frontend_get_recording_output(void);
+EXPORT obs_output_t *obs_frontend_get_replay_buffer_output(void);
 
 EXPORT config_t *obs_frontend_get_profile_config(void);
 EXPORT config_t *obs_frontend_get_global_config(void);

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

@@ -39,6 +39,10 @@ struct obs_frontend_callbacks {
 	virtual void obs_frontend_recording_stop(void)=0;
 	virtual bool obs_frontend_recording_active(void)=0;
 
+	virtual void obs_frontend_replay_buffer_start(void)=0;
+	virtual void obs_frontend_replay_buffer_stop(void)=0;
+	virtual bool obs_frontend_replay_buffer_active(void)=0;
+
 	virtual void *obs_frontend_add_tools_menu_qaction(const char *name)=0;
 	virtual void obs_frontend_add_tools_menu_item(const char *name,
 			obs_frontend_cb callback, void *private_data)=0;
@@ -50,6 +54,7 @@ struct obs_frontend_callbacks {
 
 	virtual obs_output_t *obs_frontend_get_streaming_output(void)=0;
 	virtual obs_output_t *obs_frontend_get_recording_output(void)=0;
+	virtual obs_output_t *obs_frontend_get_replay_buffer_output(void)=0;
 
 	virtual config_t *obs_frontend_get_profile_config(void)=0;
 	virtual config_t *obs_frontend_get_global_config(void)=0;

+ 112 - 18
UI/window-basic-main-outputs.cpp

@@ -84,6 +84,36 @@ static void OBSRecordStopping(void *data, calldata_t *params)
 	UNUSED_PARAMETER(params);
 }
 
+static void OBSStartReplayBuffer(void *data, calldata_t *params)
+{
+	BasicOutputHandler *output = static_cast<BasicOutputHandler*>(data);
+
+	output->replayBufferActive = true;
+	QMetaObject::invokeMethod(output->main, "ReplayBufferStart");
+
+	UNUSED_PARAMETER(params);
+}
+
+static void OBSStopReplayBuffer(void *data, calldata_t *params)
+{
+	BasicOutputHandler *output = static_cast<BasicOutputHandler*>(data);
+	int code = (int)calldata_int(params, "code");
+
+	output->replayBufferActive = false;
+	QMetaObject::invokeMethod(output->main,
+			"ReplayBufferStop", Q_ARG(int, code));
+
+	UNUSED_PARAMETER(params);
+}
+
+static void OBSReplayBufferStopping(void *data, calldata_t *params)
+{
+	BasicOutputHandler *output = static_cast<BasicOutputHandler*>(data);
+	QMetaObject::invokeMethod(output->main, "ReplayBufferStopping");
+
+	UNUSED_PARAMETER(params);
+}
+
 static void FindBestFilename(string &strPath, bool noSpace)
 {
 	int num = 2;
@@ -154,6 +184,7 @@ struct SimpleOutput : BasicOutputHandler {
 	string                 videoEncoder;
 	string                 videoQuality;
 	bool                   usingRecordingPreset = false;
+	bool                   recordingConfigured = false;
 	bool                   ffmpegOutput = false;
 	bool                   lowCPUx264 = false;
 
@@ -179,12 +210,18 @@ struct SimpleOutput : BasicOutputHandler {
 
 	void LoadStreamingPreset_h264(const char *encoder);
 
+	void UpdateRecording();
+	bool ConfigureRecording(bool useReplayBuffer);
+
 	virtual bool StartStreaming(obs_service_t *service) override;
 	virtual bool StartRecording() override;
+	virtual bool StartReplayBuffer() override;
 	virtual void StopStreaming(bool force) override;
 	virtual void StopRecording(bool force) override;
+	virtual void StopReplayBuffer(bool force) override;
 	virtual bool StreamingActive() const override;
 	virtual bool RecordingActive() const override;
+	virtual bool ReplayBufferActive() const override;
 };
 
 void SimpleOutput::LoadRecordingPreset_Lossless()
@@ -306,21 +343,34 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_)
 	LoadRecordingPreset();
 
 	if (!ffmpegOutput) {
-		replayBuffer = config_get_bool(main->Config(),
+		bool useReplayBuffer = config_get_bool(main->Config(),
 				"SimpleOutput", "RecRB");
-		if (replayBuffer) {
+		if (useReplayBuffer) {
 			const char *str = config_get_string(main->Config(),
 					"Hotkeys", "ReplayBuffer");
 			obs_data_t *hotkey = obs_data_create_from_json(str);
-			fileOutput = obs_output_create("replay_buffer",
+			replayBuffer = obs_output_create("replay_buffer",
 					Str("ReplayBuffer"), nullptr, hotkey);
 
 			obs_data_release(hotkey);
-		} else {
-			fileOutput = obs_output_create("ffmpeg_muxer",
-					"simple_file_output", nullptr, nullptr);
+			if (!replayBuffer)
+				throw "Failed to create replay buffer output "
+				      "(simple output)";
+			obs_output_release(replayBuffer);
+
+			signal_handler_t *signal =
+				obs_output_get_signal_handler(replayBuffer);
+
+			startReplayBuffer.Connect(signal, "start",
+					OBSStartReplayBuffer, this);
+			stopReplayBuffer.Connect(signal, "stop",
+					OBSStopReplayBuffer, this);
+			replayBufferStopping.Connect(signal, "stopping",
+					OBSReplayBufferStopping, this);
 		}
 
+		fileOutput = obs_output_create("ffmpeg_muxer",
+				"simple_file_output", nullptr, nullptr);
 		if (!fileOutput)
 			throw "Failed to create recording output "
 			      "(simple output)";
@@ -649,8 +699,11 @@ static void ensure_directory_exists(string &path)
 	os_mkdirs(directory.c_str());
 }
 
-bool SimpleOutput::StartRecording()
+void SimpleOutput::UpdateRecording()
 {
+	if (replayBufferActive || recordingActive)
+		return;
+
 	if (usingRecordingPreset) {
 		if (!ffmpegOutput)
 			UpdateRecordingSettings();
@@ -661,6 +714,20 @@ bool SimpleOutput::StartRecording()
 	if (!Active())
 		SetupOutputs();
 
+	if (!ffmpegOutput) {
+		obs_output_set_video_encoder(fileOutput, h264Recording);
+		obs_output_set_audio_encoder(fileOutput, aacRecording, 0);
+	}
+	if (replayBuffer) {
+		obs_output_set_video_encoder(replayBuffer, h264Recording);
+		obs_output_set_audio_encoder(replayBuffer, aacRecording, 0);
+	}
+
+	recordingConfigured = true;
+}
+
+bool SimpleOutput::ConfigureRecording(bool updateReplayBuffer)
+{
 	const char *path = config_get_string(main->Config(),
 			"SimpleOutput", "FilePath");
 	const char *format = config_get_string(main->Config(),
@@ -706,13 +773,8 @@ bool SimpleOutput::StartRecording()
 	if (!overwriteIfExists)
 		FindBestFilename(strPath, noSpace);
 
-	if (!ffmpegOutput) {
-		obs_output_set_video_encoder(fileOutput, h264Recording);
-		obs_output_set_audio_encoder(fileOutput, aacRecording, 0);
-	}
-
 	obs_data_t *settings = obs_data_create();
-	if (replayBuffer) {
+	if (updateReplayBuffer) {
 		obs_data_set_string(settings, "directory", path);
 		obs_data_set_string(settings, "format", filenameFormat);
 		obs_data_set_string(settings, "extension", format);
@@ -723,17 +785,36 @@ bool SimpleOutput::StartRecording()
 		obs_data_set_string(settings, ffmpegOutput ? "url" : "path",
 				strPath.c_str());
 	}
+
 	obs_data_set_string(settings, "muxer_settings", mux);
 
-	obs_output_update(fileOutput, settings);
+	if (updateReplayBuffer)
+		obs_output_update(replayBuffer, settings);
+	else
+		obs_output_update(fileOutput, settings);
 
 	obs_data_release(settings);
+	return true;
+}
 
-	if (obs_output_start(fileOutput)) {
-		return true;
-	}
+bool SimpleOutput::StartRecording()
+{
+	UpdateRecording();
+	if (!ConfigureRecording(false))
+		return false;
+	if (!obs_output_start(fileOutput))
+		return false;
+	return true;
+}
 
-	return false;
+bool SimpleOutput::StartReplayBuffer()
+{
+	UpdateRecording();
+	if (!ConfigureRecording(true))
+		return false;
+	if (!obs_output_start(replayBuffer))
+		return false;
+	return true;
 }
 
 void SimpleOutput::StopStreaming(bool force)
@@ -752,6 +833,14 @@ void SimpleOutput::StopRecording(bool force)
 		obs_output_stop(fileOutput);
 }
 
+void SimpleOutput::StopReplayBuffer(bool force)
+{
+	if (force)
+		obs_output_force_stop(replayBuffer);
+	else
+		obs_output_stop(replayBuffer);
+}
+
 bool SimpleOutput::StreamingActive() const
 {
 	return obs_output_active(streamOutput);
@@ -762,6 +851,11 @@ bool SimpleOutput::RecordingActive() const
 	return obs_output_active(fileOutput);
 }
 
+bool SimpleOutput::ReplayBufferActive() const
+{
+	return obs_output_active(replayBuffer);
+}
+
 /* ------------------------------------------------------------------------ */
 
 struct AdvancedOutput : BasicOutputHandler {

+ 10 - 2
UI/window-basic-main-outputs.hpp

@@ -5,19 +5,23 @@ class OBSBasic;
 struct BasicOutputHandler {
 	OBSOutput              fileOutput;
 	OBSOutput              streamOutput;
+	OBSOutput              replayBuffer;
 	bool                   streamingActive = false;
 	bool                   recordingActive = false;
 	bool                   delayActive = false;
-	bool                   replayBuffer = false;
+	bool                   replayBufferActive = false;
 	OBSBasic               *main;
 
 	OBSSignal              startRecording;
 	OBSSignal              stopRecording;
+	OBSSignal              startReplayBuffer;
+	OBSSignal              stopReplayBuffer;
 	OBSSignal              startStreaming;
 	OBSSignal              stopStreaming;
 	OBSSignal              streamDelayStarting;
 	OBSSignal              streamStopping;
 	OBSSignal              recordStopping;
+	OBSSignal              replayBufferStopping;
 
 	inline BasicOutputHandler(OBSBasic *main_) : main(main_) {}
 
@@ -25,16 +29,20 @@ struct BasicOutputHandler {
 
 	virtual bool StartStreaming(obs_service_t *service) = 0;
 	virtual bool StartRecording() = 0;
+	virtual bool StartReplayBuffer() {return false;}
 	virtual void StopStreaming(bool force = false) = 0;
 	virtual void StopRecording(bool force = false) = 0;
+	virtual void StopReplayBuffer(bool force = false) {(void)force;}
 	virtual bool StreamingActive() const = 0;
 	virtual bool RecordingActive() const = 0;
+	virtual bool ReplayBufferActive() const {return false;}
 
 	virtual void Update() = 0;
 
 	inline bool Active() const
 	{
-		return streamingActive || recordingActive || delayActive;
+		return streamingActive || recordingActive || delayActive ||
+			replayBufferActive;
 	}
 };
 

+ 182 - 47
UI/window-basic-main.cpp

@@ -1032,6 +1032,14 @@ void OBSBasic::InitPrimitives()
 	obs_leave_graphics();
 }
 
+void OBSBasic::ReplayBufferClicked()
+{
+	if (outputHandler->ReplayBufferActive())
+		StopReplayBuffer();
+	else
+		StartReplayBuffer();
+};
+
 void OBSBasic::ResetOutputs()
 {
 	ProfileScope("OBSBasic::ResetOutputs");
@@ -1045,15 +1053,23 @@ void OBSBasic::ResetOutputs()
 			CreateAdvancedOutputHandler(this) :
 			CreateSimpleOutputHandler(this));
 
-		if (outputHandler->replayBuffer)
-			ui->recordButton->setText(
-					QTStr("Basic.Main.StartReplayBuffer"));
-		else
-			ui->recordButton->setText(
-					QTStr("Basic.Main.StartRecording"));
+		delete replayBufferButton;
+
+		if (outputHandler->replayBuffer) {
+			replayBufferButton = new QPushButton(
+					QTStr("Basic.Main.StartReplayBuffer"),
+					this);
+			connect(replayBufferButton,
+					&QPushButton::clicked,
+					this,
+					&OBSBasic::ReplayBufferClicked);
+
+			ui->buttonsVLayout->insertWidget(2, replayBufferButton);
+		}
 
-		if (sysTrayRecord)
-			sysTrayRecord->setText(ui->recordButton->text());
+		if (sysTrayReplayBuffer)
+			sysTrayReplayBuffer->setEnabled(
+					!!outputHandler->replayBuffer);
 	} else {
 		outputHandler->Update();
 	}
@@ -1357,9 +1373,9 @@ void OBSBasic::CreateHotkeys()
 
 	streamingHotkeys = obs_hotkey_pair_register_frontend(
 			"OBSBasic.StartStreaming",
-			Str("Basic.Hotkeys.StartStreaming"),
+			Str("Basic.Main.StartStreaming"),
 			"OBSBasic.StopStreaming",
-			Str("Basic.Hotkeys.StopStreaming"),
+			Str("Basic.Main.StopStreaming"),
 			MAKE_CALLBACK(!basic.outputHandler->StreamingActive(),
 				basic.StartStreaming),
 			MAKE_CALLBACK(basic.outputHandler->StreamingActive(),
@@ -1385,9 +1401,9 @@ void OBSBasic::CreateHotkeys()
 
 	recordingHotkeys = obs_hotkey_pair_register_frontend(
 			"OBSBasic.StartRecording",
-			Str("Basic.Hotkeys.StartRecording"),
+			Str("Basic.Main.StartRecording"),
 			"OBSBasic.StopRecording",
-			Str("Basic.Hotkeys.StopRecording"),
+			Str("Basic.Main.StopRecording"),
 			MAKE_CALLBACK(!basic.outputHandler->RecordingActive(),
 				basic.StartRecording),
 			MAKE_CALLBACK(basic.outputHandler->RecordingActive(),
@@ -1395,6 +1411,19 @@ void OBSBasic::CreateHotkeys()
 			this, this);
 	LoadHotkeyPair(recordingHotkeys,
 			"OBSBasic.StartRecording", "OBSBasic.StopRecording");
+
+	replayBufHotkeys = obs_hotkey_pair_register_frontend(
+			"OBSBasic.StartReplayBuffer",
+			Str("Basic.Main.StartReplayBuffer"),
+			"OBSBasic.StopReplayBuffer",
+			Str("Basic.Main.StopReplayBuffer"),
+			MAKE_CALLBACK(!basic.outputHandler->ReplayBufferActive(),
+				basic.StartReplayBuffer),
+			MAKE_CALLBACK(basic.outputHandler->ReplayBufferActive(),
+				basic.StopReplayBuffer),
+			this, this);
+	LoadHotkeyPair(replayBufHotkeys,
+			"OBSBasic.StartReplayBuffer", "OBSBasic.StopReplayBuffer");
 #undef MAKE_CALLBACK
 
 	auto togglePreviewProgram = [] (void *data, obs_hotkey_id,
@@ -1431,6 +1460,7 @@ void OBSBasic::ClearHotkeys()
 {
 	obs_hotkey_pair_unregister(streamingHotkeys);
 	obs_hotkey_pair_unregister(recordingHotkeys);
+	obs_hotkey_pair_unregister(replayBufHotkeys);
 	obs_hotkey_unregister(forceStreamingStopHotkey);
 	obs_hotkey_unregister(togglePreviewProgramHotkey);
 	obs_hotkey_unregister(transitionHotkey);
@@ -3660,6 +3690,10 @@ void OBSBasic::OpenSceneFilters()
 	"==== Recording Start ==============================================="
 #define RECORDING_STOP \
 	"==== Recording Stop ================================================"
+#define REPLAY_BUFFER_START \
+	"==== Replay Buffer Start ==========================================="
+#define REPLAY_BUFFER_STOP \
+	"==== Replay Buffer Stop ============================================"
 #define STREAMING_START \
 	"==== Streaming Start ==============================================="
 #define STREAMING_STOP \
@@ -3918,31 +3952,11 @@ void OBSBasic::StreamingStop(int code)
 	}
 }
 
-#define RP_NO_HOTKEY_TITLE QTStr("Output.ReplayBuffer.NoHotkey.Title")
-#define RP_NO_HOTKEY_TEXT  QTStr("Output.ReplayBuffer.NoHotkey.Msg")
-
 void OBSBasic::StartRecording()
 {
 	if (outputHandler->RecordingActive())
 		return;
 
-	if (outputHandler->replayBuffer) {
-		obs_output_t *output = outputHandler->fileOutput;
-		obs_data_t *hotkeys = obs_hotkeys_save_output(output);
-		obs_data_array_t *bindings = obs_data_get_array(hotkeys,
-				"ReplayBuffer.Save");
-		size_t count = obs_data_array_count(bindings);
-		obs_data_array_release(bindings);
-		obs_data_release(hotkeys);
-
-		if (!count) {
-			QMessageBox::information(this,
-					RP_NO_HOTKEY_TITLE,
-					RP_NO_HOTKEY_TEXT);
-			return;
-		}
-	}
-
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_RECORDING_STARTING);
 
@@ -3952,10 +3966,7 @@ void OBSBasic::StartRecording()
 
 void OBSBasic::RecordStopping()
 {
-	if (outputHandler->replayBuffer)
-		ui->recordButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
-	else
-		ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
+	ui->recordButton->setText(QTStr("Basic.Main.StoppingRecording"));
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(ui->recordButton->text());
@@ -3978,11 +3989,7 @@ void OBSBasic::StopRecording()
 void OBSBasic::RecordingStart()
 {
 	ui->statusbar->RecordingStarted(outputHandler->fileOutput);
-
-	if (outputHandler->replayBuffer)
-		ui->recordButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
-	else
-		ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
+	ui->recordButton->setText(QTStr("Basic.Main.StopRecording"));
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(ui->recordButton->text());
@@ -3999,14 +4006,11 @@ void OBSBasic::RecordingStart()
 void OBSBasic::RecordingStop(int code)
 {
 	ui->statusbar->RecordingStopped();
-
-	if (outputHandler->replayBuffer)
-		ui->recordButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
-	else
-		ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
+	ui->recordButton->setText(QTStr("Basic.Main.StartRecording"));
 
 	if (sysTrayRecord)
 		sysTrayRecord->setText(ui->recordButton->text());
+
 	blog(LOG_INFO, RECORDING_STOP);
 
 	if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
@@ -4043,6 +4047,131 @@ void OBSBasic::RecordingStop(int code)
 	OnDeactivate();
 }
 
+#define RP_NO_HOTKEY_TITLE QTStr("Output.ReplayBuffer.NoHotkey.Title")
+#define RP_NO_HOTKEY_TEXT  QTStr("Output.ReplayBuffer.NoHotkey.Msg")
+
+void OBSBasic::StartReplayBuffer()
+{
+	if (!outputHandler || !outputHandler->replayBuffer)
+		return;
+	if (outputHandler->ReplayBufferActive())
+		return;
+
+	obs_output_t *output = outputHandler->replayBuffer;
+	obs_data_t *hotkeys = obs_hotkeys_save_output(output);
+	obs_data_array_t *bindings = obs_data_get_array(hotkeys,
+			"ReplayBuffer.Save");
+	size_t count = obs_data_array_count(bindings);
+	obs_data_array_release(bindings);
+	obs_data_release(hotkeys);
+
+	if (!count) {
+		QMessageBox::information(this,
+				RP_NO_HOTKEY_TITLE,
+				RP_NO_HOTKEY_TEXT);
+		return;
+	}
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING);
+
+	SaveProject();
+	outputHandler->StartReplayBuffer();
+}
+
+void OBSBasic::ReplayBufferStopping()
+{
+	if (!outputHandler || !outputHandler->replayBuffer)
+		return;
+
+	replayBufferButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
+
+	if (sysTrayReplayBuffer)
+		sysTrayReplayBuffer->setText(replayBufferButton->text());
+
+	replayBufferStopping = true;
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING);
+}
+
+void OBSBasic::StopReplayBuffer()
+{
+	if (!outputHandler || !outputHandler->replayBuffer)
+		return;
+
+	SaveProject();
+
+	if (outputHandler->ReplayBufferActive())
+		outputHandler->StopReplayBuffer(replayBufferStopping);
+
+	OnDeactivate();
+}
+
+void OBSBasic::ReplayBufferStart()
+{
+	if (!outputHandler || !outputHandler->replayBuffer)
+		return;
+
+	replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
+
+	if (sysTrayReplayBuffer)
+		sysTrayReplayBuffer->setText(replayBufferButton->text());
+
+	replayBufferStopping = false;
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED);
+
+	OnActivate();
+
+	blog(LOG_INFO, REPLAY_BUFFER_START);
+}
+
+void OBSBasic::ReplayBufferStop(int code)
+{
+	if (!outputHandler || !outputHandler->replayBuffer)
+		return;
+
+	replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
+
+	if (sysTrayReplayBuffer)
+		sysTrayReplayBuffer->setText(replayBufferButton->text());
+
+	blog(LOG_INFO, REPLAY_BUFFER_STOP);
+
+	if (code == OBS_OUTPUT_UNSUPPORTED && isVisible()) {
+		QMessageBox::information(this,
+				QTStr("Output.RecordFail.Title"),
+				QTStr("Output.RecordFail.Unsupported"));
+
+	} else if (code == OBS_OUTPUT_NO_SPACE && isVisible()) {
+		QMessageBox::information(this,
+				QTStr("Output.RecordNoSpace.Title"),
+				QTStr("Output.RecordNoSpace.Msg"));
+
+	} else if (code != OBS_OUTPUT_SUCCESS && isVisible()) {
+		QMessageBox::information(this,
+				QTStr("Output.RecordError.Title"),
+				QTStr("Output.RecordError.Msg"));
+
+	} else if (code == OBS_OUTPUT_UNSUPPORTED && !isVisible()) {
+		SysTrayNotify(QTStr("Output.RecordFail.Unsupported"),
+			QSystemTrayIcon::Warning);
+
+	} else if (code == OBS_OUTPUT_NO_SPACE && !isVisible()) {
+		SysTrayNotify(QTStr("Output.RecordNoSpace.Msg"),
+			QSystemTrayIcon::Warning);
+
+	} else if (code != OBS_OUTPUT_SUCCESS && !isVisible()) {
+		SysTrayNotify(QTStr("Output.RecordError.Msg"),
+			QSystemTrayIcon::Warning);
+	}
+
+	if (api)
+		api->on_event(OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED);
+
+	OnDeactivate();
+}
+
 void OBSBasic::on_streamButton_clicked()
 {
 	if (outputHandler->StreamingActive()) {
@@ -4737,10 +4866,13 @@ void OBSBasic::SystemTrayInit()
 			trayIcon);
 	sysTrayRecord = new QAction(QTStr("Basic.Main.StartRecording"),
 			trayIcon);
+	sysTrayReplayBuffer = new QAction(QTStr("Basic.Main.StartReplayBuffer"),
+			trayIcon);
 	exit = new QAction(QTStr("Exit"),
 			trayIcon);
 
-	sysTrayRecord->setText(ui->recordButton->text());	
+	if (outputHandler && !outputHandler->replayBuffer)
+		sysTrayReplayBuffer->setEnabled(false);
 
 	connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
 			this,
@@ -4751,6 +4883,8 @@ void OBSBasic::SystemTrayInit()
 			this, SLOT(on_streamButton_clicked()));
 	connect(sysTrayRecord, SIGNAL(triggered()),
 			this, SLOT(on_recordButton_clicked()));
+	connect(sysTrayReplayBuffer, &QAction::triggered,
+			this, &OBSBasic::ReplayBufferClicked);
 	connect(exit, SIGNAL(triggered()),
 			this, SLOT(close()));
 
@@ -4758,6 +4892,7 @@ void OBSBasic::SystemTrayInit()
 	trayMenu->addAction(showHide);
 	trayMenu->addAction(sysTrayStream);
 	trayMenu->addAction(sysTrayRecord);
+	trayMenu->addAction(sysTrayReplayBuffer);
 	trayMenu->addAction(exit);
 	trayIcon->setContextMenu(trayMenu);
 }

+ 15 - 1
UI/window-basic-main.hpp

@@ -131,6 +131,7 @@ private:
 	std::unique_ptr<BasicOutputHandler> outputHandler;
 	bool streamingStopping = false;
 	bool recordingStopping = false;
+	bool replayBufferStopping = false;
 
 	gs_vertbuffer_t *box = nullptr;
 	gs_vertbuffer_t *boxLeft = nullptr;
@@ -152,9 +153,12 @@ private:
 
 	QPointer<QMenu> startStreamMenu;
 
+	QPointer<QPushButton> replayBufferButton;
+
 	QPointer<QSystemTrayIcon> trayIcon;
 	QPointer<QAction>         sysTrayStream;
 	QPointer<QAction>         sysTrayRecord;
+	QPointer<QAction>         sysTrayReplayBuffer;
 	QPointer<QAction>         showHide;
 	QPointer<QAction>         exit;
 	QPointer<QMenu>           trayMenu;
@@ -245,7 +249,8 @@ private:
 
 	QListWidgetItem *GetTopSelectedSourceItem();
 
-	obs_hotkey_pair_id streamingHotkeys, recordingHotkeys;
+	obs_hotkey_pair_id streamingHotkeys, recordingHotkeys,
+	                   replayBufHotkeys;
 	obs_hotkey_id forceStreamingStopHotkey;
 
 	void InitDefaultTransitions();
@@ -314,6 +319,8 @@ private:
 	void dragMoveEvent(QDragMoveEvent *event) override;
 	void dropEvent(QDropEvent *event) override;
 
+	void ReplayBufferClicked();
+
 public slots:
 	void StartStreaming();
 	void StopStreaming();
@@ -333,6 +340,13 @@ public slots:
 	void RecordStopping();
 	void RecordingStop(int code);
 
+	void StartReplayBuffer();
+	void StopReplayBuffer();
+
+	void ReplayBufferStart();
+	void ReplayBufferStopping();
+	void ReplayBufferStop(int code);
+
 	void SaveProjectDeferred();
 	void SaveProject();
 

+ 5 - 2
UI/window-basic-settings.cpp

@@ -2663,10 +2663,13 @@ void OBSBasicSettings::SaveHotkeySettings()
 		obs_data_array_release(array);
 	}
 
-	const char *id = obs_obj_get_id(main->outputHandler->fileOutput);
+	if (!main->outputHandler || !main->outputHandler->replayBuffer)
+		return;
+
+	const char *id = obs_obj_get_id(main->outputHandler->replayBuffer);
 	if (strcmp(id, "replay_buffer") == 0) {
 		obs_data_t *hotkeys = obs_hotkeys_save_output(
-				main->outputHandler->fileOutput);
+				main->outputHandler->replayBuffer);
 		config_set_string(config, "Hotkeys", "ReplayBuffer",
 				obs_data_get_json(hotkeys));
 		obs_data_release(hotkeys);