Просмотр исходного кода

UI: Add Virtual Camera source selector dialog

Chip Bradford 3 лет назад
Родитель
Сommit
df446c3f6e

+ 3 - 0
UI/CMakeLists.txt

@@ -103,6 +103,7 @@ target_sources(
           forms/OBSBasicSettings.ui
           forms/OBSBasicSettings.ui
           forms/OBSBasicSourceSelect.ui
           forms/OBSBasicSourceSelect.ui
           forms/OBSBasicTransform.ui
           forms/OBSBasicTransform.ui
+          forms/OBSBasicVCamConfig.ui
           forms/OBSExtraBrowsers.ui
           forms/OBSExtraBrowsers.ui
           forms/OBSImporter.ui
           forms/OBSImporter.ui
           forms/OBSLogReply.ui
           forms/OBSLogReply.ui
@@ -257,6 +258,8 @@ target_sources(
           window-basic-transform.cpp
           window-basic-transform.cpp
           window-basic-transform.hpp
           window-basic-transform.hpp
           window-basic-preview.hpp
           window-basic-preview.hpp
+          window-basic-vcam-config.cpp
+          window-basic-vcam-config.hpp
           window-dock.cpp
           window-dock.cpp
           window-dock.hpp
           window-dock.hpp
           window-importer.cpp
           window-importer.cpp

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

@@ -700,6 +700,17 @@ Basic.Main.Ungroup="Ungroup"
 Basic.Main.GridMode="Grid Mode"
 Basic.Main.GridMode="Grid Mode"
 Basic.Main.ListMode="List Mode"
 Basic.Main.ListMode="List Mode"
 
 
+# virtual camera configuration
+Basic.Main.VirtualCamConfig="Configure Virtual Camera"
+Basic.VCam.VirtualCamera="Virtual Camera"
+Basic.VCam.OutputType="Output Type"
+Basic.VCam.OutputSelection="Output Selection"
+Basic.VCam.Internal="Internal"
+Basic.VCam.InternalDefault="Program Output (Default)"
+Basic.VCam.InternalPreview="Preview Output"
+Basic.VCam.Start="Start"
+Basic.VCam.Update="Update"
+
 # basic mode file menu
 # basic mode file menu
 Basic.MainMenu.File="&File"
 Basic.MainMenu.File="&File"
 Basic.MainMenu.File.Export="&Export"
 Basic.MainMenu.File.Export="&Export"

+ 113 - 0
UI/forms/OBSBasicVCamConfig.ui

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OBSBasicVCamConfig</class>
+ <widget class="QDialog" name="OBSBasicVCamConfig">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>170</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Basic.VCam.VirtualCamera</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QLabel" name="outputTypeLabel">
+     <property name="text">
+      <string>Basic.VCam.OutputType</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QComboBox" name="outputType">
+     <item>
+      <property name="text">
+       <string>Basic.VCam.Internal</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>Basic.Scene</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>Basic.Main.Source</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="outputSelectionLabel">
+     <property name="text">
+      <string>Basic.VCam.OutputSelection</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QComboBox" name="outputSelection"/>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>OBSBasicVCamConfig</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>OBSBasicVCamConfig</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>

+ 133 - 9
UI/record-button.cpp

@@ -18,19 +18,143 @@ void RecordButton::resizeEvent(QResizeEvent *event)
 	event->accept();
 	event->accept();
 }
 }
 
 
-void ReplayBufferButton::resizeEvent(QResizeEvent *event)
+static QWidget *firstWidget(QLayoutItem *item)
 {
 {
+	auto widget = item->widget();
+	if (widget)
+		return widget;
+
+	auto layout = item->layout();
+	if (!layout)
+		return nullptr;
+
+	auto n = layout->count();
+	for (auto i = 0, n = layout->count(); i < n; i++) {
+		widget = firstWidget(layout->itemAt(i));
+		if (widget)
+			return widget;
+	}
+	return nullptr;
+}
+
+static QWidget *lastWidget(QLayoutItem *item)
+{
+	auto widget = item->widget();
+	if (widget)
+		return widget;
+
+	auto layout = item->layout();
+	if (!layout)
+		return nullptr;
+
+	auto n = layout->count();
+	for (auto i = layout->count(); i > 0; i--) {
+		widget = lastWidget(layout->itemAt(i - 1));
+		if (widget)
+			return widget;
+	}
+	return nullptr;
+}
+
+static QWidget *getNextWidget(QBoxLayout *container, QLayoutItem *item)
+{
+	for (auto i = 1, n = container->count(); i < n; i++) {
+		if (container->itemAt(i - 1) == item)
+			return firstWidget(container->itemAt(i));
+	}
+	return nullptr;
+}
+
+ControlsSplitButton::ControlsSplitButton(const QString &text,
+					 const QVariant &themeID,
+					 void (OBSBasic::*clicked)())
+	: QHBoxLayout(OBSBasic::Get())
+{
+	button.reset(new QPushButton(text));
+	button->setCheckable(true);
+	button->setProperty("themeID", themeID);
+
+	button->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
+	button->installEventFilter(this);
+
 	OBSBasic *main = OBSBasic::Get();
 	OBSBasic *main = OBSBasic::Get();
-	if (!main->replay)
-		return;
+	connect(button.data(), &QPushButton::clicked, main, clicked);
 
 
-	QSize replaySize = main->replay->size();
-	int height = main->ui->recordButton->size().height();
+	addWidget(button.data());
+}
+
+void ControlsSplitButton::addIcon(const QString &name, const QVariant &themeID,
+				  void (OBSBasic::*clicked)())
+{
+	icon.reset(new QPushButton());
+	icon->setAccessibleName(name);
+	icon->setToolTip(name);
+	icon->setChecked(false);
+	icon->setProperty("themeID", themeID);
 
 
-	if (replaySize.height() != height || replaySize.width() != height) {
-		main->replay->setMinimumSize(height, height);
-		main->replay->setMaximumSize(height, height);
+	QSizePolicy sp;
+	sp.setHeightForWidth(true);
+	icon->setSizePolicy(sp);
+
+	OBSBasic *main = OBSBasic::Get();
+	connect(icon.data(), &QAbstractButton::clicked, main, clicked);
+
+	addWidget(icon.data());
+	QWidget::setTabOrder(button.data(), icon.data());
+
+	auto next = getNextWidget(main->ui->buttonsVLayout, this);
+	if (next)
+		QWidget::setTabOrder(icon.data(), next);
+}
+
+void ControlsSplitButton::removeIcon()
+{
+	icon.reset();
+}
+
+void ControlsSplitButton::insert(int index)
+{
+	OBSBasic *main = OBSBasic::Get();
+	auto count = main->ui->buttonsVLayout->count();
+	if (index < 0)
+		index = 0;
+	else if (index > count)
+		index = count;
+
+	main->ui->buttonsVLayout->insertLayout(index, this);
+
+	QWidget *prev = button.data();
+
+	if (index > 0) {
+		prev = lastWidget(main->ui->buttonsVLayout->itemAt(index - 1));
+		if (prev)
+			QWidget::setTabOrder(prev, button.data());
+		prev = button.data();
 	}
 	}
 
 
-	event->accept();
+	if (icon) {
+		QWidget::setTabOrder(button.data(), icon.data());
+		prev = icon.data();
+	}
+
+	if (index < count) {
+		auto next = firstWidget(
+			main->ui->buttonsVLayout->itemAt(index + 1));
+		if (next)
+			QWidget::setTabOrder(prev, next);
+	}
+}
+
+bool ControlsSplitButton::eventFilter(QObject *obj, QEvent *event)
+{
+	if (event->type() == QEvent::Resize && icon) {
+		QSize iconSize = icon->size();
+		int height = button->height();
+
+		if (iconSize.height() != height || iconSize.width() != height) {
+			icon->setMinimumSize(height, height);
+			icon->setMaximumSize(height, height);
+		}
+	}
+	return QObject::eventFilter(obj, event);
 }
 }

+ 21 - 7
UI/record-button.hpp

@@ -1,6 +1,8 @@
 #pragma once
 #pragma once
 
 
 #include <QPushButton>
 #include <QPushButton>
+#include <QBoxLayout>
+#include <QScopedPointer>
 
 
 class RecordButton : public QPushButton {
 class RecordButton : public QPushButton {
 	Q_OBJECT
 	Q_OBJECT
@@ -11,15 +13,27 @@ public:
 	virtual void resizeEvent(QResizeEvent *event) override;
 	virtual void resizeEvent(QResizeEvent *event) override;
 };
 };
 
 
-class ReplayBufferButton : public QPushButton {
+class OBSBasic;
+
+class ControlsSplitButton : public QHBoxLayout {
 	Q_OBJECT
 	Q_OBJECT
 
 
 public:
 public:
-	inline ReplayBufferButton(const QString &text,
-				  QWidget *parent = nullptr)
-		: QPushButton(text, parent)
-	{
-	}
+	ControlsSplitButton(const QString &text, const QVariant &themeID,
+			    void (OBSBasic::*clicked)());
 
 
-	virtual void resizeEvent(QResizeEvent *event) override;
+	void addIcon(const QString &name, const QVariant &themeID,
+		     void (OBSBasic::*clicked)());
+	void removeIcon();
+	void insert(int index);
+
+	inline QPushButton *first() { return button.data(); }
+	inline QPushButton *second() { return icon.data(); }
+
+protected:
+	virtual bool eventFilter(QObject *obj, QEvent *event) override;
+
+private:
+	QScopedPointer<QPushButton> button;
+	QScopedPointer<QPushButton> icon;
 };
 };

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

@@ -5,6 +5,7 @@
 #include "audio-encoders.hpp"
 #include "audio-encoders.hpp"
 #include "window-basic-main.hpp"
 #include "window-basic-main.hpp"
 #include "window-basic-main-outputs.hpp"
 #include "window-basic-main-outputs.hpp"
+#include "window-basic-vcam-config.hpp"
 
 
 using namespace std;
 using namespace std;
 
 
@@ -178,6 +179,9 @@ static void OBSStopVirtualCam(void *data, calldata_t *params)
 	os_atomic_set_bool(&virtualcam_active, false);
 	os_atomic_set_bool(&virtualcam_active, false);
 	QMetaObject::invokeMethod(output->main, "OnVirtualCamStop",
 	QMetaObject::invokeMethod(output->main, "OnVirtualCamStop",
 				  Q_ARG(int, code));
 				  Q_ARG(int, code));
+
+	obs_output_set_media(output->virtualCam, nullptr, nullptr);
+	OBSBasicVCamConfig::StopVideo();
 }
 }
 
 
 /* ------------------------------------------------------------------------ */
 /* ------------------------------------------------------------------------ */
@@ -226,8 +230,11 @@ inline BasicOutputHandler::BasicOutputHandler(OBSBasic *main_) : main(main_)
 bool BasicOutputHandler::StartVirtualCam()
 bool BasicOutputHandler::StartVirtualCam()
 {
 {
 	if (main->vcamEnabled) {
 	if (main->vcamEnabled) {
-		obs_output_set_media(virtualCam, obs_get_video(),
-				     obs_get_audio());
+		video_t *video = OBSBasicVCamConfig::StartVideo();
+		if (!video)
+			return false;
+
+		obs_output_set_media(virtualCam, video, obs_get_audio());
 		if (!Active())
 		if (!Active())
 			SetupOutputs();
 			SetupOutputs();
 
 

+ 4 - 0
UI/window-basic-main-transitions.cpp

@@ -21,6 +21,7 @@
 #include <QMessageBox>
 #include <QMessageBox>
 #include <util/dstr.hpp>
 #include <util/dstr.hpp>
 #include "window-basic-main.hpp"
 #include "window-basic-main.hpp"
+#include "window-basic-vcam-config.hpp"
 #include "display-helpers.hpp"
 #include "display-helpers.hpp"
 #include "window-namedialog.hpp"
 #include "window-namedialog.hpp"
 #include "menu-button.hpp"
 #include "menu-button.hpp"
@@ -283,6 +284,9 @@ void OBSBasic::OverrideTransition(OBSSource transition)
 		obs_transition_swap_begin(transition, oldTransition);
 		obs_transition_swap_begin(transition, oldTransition);
 		obs_set_output_source(0, transition);
 		obs_set_output_source(0, transition);
 		obs_transition_swap_end(transition, oldTransition);
 		obs_transition_swap_end(transition, oldTransition);
+
+		// Transition overrides don't raise an event so we need to call update directly
+		OBSBasicVCamConfig::UpdateOutputSource();
 	}
 	}
 }
 }
 
 

+ 49 - 65
UI/window-basic-main.cpp

@@ -52,6 +52,7 @@
 #include "window-basic-main.hpp"
 #include "window-basic-main.hpp"
 #include "window-basic-stats.hpp"
 #include "window-basic-stats.hpp"
 #include "window-basic-main-outputs.hpp"
 #include "window-basic-main-outputs.hpp"
+#include "window-basic-vcam-config.hpp"
 #include "window-log-reply.hpp"
 #include "window-log-reply.hpp"
 #include "window-projector.hpp"
 #include "window-projector.hpp"
 #include "window-remux.hpp"
 #include "window-remux.hpp"
@@ -1622,16 +1623,15 @@ void OBSBasic::ReplayBufferClicked()
 
 
 void OBSBasic::AddVCamButton()
 void OBSBasic::AddVCamButton()
 {
 {
-	vcamButton = new ReplayBufferButton(QTStr("Basic.Main.StartVirtualCam"),
-					    this);
-	vcamButton->setCheckable(true);
-	connect(vcamButton.data(), &QPushButton::clicked, this,
-		&OBSBasic::VCamButtonClicked);
+	OBSBasicVCamConfig::Init();
 
 
-	vcamButton->setProperty("themeID", "vcamButton");
-	ui->buttonsVLayout->insertWidget(2, vcamButton);
-	setTabOrder(ui->recordButton, vcamButton);
-	setTabOrder(vcamButton, ui->modeSwitch);
+	vcamButton = new ControlsSplitButton(
+		QTStr("Basic.Main.StartVirtualCam"), "vcamButton",
+		&OBSBasic::VCamButtonClicked);
+	vcamButton->addIcon(QTStr("Basic.Main.VirtualCamConfig"),
+			    QStringLiteral("configIconSmall"),
+			    &OBSBasic::VCamConfigButtonClicked);
+	vcamButton->insert(2);
 }
 }
 
 
 void OBSBasic::ResetOutputs()
 void OBSBasic::ResetOutputs()
@@ -1647,28 +1647,13 @@ void OBSBasic::ResetOutputs()
 					   : CreateSimpleOutputHandler(this));
 					   : CreateSimpleOutputHandler(this));
 
 
 		delete replayBufferButton;
 		delete replayBufferButton;
-		delete replayLayout;
 
 
 		if (outputHandler->replayBuffer) {
 		if (outputHandler->replayBuffer) {
-			replayBufferButton = new ReplayBufferButton(
-				QTStr("Basic.Main.StartReplayBuffer"), this);
-			replayBufferButton->setCheckable(true);
-			connect(replayBufferButton.data(),
-				&QPushButton::clicked, this,
+			replayBufferButton = new ControlsSplitButton(
+				QTStr("Basic.Main.StartReplayBuffer"),
+				"replayBufferButton",
 				&OBSBasic::ReplayBufferClicked);
 				&OBSBasic::ReplayBufferClicked);
-
-			replayBufferButton->setSizePolicy(QSizePolicy::Ignored,
-							  QSizePolicy::Fixed);
-
-			replayLayout = new QHBoxLayout(this);
-			replayLayout->addWidget(replayBufferButton);
-
-			replayBufferButton->setProperty("themeID",
-							"replayBufferButton");
-			ui->buttonsVLayout->insertLayout(2, replayLayout);
-			setTabOrder(ui->recordButton, replayBufferButton);
-			setTabOrder(replayBufferButton,
-				    ui->buttonsVLayout->itemAt(3)->widget());
+			replayBufferButton->insert(2);
 		}
 		}
 
 
 		if (sysTrayReplayBuffer)
 		if (sysTrayReplayBuffer)
@@ -7257,19 +7242,19 @@ void OBSBasic::StartReplayBuffer()
 		return;
 		return;
 
 
 	if (!UIValidation::NoSourcesConfirmation(this)) {
 	if (!UIValidation::NoSourcesConfirmation(this)) {
-		replayBufferButton->setChecked(false);
+		replayBufferButton->first()->setChecked(false);
 		return;
 		return;
 	}
 	}
 
 
 	if (!OutputPathValid()) {
 	if (!OutputPathValid()) {
 		OutputPathInvalidMessage();
 		OutputPathInvalidMessage();
-		replayBufferButton->setChecked(false);
+		replayBufferButton->first()->setChecked(false);
 		return;
 		return;
 	}
 	}
 
 
 	if (LowDiskSpace()) {
 	if (LowDiskSpace()) {
 		DiskSpaceMessage();
 		DiskSpaceMessage();
-		replayBufferButton->setChecked(false);
+		replayBufferButton->first()->setChecked(false);
 		return;
 		return;
 	}
 	}
 
 
@@ -7279,7 +7264,7 @@ void OBSBasic::StartReplayBuffer()
 	SaveProject();
 	SaveProject();
 
 
 	if (!outputHandler->StartReplayBuffer()) {
 	if (!outputHandler->StartReplayBuffer()) {
-		replayBufferButton->setChecked(false);
+		replayBufferButton->first()->setChecked(false);
 	} else if (os_atomic_load_bool(&recording_paused)) {
 	} else if (os_atomic_load_bool(&recording_paused)) {
 		ShowReplayBufferPauseWarning();
 		ShowReplayBufferPauseWarning();
 	}
 	}
@@ -7290,10 +7275,12 @@ void OBSBasic::ReplayBufferStopping()
 	if (!outputHandler || !outputHandler->replayBuffer)
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 		return;
 
 
-	replayBufferButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
+	replayBufferButton->first()->setText(
+		QTStr("Basic.Main.StoppingReplayBuffer"));
 
 
 	if (sysTrayReplayBuffer)
 	if (sysTrayReplayBuffer)
-		sysTrayReplayBuffer->setText(replayBufferButton->text());
+		sysTrayReplayBuffer->setText(
+			replayBufferButton->first()->text());
 
 
 	replayBufferStopping = true;
 	replayBufferStopping = true;
 	if (api)
 	if (api)
@@ -7318,11 +7305,13 @@ void OBSBasic::ReplayBufferStart()
 	if (!outputHandler || !outputHandler->replayBuffer)
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 		return;
 
 
-	replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
-	replayBufferButton->setChecked(true);
+	replayBufferButton->first()->setText(
+		QTStr("Basic.Main.StopReplayBuffer"));
+	replayBufferButton->first()->setChecked(true);
 
 
 	if (sysTrayReplayBuffer)
 	if (sysTrayReplayBuffer)
-		sysTrayReplayBuffer->setText(replayBufferButton->text());
+		sysTrayReplayBuffer->setText(
+			replayBufferButton->first()->text());
 
 
 	replayBufferStopping = false;
 	replayBufferStopping = false;
 	if (api)
 	if (api)
@@ -7375,11 +7364,13 @@ void OBSBasic::ReplayBufferStop(int code)
 	if (!outputHandler || !outputHandler->replayBuffer)
 	if (!outputHandler || !outputHandler->replayBuffer)
 		return;
 		return;
 
 
-	replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
-	replayBufferButton->setChecked(false);
+	replayBufferButton->first()->setText(
+		QTStr("Basic.Main.StartReplayBuffer"));
+	replayBufferButton->first()->setChecked(false);
 
 
 	if (sysTrayReplayBuffer)
 	if (sysTrayReplayBuffer)
-		sysTrayReplayBuffer->setText(replayBufferButton->text());
+		sysTrayReplayBuffer->setText(
+			replayBufferButton->first()->text());
 
 
 	blog(LOG_INFO, REPLAY_BUFFER_STOP);
 	blog(LOG_INFO, REPLAY_BUFFER_STOP);
 
 
@@ -7428,7 +7419,7 @@ void OBSBasic::StartVirtualCam()
 	SaveProject();
 	SaveProject();
 
 
 	if (!outputHandler->StartVirtualCam()) {
 	if (!outputHandler->StartVirtualCam()) {
-		vcamButton->setChecked(false);
+		vcamButton->first()->setChecked(false);
 	}
 	}
 }
 }
 
 
@@ -7450,10 +7441,10 @@ void OBSBasic::OnVirtualCamStart()
 	if (!outputHandler || !outputHandler->virtualCam)
 	if (!outputHandler || !outputHandler->virtualCam)
 		return;
 		return;
 
 
-	vcamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
+	vcamButton->first()->setText(QTStr("Basic.Main.StopVirtualCam"));
 	if (sysTrayVirtualCam)
 	if (sysTrayVirtualCam)
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
-	vcamButton->setChecked(true);
+	vcamButton->first()->setChecked(true);
 
 
 	if (api)
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
@@ -7468,10 +7459,10 @@ void OBSBasic::OnVirtualCamStop(int)
 	if (!outputHandler || !outputHandler->virtualCam)
 	if (!outputHandler || !outputHandler->virtualCam)
 		return;
 		return;
 
 
-	vcamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
+	vcamButton->first()->setText(QTStr("Basic.Main.StartVirtualCam"));
 	if (sysTrayVirtualCam)
 	if (sysTrayVirtualCam)
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
 		sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
-	vcamButton->setChecked(false);
+	vcamButton->first()->setChecked(false);
 
 
 	if (api)
 	if (api)
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
 		api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
@@ -7623,7 +7614,7 @@ void OBSBasic::VCamButtonClicked()
 		StopVirtualCam();
 		StopVirtualCam();
 	} else {
 	} else {
 		if (!UIValidation::NoSourcesConfirmation(this)) {
 		if (!UIValidation::NoSourcesConfirmation(this)) {
-			vcamButton->setChecked(false);
+			vcamButton->first()->setChecked(false);
 			return;
 			return;
 		}
 		}
 
 
@@ -7631,6 +7622,12 @@ void OBSBasic::VCamButtonClicked()
 	}
 	}
 }
 }
 
 
+void OBSBasic::VCamConfigButtonClicked()
+{
+	OBSBasicVCamConfig config(this);
+	config.exec();
+}
+
 void OBSBasic::on_settingsButton_clicked()
 void OBSBasic::on_settingsButton_clicked()
 {
 {
 	on_action_Settings_triggered();
 	on_action_Settings_triggered();
@@ -9757,6 +9754,7 @@ void OBSBasic::PauseRecording()
 
 
 		os_atomic_set_bool(&recording_paused, true);
 		os_atomic_set_bool(&recording_paused, true);
 
 
+		auto replay = replayBufferButton->second();
 		if (replay)
 		if (replay)
 			replay->setEnabled(false);
 			replay->setEnabled(false);
 
 
@@ -9801,6 +9799,7 @@ void OBSBasic::UnpauseRecording()
 
 
 		os_atomic_set_bool(&recording_paused, false);
 		os_atomic_set_bool(&recording_paused, false);
 
 
+		auto replay = replayBufferButton->second();
 		if (replay)
 		if (replay)
 			replay->setEnabled(true);
 			replay->setEnabled(true);
 
 
@@ -9877,28 +9876,13 @@ void OBSBasic::UpdateReplayBuffer(bool activate)
 {
 {
 	if (!activate || !outputHandler ||
 	if (!activate || !outputHandler ||
 	    !outputHandler->ReplayBufferActive()) {
 	    !outputHandler->ReplayBufferActive()) {
-		replay.reset();
+		replayBufferButton->removeIcon();
 		return;
 		return;
 	}
 	}
 
 
-	replay.reset(new QPushButton());
-	replay->setAccessibleName(QTStr("Basic.Main.SaveReplay"));
-	replay->setToolTip(QTStr("Basic.Main.SaveReplay"));
-	replay->setChecked(false);
-	replay->setProperty("themeID",
-			    QVariant(QStringLiteral("replayIconSmall")));
-
-	QSizePolicy sp;
-	sp.setHeightForWidth(true);
-	replay->setSizePolicy(sp);
-
-	connect(replay.data(), &QAbstractButton::clicked, this,
-		&OBSBasic::ReplayBufferSave);
-	replayLayout->addWidget(replay.data());
-	setTabOrder(replayLayout->itemAt(0)->widget(),
-		    replayLayout->itemAt(1)->widget());
-	setTabOrder(replayLayout->itemAt(1)->widget(),
-		    ui->buttonsVLayout->itemAt(3)->widget());
+	replayBufferButton->addIcon(QTStr("Basic.Main.SaveReplay"),
+				    QStringLiteral("replayIconSmall"),
+				    &OBSBasic::ReplayBufferSave);
 }
 }
 
 
 #define MBYTE (1024ULL * 1024ULL)
 #define MBYTE (1024ULL * 1024ULL)

+ 4 - 5
UI/window-basic-main.hpp

@@ -178,7 +178,7 @@ class OBSBasic : public OBSMainWindow {
 	friend class AutoConfig;
 	friend class AutoConfig;
 	friend class AutoConfigStreamPage;
 	friend class AutoConfigStreamPage;
 	friend class RecordButton;
 	friend class RecordButton;
-	friend class ReplayBufferButton;
+	friend class ControlsSplitButton;
 	friend class ExtraBrowsersModel;
 	friend class ExtraBrowsersModel;
 	friend class ExtraBrowsersDelegate;
 	friend class ExtraBrowsersDelegate;
 	friend class DeviceCaptureToolbar;
 	friend class DeviceCaptureToolbar;
@@ -298,12 +298,10 @@ private:
 	QPointer<QMenu> startStreamMenu;
 	QPointer<QMenu> startStreamMenu;
 
 
 	QPointer<QPushButton> transitionButton;
 	QPointer<QPushButton> transitionButton;
-	QPointer<QPushButton> replayBufferButton;
-	QPointer<QHBoxLayout> replayLayout;
+	QPointer<ControlsSplitButton> replayBufferButton;
 	QScopedPointer<QPushButton> pause;
 	QScopedPointer<QPushButton> pause;
-	QScopedPointer<QPushButton> replay;
 
 
-	QPointer<QPushButton> vcamButton;
+	QPointer<ControlsSplitButton> vcamButton;
 	bool vcamEnabled = false;
 	bool vcamEnabled = false;
 
 
 	QScopedPointer<QSystemTrayIcon> trayIcon;
 	QScopedPointer<QSystemTrayIcon> trayIcon;
@@ -1038,6 +1036,7 @@ private slots:
 	void on_streamButton_clicked();
 	void on_streamButton_clicked();
 	void on_recordButton_clicked();
 	void on_recordButton_clicked();
 	void VCamButtonClicked();
 	void VCamButtonClicked();
+	void VCamConfigButtonClicked();
 	void on_settingsButton_clicked();
 	void on_settingsButton_clicked();
 	void Screenshot(OBSSource source_ = nullptr);
 	void Screenshot(OBSSource source_ = nullptr);
 	void ScreenshotSelectedSource();
 	void ScreenshotSelectedSource();

+ 264 - 0
UI/window-basic-vcam-config.cpp

@@ -0,0 +1,264 @@
+#include "window-basic-vcam-config.hpp"
+#include "window-basic-main.hpp"
+#include "qt-wrappers.hpp"
+#include "remote-text.hpp"
+#include <util/util.hpp>
+#include <util/platform.h>
+#include <platform.hpp>
+#include <mutex>
+
+using namespace std;
+
+enum class VCamOutputType {
+	Internal,
+	Scene,
+	Source,
+};
+
+enum class VCamInternalType {
+	Default,
+	Preview,
+};
+
+struct VCamConfig {
+	VCamOutputType type = VCamOutputType::Internal;
+	VCamInternalType internal = VCamInternalType::Default;
+	string scene;
+	string source;
+};
+
+static VCamConfig *vCamConfig = nullptr;
+
+OBSBasicVCamConfig::OBSBasicVCamConfig(QWidget *parent)
+	: QDialog(parent), ui(new Ui::OBSBasicVCamConfig)
+{
+	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+	ui->setupUi(this);
+
+	auto type = (int)vCamConfig->type;
+	ui->outputType->setCurrentIndex(type);
+	OutputTypeChanged(type);
+	connect(ui->outputType,
+		static_cast<void (QComboBox::*)(int)>(
+			&QComboBox::currentIndexChanged),
+		this, &OBSBasicVCamConfig::OutputTypeChanged);
+
+	auto start = ui->buttonBox->button(QDialogButtonBox::Ok);
+	if (!obs_frontend_virtualcam_active())
+		start->setText(QTStr("Basic.VCam.Start"));
+	else
+		start->setText(QTStr("Basic.VCam.Update"));
+	connect(start, &QPushButton::clicked, this,
+		&OBSBasicVCamConfig::SaveAndStart);
+}
+
+void OBSBasicVCamConfig::OutputTypeChanged(int type)
+{
+	auto list = ui->outputSelection;
+	list->clear();
+
+	switch ((VCamOutputType)type) {
+	case VCamOutputType::Internal:
+		list->addItem(QTStr("Basic.VCam.InternalDefault"));
+		list->addItem(QTStr("Basic.VCam.InternalPreview"));
+		list->setCurrentIndex((int)vCamConfig->internal);
+		break;
+
+	case VCamOutputType::Scene: {
+		// Scenes in default order
+		BPtr<char *> scenes = obs_frontend_get_scene_names();
+		int idx = 0;
+		for (char **temp = scenes; *temp; temp++) {
+			list->addItem(*temp);
+
+			if (vCamConfig->scene.compare(*temp) == 0)
+				list->setCurrentIndex(list->count() - 1);
+		}
+		break;
+	}
+
+	case VCamOutputType::Source: {
+		// Sources in alphabetical order
+		vector<string> sources;
+		auto AddSource = [&](obs_source_t *source) {
+			auto name = obs_source_get_name(source);
+			auto flags = obs_source_get_output_flags(source);
+
+			if (!(obs_source_get_output_flags(source) &
+			      OBS_SOURCE_VIDEO))
+				return;
+
+			sources.push_back(name);
+		};
+		using AddSource_t = decltype(AddSource);
+
+		obs_enum_sources(
+			[](void *data, obs_source_t *source) {
+				auto &AddSource =
+					*static_cast<AddSource_t *>(data);
+				if (!obs_source_removed(source))
+					AddSource(source);
+				return true;
+			},
+			static_cast<void *>(&AddSource));
+
+		// Sort and select current item
+		sort(sources.begin(), sources.end());
+		for (auto &&source : sources) {
+			list->addItem(source.c_str());
+
+			if (vCamConfig->source == source)
+				list->setCurrentIndex(list->count() - 1);
+		}
+		break;
+	}
+	}
+}
+
+void OBSBasicVCamConfig::SaveAndStart()
+{
+	auto type = (VCamOutputType)ui->outputType->currentIndex();
+	auto out = ui->outputSelection;
+	switch (type) {
+	case VCamOutputType::Internal:
+		vCamConfig->internal = (VCamInternalType)out->currentIndex();
+		break;
+	case VCamOutputType::Scene:
+		vCamConfig->scene = out->currentText().toStdString();
+		break;
+	case VCamOutputType::Source:
+		vCamConfig->source = out->currentText().toStdString();
+		break;
+	default:
+		// unknown value, don't save type
+		return;
+	}
+
+	vCamConfig->type = type;
+
+	// Start the vcam if needed, if already running just update the source
+	if (!obs_frontend_virtualcam_active())
+		obs_frontend_start_virtualcam();
+	else
+		UpdateOutputSource();
+}
+
+static void SaveCallback(obs_data_t *data, bool saving, void *)
+{
+	if (saving) {
+		OBSDataAutoRelease obj = obs_data_create();
+
+		obs_data_set_int(obj, "type", (int)vCamConfig->type);
+		obs_data_set_int(obj, "internal", (int)vCamConfig->internal);
+		obs_data_set_string(obj, "scene", vCamConfig->scene.c_str());
+		obs_data_set_string(obj, "source", vCamConfig->source.c_str());
+
+		obs_data_set_obj(data, "virtual-camera", obj);
+	} else {
+		OBSDataAutoRelease obj =
+			obs_data_get_obj(data, "virtual-camera");
+
+		vCamConfig->type =
+			(VCamOutputType)obs_data_get_int(obj, "type");
+		vCamConfig->internal =
+			(VCamInternalType)obs_data_get_int(obj, "internal");
+		vCamConfig->scene = obs_data_get_string(obj, "scene");
+		vCamConfig->source = obs_data_get_string(obj, "source");
+	}
+}
+
+static void EventCallback(enum obs_frontend_event event, void *)
+{
+	if (vCamConfig->type != VCamOutputType::Internal)
+		return;
+
+	// Update output source if the preview scene changes
+	// or if the default transition is changed
+	switch (event) {
+	case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
+		if (vCamConfig->internal != VCamInternalType::Preview)
+			return;
+		break;
+	case OBS_FRONTEND_EVENT_TRANSITION_CHANGED:
+		if (vCamConfig->internal != VCamInternalType::Default)
+			return;
+		break;
+	default:
+		return;
+	}
+
+	OBSBasicVCamConfig::UpdateOutputSource();
+}
+
+void OBSBasicVCamConfig::Init()
+{
+	if (vCamConfig)
+		return;
+
+	vCamConfig = new VCamConfig;
+
+	obs_frontend_add_save_callback(SaveCallback, nullptr);
+	obs_frontend_add_event_callback(EventCallback, nullptr);
+}
+
+static obs_view_t *view = nullptr;
+static video_t *video = nullptr;
+
+video_t *OBSBasicVCamConfig::StartVideo()
+{
+	if (!video) {
+		view = obs_view_create();
+		video = obs_view_add(view);
+	}
+	UpdateOutputSource();
+	return video;
+}
+
+void OBSBasicVCamConfig::StopVideo()
+{
+	if (view) {
+		obs_view_remove(view);
+		obs_view_set_source(view, 0, nullptr);
+		obs_view_destroy(view);
+		view = nullptr;
+	}
+	video = nullptr;
+}
+
+void OBSBasicVCamConfig::UpdateOutputSource()
+{
+	if (!view)
+		return;
+
+	obs_source_t *source = nullptr;
+
+	switch ((VCamOutputType)vCamConfig->type) {
+	case VCamOutputType::Internal:
+		switch (vCamConfig->internal) {
+		case VCamInternalType::Default:
+			source = obs_get_output_source(0);
+			break;
+		case VCamInternalType::Preview:
+			OBSSource s = OBSBasic::Get()->GetCurrentSceneSource();
+			obs_source_get_ref(s);
+			source = s;
+			break;
+		}
+		break;
+
+	case VCamOutputType::Scene:
+		source = obs_get_source_by_name(vCamConfig->scene.c_str());
+		break;
+
+	case VCamOutputType::Source:
+		source = obs_get_source_by_name(vCamConfig->source.c_str());
+		break;
+	}
+
+	auto current = obs_view_get_source(view, 0);
+	if (source != current)
+		obs_view_set_source(view, 0, source);
+	obs_source_release(source);
+	obs_source_release(current);
+}

+ 27 - 0
UI/window-basic-vcam-config.hpp

@@ -0,0 +1,27 @@
+#pragma once
+
+#include <obs.hpp>
+#include <QDialog>
+#include <memory>
+
+#include "ui_OBSBasicVCamConfig.h"
+
+class OBSBasicVCamConfig : public QDialog {
+	Q_OBJECT
+
+public:
+	static void Init();
+
+	static video_t *StartVideo();
+	static void StopVideo();
+	static void UpdateOutputSource();
+
+	explicit OBSBasicVCamConfig(QWidget *parent = 0);
+
+private slots:
+	void OutputTypeChanged(int type);
+	void SaveAndStart();
+
+private:
+	std::unique_ptr<Ui::OBSBasicVCamConfig> ui;
+};