diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt
index 8e25b0d6c..1585a49d2 100644
--- a/UI/CMakeLists.txt
+++ b/UI/CMakeLists.txt
@@ -103,6 +103,7 @@ target_sources(
forms/OBSBasicSettings.ui
forms/OBSBasicSourceSelect.ui
forms/OBSBasicTransform.ui
+ forms/OBSBasicVCamConfig.ui
forms/OBSExtraBrowsers.ui
forms/OBSImporter.ui
forms/OBSLogReply.ui
@@ -257,6 +258,8 @@ target_sources(
window-basic-transform.cpp
window-basic-transform.hpp
window-basic-preview.hpp
+ window-basic-vcam-config.cpp
+ window-basic-vcam-config.hpp
window-dock.cpp
window-dock.hpp
window-importer.cpp
diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 3403d966c..98e8755dc 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -700,6 +700,17 @@ Basic.Main.Ungroup="Ungroup"
Basic.Main.GridMode="Grid 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.MainMenu.File="&File"
Basic.MainMenu.File.Export="&Export"
diff --git a/UI/forms/OBSBasicVCamConfig.ui b/UI/forms/OBSBasicVCamConfig.ui
new file mode 100644
index 000000000..bc255b159
--- /dev/null
+++ b/UI/forms/OBSBasicVCamConfig.ui
@@ -0,0 +1,113 @@
+
+
+ OBSBasicVCamConfig
+
+
+
+ 0
+ 0
+ 400
+ 170
+
+
+
+ Basic.VCam.VirtualCamera
+
+
+ -
+
+
+ Basic.VCam.OutputType
+
+
+
+ -
+
+
-
+
+ Basic.VCam.Internal
+
+
+ -
+
+ Basic.Scene
+
+
+ -
+
+ Basic.Main.Source
+
+
+
+
+ -
+
+
+ Basic.VCam.OutputSelection
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ OBSBasicVCamConfig
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ OBSBasicVCamConfig
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/UI/record-button.cpp b/UI/record-button.cpp
index 0e080a796..a0f028938 100644
--- a/UI/record-button.cpp
+++ b/UI/record-button.cpp
@@ -18,19 +18,143 @@ void RecordButton::resizeEvent(QResizeEvent *event)
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();
+ connect(button.data(), &QPushButton::clicked, main, clicked);
+
+ 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);
+
+ 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();
- if (!main->replay)
- return;
+ auto count = main->ui->buttonsVLayout->count();
+ if (index < 0)
+ index = 0;
+ else if (index > count)
+ index = count;
- QSize replaySize = main->replay->size();
- int height = main->ui->recordButton->size().height();
+ main->ui->buttonsVLayout->insertLayout(index, this);
- if (replaySize.height() != height || replaySize.width() != height) {
- main->replay->setMinimumSize(height, height);
- main->replay->setMaximumSize(height, height);
+ 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);
}
diff --git a/UI/record-button.hpp b/UI/record-button.hpp
index d6c083e21..ea8d40c7d 100644
--- a/UI/record-button.hpp
+++ b/UI/record-button.hpp
@@ -1,6 +1,8 @@
#pragma once
#include
+#include
+#include
class RecordButton : public QPushButton {
Q_OBJECT
@@ -11,15 +13,27 @@ public:
virtual void resizeEvent(QResizeEvent *event) override;
};
-class ReplayBufferButton : public QPushButton {
+class OBSBasic;
+
+class ControlsSplitButton : public QHBoxLayout {
Q_OBJECT
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 button;
+ QScopedPointer icon;
};
diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp
index dac421632..e423e176f 100644
--- a/UI/window-basic-main-outputs.cpp
+++ b/UI/window-basic-main-outputs.cpp
@@ -5,6 +5,7 @@
#include "audio-encoders.hpp"
#include "window-basic-main.hpp"
#include "window-basic-main-outputs.hpp"
+#include "window-basic-vcam-config.hpp"
using namespace std;
@@ -178,6 +179,9 @@ static void OBSStopVirtualCam(void *data, calldata_t *params)
os_atomic_set_bool(&virtualcam_active, false);
QMetaObject::invokeMethod(output->main, "OnVirtualCamStop",
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()
{
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())
SetupOutputs();
diff --git a/UI/window-basic-main-transitions.cpp b/UI/window-basic-main-transitions.cpp
index 8a8d709fe..0c86dd272 100644
--- a/UI/window-basic-main-transitions.cpp
+++ b/UI/window-basic-main-transitions.cpp
@@ -21,6 +21,7 @@
#include
#include
#include "window-basic-main.hpp"
+#include "window-basic-vcam-config.hpp"
#include "display-helpers.hpp"
#include "window-namedialog.hpp"
#include "menu-button.hpp"
@@ -283,6 +284,9 @@ void OBSBasic::OverrideTransition(OBSSource transition)
obs_transition_swap_begin(transition, oldTransition);
obs_set_output_source(0, transition);
obs_transition_swap_end(transition, oldTransition);
+
+ // Transition overrides don't raise an event so we need to call update directly
+ OBSBasicVCamConfig::UpdateOutputSource();
}
}
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index 7736e03bf..a244b5039 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -52,6 +52,7 @@
#include "window-basic-main.hpp"
#include "window-basic-stats.hpp"
#include "window-basic-main-outputs.hpp"
+#include "window-basic-vcam-config.hpp"
#include "window-log-reply.hpp"
#include "window-projector.hpp"
#include "window-remux.hpp"
@@ -1622,16 +1623,15 @@ void OBSBasic::ReplayBufferClicked()
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()
@@ -1647,28 +1647,13 @@ void OBSBasic::ResetOutputs()
: CreateSimpleOutputHandler(this));
delete replayBufferButton;
- delete replayLayout;
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);
-
- 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)
@@ -7257,19 +7242,19 @@ void OBSBasic::StartReplayBuffer()
return;
if (!UIValidation::NoSourcesConfirmation(this)) {
- replayBufferButton->setChecked(false);
+ replayBufferButton->first()->setChecked(false);
return;
}
if (!OutputPathValid()) {
OutputPathInvalidMessage();
- replayBufferButton->setChecked(false);
+ replayBufferButton->first()->setChecked(false);
return;
}
if (LowDiskSpace()) {
DiskSpaceMessage();
- replayBufferButton->setChecked(false);
+ replayBufferButton->first()->setChecked(false);
return;
}
@@ -7279,7 +7264,7 @@ void OBSBasic::StartReplayBuffer()
SaveProject();
if (!outputHandler->StartReplayBuffer()) {
- replayBufferButton->setChecked(false);
+ replayBufferButton->first()->setChecked(false);
} else if (os_atomic_load_bool(&recording_paused)) {
ShowReplayBufferPauseWarning();
}
@@ -7290,10 +7275,12 @@ void OBSBasic::ReplayBufferStopping()
if (!outputHandler || !outputHandler->replayBuffer)
return;
- replayBufferButton->setText(QTStr("Basic.Main.StoppingReplayBuffer"));
+ replayBufferButton->first()->setText(
+ QTStr("Basic.Main.StoppingReplayBuffer"));
if (sysTrayReplayBuffer)
- sysTrayReplayBuffer->setText(replayBufferButton->text());
+ sysTrayReplayBuffer->setText(
+ replayBufferButton->first()->text());
replayBufferStopping = true;
if (api)
@@ -7318,11 +7305,13 @@ void OBSBasic::ReplayBufferStart()
if (!outputHandler || !outputHandler->replayBuffer)
return;
- replayBufferButton->setText(QTStr("Basic.Main.StopReplayBuffer"));
- replayBufferButton->setChecked(true);
+ replayBufferButton->first()->setText(
+ QTStr("Basic.Main.StopReplayBuffer"));
+ replayBufferButton->first()->setChecked(true);
if (sysTrayReplayBuffer)
- sysTrayReplayBuffer->setText(replayBufferButton->text());
+ sysTrayReplayBuffer->setText(
+ replayBufferButton->first()->text());
replayBufferStopping = false;
if (api)
@@ -7375,11 +7364,13 @@ void OBSBasic::ReplayBufferStop(int code)
if (!outputHandler || !outputHandler->replayBuffer)
return;
- replayBufferButton->setText(QTStr("Basic.Main.StartReplayBuffer"));
- replayBufferButton->setChecked(false);
+ replayBufferButton->first()->setText(
+ QTStr("Basic.Main.StartReplayBuffer"));
+ replayBufferButton->first()->setChecked(false);
if (sysTrayReplayBuffer)
- sysTrayReplayBuffer->setText(replayBufferButton->text());
+ sysTrayReplayBuffer->setText(
+ replayBufferButton->first()->text());
blog(LOG_INFO, REPLAY_BUFFER_STOP);
@@ -7428,7 +7419,7 @@ void OBSBasic::StartVirtualCam()
SaveProject();
if (!outputHandler->StartVirtualCam()) {
- vcamButton->setChecked(false);
+ vcamButton->first()->setChecked(false);
}
}
@@ -7450,10 +7441,10 @@ void OBSBasic::OnVirtualCamStart()
if (!outputHandler || !outputHandler->virtualCam)
return;
- vcamButton->setText(QTStr("Basic.Main.StopVirtualCam"));
+ vcamButton->first()->setText(QTStr("Basic.Main.StopVirtualCam"));
if (sysTrayVirtualCam)
sysTrayVirtualCam->setText(QTStr("Basic.Main.StopVirtualCam"));
- vcamButton->setChecked(true);
+ vcamButton->first()->setChecked(true);
if (api)
api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STARTED);
@@ -7468,10 +7459,10 @@ void OBSBasic::OnVirtualCamStop(int)
if (!outputHandler || !outputHandler->virtualCam)
return;
- vcamButton->setText(QTStr("Basic.Main.StartVirtualCam"));
+ vcamButton->first()->setText(QTStr("Basic.Main.StartVirtualCam"));
if (sysTrayVirtualCam)
sysTrayVirtualCam->setText(QTStr("Basic.Main.StartVirtualCam"));
- vcamButton->setChecked(false);
+ vcamButton->first()->setChecked(false);
if (api)
api->on_event(OBS_FRONTEND_EVENT_VIRTUALCAM_STOPPED);
@@ -7623,7 +7614,7 @@ void OBSBasic::VCamButtonClicked()
StopVirtualCam();
} else {
if (!UIValidation::NoSourcesConfirmation(this)) {
- vcamButton->setChecked(false);
+ vcamButton->first()->setChecked(false);
return;
}
@@ -7631,6 +7622,12 @@ void OBSBasic::VCamButtonClicked()
}
}
+void OBSBasic::VCamConfigButtonClicked()
+{
+ OBSBasicVCamConfig config(this);
+ config.exec();
+}
+
void OBSBasic::on_settingsButton_clicked()
{
on_action_Settings_triggered();
@@ -9757,6 +9754,7 @@ void OBSBasic::PauseRecording()
os_atomic_set_bool(&recording_paused, true);
+ auto replay = replayBufferButton->second();
if (replay)
replay->setEnabled(false);
@@ -9801,6 +9799,7 @@ void OBSBasic::UnpauseRecording()
os_atomic_set_bool(&recording_paused, false);
+ auto replay = replayBufferButton->second();
if (replay)
replay->setEnabled(true);
@@ -9877,28 +9876,13 @@ void OBSBasic::UpdateReplayBuffer(bool activate)
{
if (!activate || !outputHandler ||
!outputHandler->ReplayBufferActive()) {
- replay.reset();
+ replayBufferButton->removeIcon();
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)
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index d16dad7cd..ff9c6ac9b 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -178,7 +178,7 @@ class OBSBasic : public OBSMainWindow {
friend class AutoConfig;
friend class AutoConfigStreamPage;
friend class RecordButton;
- friend class ReplayBufferButton;
+ friend class ControlsSplitButton;
friend class ExtraBrowsersModel;
friend class ExtraBrowsersDelegate;
friend class DeviceCaptureToolbar;
@@ -298,12 +298,10 @@ private:
QPointer startStreamMenu;
QPointer transitionButton;
- QPointer replayBufferButton;
- QPointer replayLayout;
+ QPointer replayBufferButton;
QScopedPointer pause;
- QScopedPointer replay;
- QPointer vcamButton;
+ QPointer vcamButton;
bool vcamEnabled = false;
QScopedPointer trayIcon;
@@ -1038,6 +1036,7 @@ private slots:
void on_streamButton_clicked();
void on_recordButton_clicked();
void VCamButtonClicked();
+ void VCamConfigButtonClicked();
void on_settingsButton_clicked();
void Screenshot(OBSSource source_ = nullptr);
void ScreenshotSelectedSource();
diff --git a/UI/window-basic-vcam-config.cpp b/UI/window-basic-vcam-config.cpp
new file mode 100644
index 000000000..5f21f71e7
--- /dev/null
+++ b/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
+#include
+#include
+#include
+
+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(
+ &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 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 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(data);
+ if (!obs_source_removed(source))
+ AddSource(source);
+ return true;
+ },
+ static_cast(&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);
+}
diff --git a/UI/window-basic-vcam-config.hpp b/UI/window-basic-vcam-config.hpp
new file mode 100644
index 000000000..4a00e0219
--- /dev/null
+++ b/UI/window-basic-vcam-config.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+#include
+#include
+
+#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;
+};