Prechádzať zdrojové kódy

Merge pull request #6577 from chippydip/multiple-video-mixes

libobs: Add support for multiple video mixes
Jim 3 rokov pred
rodič
commit
9e15114750

+ 3 - 0
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
@@ -258,6 +259,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

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

@@ -723,6 +723,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"

+ 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();
 }
 
-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();
-	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
 
 #include <QPushButton>
+#include <QBoxLayout>
+#include <QScopedPointer>
 
 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<QPushButton> button;
+	QScopedPointer<QPushButton> icon;
 };

+ 9 - 2
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();
 

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

@@ -21,6 +21,7 @@
 #include <QMessageBox>
 #include <util/dstr.hpp>
 #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();
 	}
 }
 

+ 49 - 65
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"
 #ifdef __APPLE__
 #include "window-permissions.hpp"
@@ -1655,16 +1656,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()
@@ -1680,28 +1680,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)
@@ -7354,19 +7339,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;
 	}
 
@@ -7376,7 +7361,7 @@ void OBSBasic::StartReplayBuffer()
 	SaveProject();
 
 	if (!outputHandler->StartReplayBuffer()) {
-		replayBufferButton->setChecked(false);
+		replayBufferButton->first()->setChecked(false);
 	} else if (os_atomic_load_bool(&recording_paused)) {
 		ShowReplayBufferPauseWarning();
 	}
@@ -7387,10 +7372,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)
@@ -7415,11 +7402,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)
@@ -7472,11 +7461,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);
 
@@ -7525,7 +7516,7 @@ void OBSBasic::StartVirtualCam()
 	SaveProject();
 
 	if (!outputHandler->StartVirtualCam()) {
-		vcamButton->setChecked(false);
+		vcamButton->first()->setChecked(false);
 	}
 }
 
@@ -7547,10 +7538,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);
@@ -7565,10 +7556,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);
@@ -7720,7 +7711,7 @@ void OBSBasic::VCamButtonClicked()
 		StopVirtualCam();
 	} else {
 		if (!UIValidation::NoSourcesConfirmation(this)) {
-			vcamButton->setChecked(false);
+			vcamButton->first()->setChecked(false);
 			return;
 		}
 
@@ -7728,6 +7719,12 @@ void OBSBasic::VCamButtonClicked()
 	}
 }
 
+void OBSBasic::VCamConfigButtonClicked()
+{
+	OBSBasicVCamConfig config(this);
+	config.exec();
+}
+
 void OBSBasic::on_settingsButton_clicked()
 {
 	on_action_Settings_triggered();
@@ -9854,6 +9851,7 @@ void OBSBasic::PauseRecording()
 
 		os_atomic_set_bool(&recording_paused, true);
 
+		auto replay = replayBufferButton->second();
 		if (replay)
 			replay->setEnabled(false);
 
@@ -9898,6 +9896,7 @@ void OBSBasic::UnpauseRecording()
 
 		os_atomic_set_bool(&recording_paused, false);
 
+		auto replay = replayBufferButton->second();
 		if (replay)
 			replay->setEnabled(true);
 
@@ -9974,28 +9973,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)

+ 4 - 5
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;
@@ -299,12 +299,10 @@ private:
 	QPointer<QMenu> startStreamMenu;
 
 	QPointer<QPushButton> transitionButton;
-	QPointer<QPushButton> replayBufferButton;
-	QPointer<QHBoxLayout> replayLayout;
+	QPointer<ControlsSplitButton> replayBufferButton;
 	QScopedPointer<QPushButton> pause;
-	QScopedPointer<QPushButton> replay;
 
-	QPointer<QPushButton> vcamButton;
+	QPointer<ControlsSplitButton> vcamButton;
 	bool vcamEnabled = false;
 
 	QScopedPointer<QSystemTrayIcon> trayIcon;
@@ -1062,6 +1060,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();

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

+ 1 - 1
libobs/obs-encoder.c

@@ -187,7 +187,7 @@ static inline bool has_scaling(const struct obs_encoder *encoder)
 
 static inline bool gpu_encode_available(const struct obs_encoder *encoder)
 {
-	struct obs_core_video *const video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	return (encoder->info.caps & OBS_ENCODER_CAP_PASS_TEXTURE) != 0 &&
 	       (video->using_p010_tex || video->using_nv12_tex);
 }

+ 42 - 30
libobs/obs-internal.h

@@ -245,8 +245,9 @@ struct obs_task_info {
 	void *param;
 };
 
-struct obs_core_video {
-	graphics_t *graphics;
+struct obs_core_video_mix {
+	struct obs_view *view;
+
 	gs_stagesurf_t *active_copy_surfaces[NUM_TEXTURES][NUM_CHANNELS];
 	gs_stagesurf_t *copy_surfaces[NUM_TEXTURES][NUM_CHANNELS];
 	gs_texture_t *convert_textures[NUM_CHANNELS];
@@ -264,22 +265,13 @@ struct obs_core_video {
 	bool using_p010_tex;
 	struct circlebuf vframe_info_buffer;
 	struct circlebuf vframe_info_buffer_gpu;
-	gs_effect_t *default_effect;
-	gs_effect_t *default_rect_effect;
-	gs_effect_t *opaque_effect;
-	gs_effect_t *solid_effect;
-	gs_effect_t *repeat_effect;
-	gs_effect_t *conversion_effect;
-	gs_effect_t *bicubic_effect;
-	gs_effect_t *lanczos_effect;
-	gs_effect_t *area_effect;
-	gs_effect_t *bilinear_lowres_effect;
-	gs_effect_t *premultiplied_alpha_effect;
-	gs_samplerstate_t *point_sampler;
 	gs_stagesurf_t *mapped_surfaces[NUM_CHANNELS];
 	int cur_texture;
 	volatile long raw_active;
 	volatile long gpu_encoder_active;
+	bool gpu_was_active;
+	bool raw_was_active;
+	bool was_active;
 	pthread_mutex_t gpu_encoder_mutex;
 	struct circlebuf gpu_encoder_queue;
 	struct circlebuf gpu_encoder_avail_queue;
@@ -290,28 +282,49 @@ struct obs_core_video {
 	bool gpu_encode_thread_initialized;
 	volatile bool gpu_encode_stop;
 
+	video_t *video;
+
+	bool gpu_conversion;
+	const char *conversion_techs[NUM_CHANNELS];
+	bool conversion_needed;
+	float conversion_width_i;
+	float conversion_height_i;
+
+	float color_matrix[16];
+	enum obs_scale_type scale_type;
+};
+
+extern int obs_init_video_mix(struct obs_video_info *ovi,
+			      struct obs_core_video_mix *video);
+extern void obs_free_video_mix(struct obs_core_video_mix *video);
+
+struct obs_core_video {
+	graphics_t *graphics;
+	gs_effect_t *default_effect;
+	gs_effect_t *default_rect_effect;
+	gs_effect_t *opaque_effect;
+	gs_effect_t *solid_effect;
+	gs_effect_t *repeat_effect;
+	gs_effect_t *conversion_effect;
+	gs_effect_t *bicubic_effect;
+	gs_effect_t *lanczos_effect;
+	gs_effect_t *area_effect;
+	gs_effect_t *bilinear_lowres_effect;
+	gs_effect_t *premultiplied_alpha_effect;
+	gs_samplerstate_t *point_sampler;
+
 	uint64_t video_time;
 	uint64_t video_frame_interval_ns;
+	uint64_t video_half_frame_interval_ns;
 	uint64_t video_avg_frame_time_ns;
 	double video_fps;
-	video_t *video;
 	pthread_t video_thread;
 	uint32_t total_frames;
 	uint32_t lagged_frames;
 	bool thread_initialized;
 
-	bool gpu_conversion;
-	const char *conversion_techs[NUM_CHANNELS];
-	bool conversion_needed;
-	float conversion_width_i;
-	float conversion_height_i;
-
-	uint32_t output_width;
-	uint32_t output_height;
 	uint32_t base_width;
 	uint32_t base_height;
-	float color_matrix[16];
-	enum obs_scale_type scale_type;
 
 	gs_texture_t *transparent_texture;
 
@@ -330,6 +343,10 @@ struct obs_core_video {
 
 	pthread_mutex_t task_mutex;
 	struct circlebuf tasks;
+
+	pthread_mutex_t mixes_mutex;
+	DARRAY(struct obs_core_video_mix) mixes;
+	struct obs_core_video_mix *main_mix;
 };
 
 struct audio_monitor;
@@ -463,11 +480,6 @@ struct obs_graphics_context {
 	uint64_t frame_time_total_ns;
 	uint64_t fps_total_ns;
 	uint32_t fps_total_frames;
-#ifdef _WIN32
-	bool gpu_was_active;
-#endif
-	bool raw_was_active;
-	bool was_active;
 	const char *video_thread_name;
 };
 

+ 1 - 4
libobs/obs-source-deinterlace.c

@@ -148,7 +148,6 @@ static inline uint64_t uint64_diff(uint64_t ts1, uint64_t ts2)
 static inline void deinterlace_get_closest_frames(obs_source_t *s,
 						  uint64_t sys_time)
 {
-	const struct video_output_info *info;
 	uint64_t half_interval;
 
 	if (s->async_unbuffered && s->deinterlace_offset) {
@@ -169,9 +168,7 @@ static inline void deinterlace_get_closest_frames(obs_source_t *s,
 	if (!s->async_frames.num)
 		return;
 
-	info = video_output_get_info(obs->video.video);
-	half_interval = (uint64_t)info->fps_den * 500000000ULL /
-			(uint64_t)info->fps_num;
+	half_interval = obs->video.video_half_frame_interval_ns;
 
 	if (first_frame(s) || ready_deinterlace_frames(s, sys_time)) {
 		uint64_t offset;

+ 15 - 18
libobs/obs-video-gpu-encode.c

@@ -17,14 +17,12 @@
 
 #include "obs-internal.h"
 
-static void *gpu_encode_thread(void *unused)
+static void *gpu_encode_thread(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
-	uint64_t interval = video_output_get_frame_time(obs->video.video);
+	uint64_t interval = video_output_get_frame_time(video->video);
 	DARRAY(obs_encoder_t *) encoders;
 	int wait_frames = NUM_ENCODE_TEXTURE_FRAMES_TO_WAIT;
 
-	UNUSED_PARAMETER(unused);
 	da_init(encoders);
 
 	os_set_thread_name("obs gpu encode thread");
@@ -149,10 +147,11 @@ static void *gpu_encode_thread(void *unused)
 	return NULL;
 }
 
-bool init_gpu_encoding(struct obs_core_video *video)
+bool init_gpu_encoding(struct obs_core_video_mix *video)
 {
 #ifdef _WIN32
-	struct obs_video_info *ovi = &video->ovi;
+	const struct video_output_info *info =
+		video_output_get_info(video->video);
 
 	video->gpu_encode_stop = false;
 
@@ -161,16 +160,14 @@ bool init_gpu_encoding(struct obs_core_video *video)
 		gs_texture_t *tex;
 		gs_texture_t *tex_uv;
 
-		if (ovi->output_format == VIDEO_FORMAT_P010) {
-			gs_texture_create_p010(&tex, &tex_uv, ovi->output_width,
-					       ovi->output_height,
-					       GS_RENDER_TARGET |
-						       GS_SHARED_KM_TEX);
+		if (info->format == VIDEO_FORMAT_P010) {
+			gs_texture_create_p010(
+				&tex, &tex_uv, info->width, info->height,
+				GS_RENDER_TARGET | GS_SHARED_KM_TEX);
 		} else {
-			gs_texture_create_nv12(&tex, &tex_uv, ovi->output_width,
-					       ovi->output_height,
-					       GS_RENDER_TARGET |
-						       GS_SHARED_KM_TEX);
+			gs_texture_create_nv12(
+				&tex, &tex_uv, info->width, info->height,
+				GS_RENDER_TARGET | GS_SHARED_KM_TEX);
 		}
 		if (!tex) {
 			return false;
@@ -191,7 +188,7 @@ bool init_gpu_encoding(struct obs_core_video *video)
 	    0)
 		return false;
 	if (pthread_create(&video->gpu_encode_thread, NULL, gpu_encode_thread,
-			   NULL) != 0)
+			   video) != 0)
 		return false;
 
 	os_event_signal(video->gpu_encode_inactive);
@@ -204,7 +201,7 @@ bool init_gpu_encoding(struct obs_core_video *video)
 #endif
 }
 
-void stop_gpu_encoding_thread(struct obs_core_video *video)
+void stop_gpu_encoding_thread(struct obs_core_video_mix *video)
 {
 	if (video->gpu_encode_thread_initialized) {
 		os_atomic_set_bool(&video->gpu_encode_stop, true);
@@ -214,7 +211,7 @@ void stop_gpu_encoding_thread(struct obs_core_video *video)
 	}
 }
 
-void free_gpu_encoding(struct obs_core_video *video)
+void free_gpu_encoding(struct obs_core_video_mix *video)
 {
 	if (video->gpu_encode_semaphore) {
 		os_sem_destroy(video->gpu_encode_semaphore);

+ 131 - 75
libobs/obs-video.c

@@ -37,8 +37,7 @@ static uint64_t tick_sources(uint64_t cur_time, uint64_t last_time)
 	float seconds;
 
 	if (!last_time)
-		last_time = cur_time -
-			    video_output_get_frame_time(obs->video.video);
+		last_time = cur_time - obs->video.video_frame_interval_ns;
 
 	delta_time = cur_time - last_time;
 	seconds = (float)((double)delta_time / 1000000000.0);
@@ -113,7 +112,7 @@ static inline void set_render_size(uint32_t width, uint32_t height)
 	gs_set_viewport(0, 0, width, height);
 }
 
-static inline void unmap_last_surface(struct obs_core_video *video)
+static inline void unmap_last_surface(struct obs_core_video_mix *video)
 {
 	for (int c = 0; c < NUM_CHANNELS; ++c) {
 		if (video->mapped_surfaces[c]) {
@@ -124,8 +123,11 @@ static inline void unmap_last_surface(struct obs_core_video *video)
 }
 
 static const char *render_main_texture_name = "render_main_texture";
-static inline void render_main_texture(struct obs_core_video *video)
+static inline void render_main_texture(struct obs_core_video_mix *video)
 {
+	uint32_t base_width = obs->video.base_width;
+	uint32_t base_height = obs->video.base_height;
+
 	profile_start(render_main_texture_name);
 	GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_MAIN_TEXTURE,
 			      render_main_texture_name);
@@ -137,7 +139,7 @@ static inline void render_main_texture(struct obs_core_video *video)
 					      video->render_space);
 	gs_clear(GS_CLEAR_COLOR, &clear_color, 1.0f, 0);
 
-	set_render_size(video->base_width, video->base_height);
+	set_render_size(base_width, base_height);
 
 	pthread_mutex_lock(&obs->data.draw_callbacks_mutex);
 
@@ -145,13 +147,12 @@ static inline void render_main_texture(struct obs_core_video *video)
 		struct draw_callback *callback;
 		callback = obs->data.draw_callbacks.array + (i - 1);
 
-		callback->draw(callback->param, video->base_width,
-			       video->base_height);
+		callback->draw(callback->param, base_width, base_height);
 	}
 
 	pthread_mutex_unlock(&obs->data.draw_callbacks_mutex);
 
-	obs_view_render(&obs->data.main_view);
+	obs_view_render(video->view);
 
 	video->texture_rendered = true;
 
@@ -160,17 +161,21 @@ static inline void render_main_texture(struct obs_core_video *video)
 }
 
 static inline gs_effect_t *
-get_scale_effect_internal(struct obs_core_video *video)
+get_scale_effect_internal(struct obs_core_video_mix *mix)
 {
+	struct obs_core_video *video = &obs->video;
+	const struct video_output_info *info =
+		video_output_get_info(mix->video);
+
 	/* if the dimension is under half the size of the original image,
 	 * bicubic/lanczos can't sample enough pixels to create an accurate
 	 * image, so use the bilinear low resolution effect instead */
-	if (video->output_width < (video->base_width / 2) &&
-	    video->output_height < (video->base_height / 2)) {
+	if (info->width < (video->base_width / 2) &&
+	    info->height < (video->base_height / 2)) {
 		return video->bilinear_lowres_effect;
 	}
 
-	switch (video->scale_type) {
+	switch (mix->scale_type) {
 	case OBS_SCALE_BILINEAR:
 		return video->default_effect;
 	case OBS_SCALE_LANCZOS:
@@ -193,15 +198,17 @@ static inline bool resolution_close(struct obs_core_video *video,
 	return labs(width_cmp) <= 16 && labs(height_cmp) <= 16;
 }
 
-static inline gs_effect_t *get_scale_effect(struct obs_core_video *video,
+static inline gs_effect_t *get_scale_effect(struct obs_core_video_mix *mix,
 					    uint32_t width, uint32_t height)
 {
+	struct obs_core_video *video = &obs->video;
+
 	if (resolution_close(video, width, height)) {
 		return video->default_effect;
 	} else {
 		/* if the scale method couldn't be loaded, use either bicubic
 		 * or bilinear by default */
-		gs_effect_t *effect = get_scale_effect_internal(video);
+		gs_effect_t *effect = get_scale_effect_internal(mix);
 		if (!effect)
 			effect = !!video->bicubic_effect
 					 ? video->bicubic_effect
@@ -211,17 +218,19 @@ static inline gs_effect_t *get_scale_effect(struct obs_core_video *video,
 }
 
 static const char *render_output_texture_name = "render_output_texture";
-static inline gs_texture_t *render_output_texture(struct obs_core_video *video)
+static inline gs_texture_t *
+render_output_texture(struct obs_core_video_mix *mix)
 {
-	gs_texture_t *texture = video->render_texture;
-	gs_texture_t *target = video->output_texture;
+	struct obs_core_video *video = &obs->video;
+	gs_texture_t *texture = mix->render_texture;
+	gs_texture_t *target = mix->output_texture;
 	uint32_t width = gs_texture_get_width(target);
 	uint32_t height = gs_texture_get_height(target);
 
-	gs_effect_t *effect = get_scale_effect(video, width, height);
+	gs_effect_t *effect = get_scale_effect(mix, width, height);
 	gs_technique_t *tech;
 
-	if (video->ovi.output_format == VIDEO_FORMAT_RGBA) {
+	if (video_output_get_format(mix->video) == VIDEO_FORMAT_RGBA) {
 		tech = gs_effect_get_technique(effect, "DrawAlphaDivide");
 	} else {
 		if ((effect == video->default_effect) &&
@@ -298,13 +307,13 @@ static void render_convert_plane(gs_effect_t *effect, gs_texture_t *target,
 }
 
 static const char *render_convert_texture_name = "render_convert_texture";
-static void render_convert_texture(struct obs_core_video *video,
+static void render_convert_texture(struct obs_core_video_mix *video,
 				   gs_texture_t *const *const convert_textures,
 				   gs_texture_t *texture)
 {
 	profile_start(render_convert_texture_name);
 
-	gs_effect_t *effect = video->conversion_effect;
+	gs_effect_t *effect = obs->video.conversion_effect;
 	gs_eparam_t *color_vec0 =
 		gs_effect_get_param_by_name(effect, "color_vec0");
 	gs_eparam_t *color_vec1 =
@@ -330,7 +339,7 @@ static void render_convert_texture(struct obs_core_video *video,
 
 	if (convert_textures[0]) {
 		const float hdr_nominal_peak_level =
-			video->hdr_nominal_peak_level;
+			obs->video.hdr_nominal_peak_level;
 		const float multiplier =
 			obs_get_video_sdr_white_level() / 10000.f;
 		gs_effect_set_texture(image, texture);
@@ -381,7 +390,7 @@ static void render_convert_texture(struct obs_core_video *video,
 
 static const char *stage_output_texture_name = "stage_output_texture";
 static inline void
-stage_output_texture(struct obs_core_video *video, int cur_texture,
+stage_output_texture(struct obs_core_video_mix *video, int cur_texture,
 		     gs_texture_t *const *const convert_textures,
 		     gs_stagesurf_t *const *const copy_surfaces,
 		     size_t channel_count)
@@ -418,7 +427,8 @@ stage_output_texture(struct obs_core_video *video, int cur_texture,
 }
 
 #ifdef _WIN32
-static inline bool queue_frame(struct obs_core_video *video, bool raw_active,
+static inline bool queue_frame(struct obs_core_video_mix *video,
+			       bool raw_active,
 			       struct obs_vframe_info *vframe_info)
 {
 	bool duplicate =
@@ -480,7 +490,7 @@ finish:
 
 extern void full_stop(struct obs_encoder *encoder);
 
-static inline void encode_gpu(struct obs_core_video *video, bool raw_active,
+static inline void encode_gpu(struct obs_core_video_mix *video, bool raw_active,
 			      struct obs_vframe_info *vframe_info)
 {
 	while (queue_frame(video, raw_active, vframe_info))
@@ -488,7 +498,8 @@ static inline void encode_gpu(struct obs_core_video *video, bool raw_active,
 }
 
 static const char *output_gpu_encoders_name = "output_gpu_encoders";
-static void output_gpu_encoders(struct obs_core_video *video, bool raw_active)
+static void output_gpu_encoders(struct obs_core_video_mix *video,
+				bool raw_active)
 {
 	profile_start(output_gpu_encoders_name);
 
@@ -510,8 +521,9 @@ end:
 }
 #endif
 
-static inline void render_video(struct obs_core_video *video, bool raw_active,
-				const bool gpu_active, int cur_texture)
+static inline void render_video(struct obs_core_video_mix *video,
+				bool raw_active, const bool gpu_active,
+				int cur_texture)
 {
 	gs_begin_scene();
 
@@ -559,7 +571,7 @@ static inline void render_video(struct obs_core_video *video, bool raw_active,
 	gs_end_scene();
 }
 
-static inline bool download_frame(struct obs_core_video *video,
+static inline bool download_frame(struct obs_core_video_mix *video,
 				  int prev_texture, struct video_data *frame)
 {
 	if (!video->textures_copied[prev_texture])
@@ -763,7 +775,7 @@ static inline void copy_rgbx_frame(struct video_frame *output,
 	}
 }
 
-static inline void output_video_data(struct obs_core_video *video,
+static inline void output_video_data(struct obs_core_video_mix *video,
 				     struct video_data *input_frame, int count)
 {
 	const struct video_output_info *info;
@@ -786,8 +798,7 @@ static inline void output_video_data(struct obs_core_video *video,
 	}
 }
 
-static inline void video_sleep(struct obs_core_video *video, bool raw_active,
-			       const bool gpu_active, uint64_t *p_time,
+static inline void video_sleep(struct obs_core_video *video, uint64_t *p_time,
 			       uint64_t interval_ns)
 {
 	struct obs_vframe_info vframe_info;
@@ -815,12 +826,20 @@ static inline void video_sleep(struct obs_core_video *video, bool raw_active,
 	vframe_info.timestamp = cur_time;
 	vframe_info.count = count;
 
-	if (raw_active)
-		circlebuf_push_back(&video->vframe_info_buffer, &vframe_info,
-				    sizeof(vframe_info));
-	if (gpu_active)
-		circlebuf_push_back(&video->vframe_info_buffer_gpu,
-				    &vframe_info, sizeof(vframe_info));
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++) {
+		struct obs_core_video_mix *video = obs->video.mixes.array + i;
+		bool raw_active = video->raw_was_active;
+		bool gpu_active = video->gpu_was_active;
+
+		if (raw_active)
+			circlebuf_push_back(&video->vframe_info_buffer,
+					    &vframe_info, sizeof(vframe_info));
+		if (gpu_active)
+			circlebuf_push_back(&video->vframe_info_buffer_gpu,
+					    &vframe_info, sizeof(vframe_info));
+	}
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
 }
 
 static const char *output_frame_gs_context_name = "gs_context(video->graphics)";
@@ -828,9 +847,11 @@ static const char *output_frame_render_video_name = "render_video";
 static const char *output_frame_download_frame_name = "download_frame";
 static const char *output_frame_gs_flush_name = "gs_flush";
 static const char *output_frame_output_video_data_name = "output_video_data";
-static inline void output_frame(bool raw_active, const bool gpu_active)
+static inline void output_frame(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
+	const bool raw_active = video->raw_was_active;
+	const bool gpu_active = video->gpu_was_active;
+
 	int cur_texture = video->cur_texture;
 	int prev_texture = cur_texture == 0 ? NUM_TEXTURES - 1
 					    : cur_texture - 1;
@@ -840,7 +861,7 @@ static inline void output_frame(bool raw_active, const bool gpu_active)
 	memset(&frame, 0, sizeof(struct video_data));
 
 	profile_start(output_frame_gs_context_name);
-	gs_enter_context(video->graphics);
+	gs_enter_context(obs->video.graphics);
 
 	profile_start(output_frame_render_video_name);
 	GS_DEBUG_MARKER_BEGIN(GS_DEBUG_COLOR_RENDER_VIDEO,
@@ -877,28 +898,42 @@ static inline void output_frame(bool raw_active, const bool gpu_active)
 		video->cur_texture = 0;
 }
 
+static inline void output_frames(void)
+{
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++) {
+		struct obs_core_video_mix *mix = obs->video.mixes.array + i;
+		if (mix->view) {
+			output_frame(mix);
+		} else {
+			obs_free_video_mix(mix);
+			da_erase(obs->video.mixes, i);
+			i--;
+			num--;
+		}
+	}
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+}
+
 #define NBSP "\xC2\xA0"
 
-static void clear_base_frame_data(void)
+static void clear_base_frame_data(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
 	video->texture_rendered = false;
 	video->texture_converted = false;
 	circlebuf_free(&video->vframe_info_buffer);
 	video->cur_texture = 0;
 }
 
-static void clear_raw_frame_data(void)
+static void clear_raw_frame_data(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
 	memset(video->textures_copied, 0, sizeof(video->textures_copied));
 	circlebuf_free(&video->vframe_info_buffer);
 }
 
 #ifdef _WIN32
-static void clear_gpu_frame_data(void)
+static void clear_gpu_frame_data(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
 	circlebuf_free(&video->vframe_info_buffer_gpu);
 }
 #endif
@@ -1006,35 +1041,63 @@ static void uninit_winrt_state(struct winrt_state *winrt)
 static const char *tick_sources_name = "tick_sources";
 static const char *render_displays_name = "render_displays";
 static const char *output_frame_name = "output_frame";
-bool obs_graphics_thread_loop(struct obs_graphics_context *context)
+static inline void update_active_state(struct obs_core_video_mix *video)
 {
-	/* defer loop break to clean up sources */
-	const bool stop_requested = video_output_stopped(obs->video.video);
+	const bool raw_was_active = video->raw_was_active;
+	const bool gpu_was_active = video->gpu_was_active;
+	const bool was_active = video->was_active;
 
-	uint64_t frame_start = os_gettime_ns();
-	uint64_t frame_time_ns;
-	bool raw_active = os_atomic_load_long(&obs->video.raw_active) > 0;
+	bool raw_active = os_atomic_load_long(&video->raw_active) > 0;
 #ifdef _WIN32
 	const bool gpu_active =
-		os_atomic_load_long(&obs->video.gpu_encoder_active) > 0;
+		os_atomic_load_long(&video->gpu_encoder_active) > 0;
 	const bool active = raw_active || gpu_active;
 #else
 	const bool gpu_active = 0;
 	const bool active = raw_active;
 #endif
 
-	if (!context->was_active && active)
-		clear_base_frame_data();
-	if (!context->raw_was_active && raw_active)
-		clear_raw_frame_data();
+	if (!was_active && active)
+		clear_base_frame_data(video);
+	if (!raw_was_active && raw_active)
+		clear_raw_frame_data(video);
 #ifdef _WIN32
-	if (!context->gpu_was_active && gpu_active)
-		clear_gpu_frame_data();
+	if (!gpu_was_active && gpu_active)
+		clear_gpu_frame_data(video);
 
-	context->gpu_was_active = gpu_active;
+	video->gpu_was_active = gpu_active;
 #endif
-	context->raw_was_active = raw_active;
-	context->was_active = active;
+	video->raw_was_active = raw_active;
+	video->was_active = active;
+}
+
+static inline void update_active_states(void)
+{
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++)
+		update_active_state(obs->video.mixes.array + i);
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+}
+
+static inline bool stop_requested(void)
+{
+	bool success = true;
+
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++)
+		if (!video_output_stopped(obs->video.mixes.array[i].video))
+			success = false;
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	return success;
+}
+
+bool obs_graphics_thread_loop(struct obs_graphics_context *context)
+{
+	uint64_t frame_start = os_gettime_ns();
+	uint64_t frame_time_ns;
+
+	update_active_states();
 
 	profile_start(context->video_thread_name);
 
@@ -1056,7 +1119,7 @@ bool obs_graphics_thread_loop(struct obs_graphics_context *context)
 #endif
 
 	profile_start(output_frame_name);
-	output_frame(raw_active, gpu_active);
+	output_frames();
 	profile_end(output_frame_name);
 
 	profile_start(render_displays_name);
@@ -1071,8 +1134,7 @@ bool obs_graphics_thread_loop(struct obs_graphics_context *context)
 
 	profile_reenable_thread();
 
-	video_sleep(&obs->video, raw_active, gpu_active, &obs->video.video_time,
-		    context->interval);
+	video_sleep(&obs->video, &obs->video.video_time, context->interval);
 
 	context->frame_time_total_ns += frame_time_ns;
 	context->fps_total_ns += (obs->video.video_time - context->last_time);
@@ -1091,7 +1153,7 @@ bool obs_graphics_thread_loop(struct obs_graphics_context *context)
 		context->fps_total_frames = 0;
 	}
 
-	return !stop_requested;
+	return !stop_requested();
 }
 
 void *obs_graphics_thread(void *param)
@@ -1103,10 +1165,9 @@ void *obs_graphics_thread(void *param)
 
 	is_graphics_thread = true;
 
-	const uint64_t interval = video_output_get_frame_time(obs->video.video);
+	const uint64_t interval = obs->video.video_frame_interval_ns;
 
 	obs->video.video_time = os_gettime_ns();
-	obs->video.video_frame_interval_ns = interval;
 
 	os_set_thread_name("libobs: graphics thread");
 
@@ -1118,16 +1179,11 @@ void *obs_graphics_thread(void *param)
 	srand((unsigned int)time(NULL));
 
 	struct obs_graphics_context context;
-	context.interval = video_output_get_frame_time(obs->video.video);
+	context.interval = interval;
 	context.frame_time_total_ns = 0;
 	context.fps_total_ns = 0;
 	context.fps_total_frames = 0;
 	context.last_time = 0;
-#ifdef _WIN32
-	context.gpu_was_active = false;
-#endif
-	context.raw_was_active = false;
-	context.was_active = false;
 	context.video_thread_name = video_thread_name;
 
 #ifdef __APPLE__

+ 47 - 0
libobs/obs-view.c

@@ -139,3 +139,50 @@ void obs_view_render(obs_view_t *view)
 
 	pthread_mutex_unlock(&view->channels_mutex);
 }
+
+static inline size_t find_mix_for_view(obs_view_t *view)
+{
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++) {
+		if (obs->video.mixes.array[i].view == view)
+			return i;
+	}
+
+	return DARRAY_INVALID;
+}
+
+static inline void set_main_mix()
+{
+	size_t idx = find_mix_for_view(&obs->data.main_view);
+
+	struct obs_core_video_mix *mix = NULL;
+	if (idx != DARRAY_INVALID)
+		mix = obs->video.mixes.array + idx;
+	obs->video.main_mix = mix;
+}
+
+video_t *obs_view_add(obs_view_t *view)
+{
+	struct obs_core_video_mix mix = {0};
+	mix.view = view;
+	if (obs_init_video_mix(&obs->video.ovi, &mix) != 0) {
+		obs_free_video_mix(&mix);
+		return NULL;
+	}
+
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	da_push_back(obs->video.mixes, &mix);
+	set_main_mix();
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	return mix.video;
+}
+
+void obs_view_remove(obs_view_t *view)
+{
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	size_t idx = find_mix_for_view(view);
+	if (idx != DARRAY_INVALID)
+		obs->video.mixes.array[idx].view = NULL;
+	set_main_mix();
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+}

+ 265 - 203
libobs/obs.c

@@ -44,9 +44,10 @@ static inline void make_video_info(struct video_output_info *vi,
 	vi->cache_size = 6;
 }
 
-static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi)
+static inline void calc_gpu_conversion_sizes(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
+	const struct video_output_info *info =
+		video_output_get_info(video->video);
 
 	video->conversion_needed = false;
 	video->conversion_techs[0] = NULL;
@@ -55,19 +56,19 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi)
 	video->conversion_width_i = 0.f;
 	video->conversion_height_i = 0.f;
 
-	switch ((uint32_t)ovi->output_format) {
+	switch ((uint32_t)info->format) {
 	case VIDEO_FORMAT_I420:
 		video->conversion_needed = true;
 		video->conversion_techs[0] = "Planar_Y";
 		video->conversion_techs[1] = "Planar_U_Left";
 		video->conversion_techs[2] = "Planar_V_Left";
-		video->conversion_width_i = 1.f / (float)ovi->output_width;
+		video->conversion_width_i = 1.f / (float)info->width;
 		break;
 	case VIDEO_FORMAT_NV12:
 		video->conversion_needed = true;
 		video->conversion_techs[0] = "NV12_Y";
 		video->conversion_techs[1] = "NV12_UV";
-		video->conversion_width_i = 1.f / (float)ovi->output_width;
+		video->conversion_width_i = 1.f / (float)info->width;
 		break;
 	case VIDEO_FORMAT_I444:
 		video->conversion_needed = true;
@@ -77,13 +78,13 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi)
 		break;
 	case VIDEO_FORMAT_I010:
 		video->conversion_needed = true;
-		video->conversion_width_i = 1.f / (float)ovi->output_width;
-		video->conversion_height_i = 1.f / (float)ovi->output_height;
-		if (ovi->colorspace == VIDEO_CS_2100_PQ) {
+		video->conversion_width_i = 1.f / (float)info->width;
+		video->conversion_height_i = 1.f / (float)info->height;
+		if (info->colorspace == VIDEO_CS_2100_PQ) {
 			video->conversion_techs[0] = "I010_PQ_Y";
 			video->conversion_techs[1] = "I010_PQ_U";
 			video->conversion_techs[2] = "I010_PQ_V";
-		} else if (ovi->colorspace == VIDEO_CS_2100_HLG) {
+		} else if (info->colorspace == VIDEO_CS_2100_HLG) {
 			video->conversion_techs[0] = "I010_HLG_Y";
 			video->conversion_techs[1] = "I010_HLG_U";
 			video->conversion_techs[2] = "I010_HLG_V";
@@ -95,12 +96,12 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi)
 		break;
 	case VIDEO_FORMAT_P010:
 		video->conversion_needed = true;
-		video->conversion_width_i = 1.f / (float)ovi->output_width;
-		video->conversion_height_i = 1.f / (float)ovi->output_height;
-		if (ovi->colorspace == VIDEO_CS_2100_PQ) {
+		video->conversion_width_i = 1.f / (float)info->width;
+		video->conversion_height_i = 1.f / (float)info->height;
+		if (info->colorspace == VIDEO_CS_2100_PQ) {
 			video->conversion_techs[0] = "P010_PQ_Y";
 			video->conversion_techs[1] = "P010_PQ_UV";
-		} else if (ovi->colorspace == VIDEO_CS_2100_HLG) {
+		} else if (info->colorspace == VIDEO_CS_2100_HLG) {
 			video->conversion_techs[0] = "P010_HLG_Y";
 			video->conversion_techs[1] = "P010_HLG_UV";
 		} else {
@@ -110,22 +111,21 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi)
 	}
 }
 
-static bool obs_init_gpu_conversion(struct obs_video_info *ovi)
+static bool obs_init_gpu_conversion(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
+	const struct video_output_info *info =
+		video_output_get_info(video->video);
 
-	calc_gpu_conversion_sizes(ovi);
+	calc_gpu_conversion_sizes(video);
 
-	video->using_nv12_tex = ovi->output_format == VIDEO_FORMAT_NV12
-					? gs_nv12_available()
-					: false;
-	video->using_p010_tex = ovi->output_format == VIDEO_FORMAT_P010
-					? gs_p010_available()
-					: false;
+	video->using_nv12_tex =
+		info->format == VIDEO_FORMAT_NV12 ? gs_nv12_available() : false;
+	video->using_p010_tex =
+		info->format == VIDEO_FORMAT_P010 ? gs_p010_available() : false;
 
 	if (!video->conversion_needed) {
 		blog(LOG_INFO, "GPU conversion not available for format: %u",
-		     (unsigned int)ovi->output_format);
+		     (unsigned int)info->format);
 		video->gpu_conversion = false;
 		video->using_nv12_tex = false;
 		video->using_p010_tex = false;
@@ -151,19 +151,19 @@ static bool obs_init_gpu_conversion(struct obs_video_info *ovi)
 	video->convert_textures_encode[1] = NULL;
 	video->convert_textures_encode[2] = NULL;
 	if (video->using_nv12_tex) {
-		if (!gs_texture_create_nv12(
-			    &video->convert_textures_encode[0],
-			    &video->convert_textures_encode[1],
-			    ovi->output_width, ovi->output_height,
-			    GS_RENDER_TARGET | GS_SHARED_KM_TEX)) {
+		if (!gs_texture_create_nv12(&video->convert_textures_encode[0],
+					    &video->convert_textures_encode[1],
+					    info->width, info->height,
+					    GS_RENDER_TARGET |
+						    GS_SHARED_KM_TEX)) {
 			return false;
 		}
 	} else if (video->using_p010_tex) {
-		if (!gs_texture_create_p010(
-			    &video->convert_textures_encode[0],
-			    &video->convert_textures_encode[1],
-			    ovi->output_width, ovi->output_height,
-			    GS_RENDER_TARGET | GS_SHARED_KM_TEX)) {
+		if (!gs_texture_create_p010(&video->convert_textures_encode[0],
+					    &video->convert_textures_encode[1],
+					    info->width, info->height,
+					    GS_RENDER_TARGET |
+						    GS_SHARED_KM_TEX)) {
 			return false;
 		}
 	}
@@ -171,68 +171,66 @@ static bool obs_init_gpu_conversion(struct obs_video_info *ovi)
 
 	bool success = true;
 
-	const struct video_output_info *info =
-		video_output_get_info(video->video);
 	switch (info->format) {
 	case VIDEO_FORMAT_I420:
 		video->convert_textures[0] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
+			gs_texture_create(info->width, info->height, GS_R8, 1,
+					  NULL, GS_RENDER_TARGET);
+		video->convert_textures[1] =
+			gs_texture_create(info->width / 2, info->height / 2,
+					  GS_R8, 1, NULL, GS_RENDER_TARGET);
+		video->convert_textures[2] =
+			gs_texture_create(info->width / 2, info->height / 2,
 					  GS_R8, 1, NULL, GS_RENDER_TARGET);
-		video->convert_textures[1] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8, 1,
-			NULL, GS_RENDER_TARGET);
-		video->convert_textures[2] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8, 1,
-			NULL, GS_RENDER_TARGET);
 		if (!video->convert_textures[0] ||
 		    !video->convert_textures[1] || !video->convert_textures[2])
 			success = false;
 		break;
 	case VIDEO_FORMAT_NV12:
 		video->convert_textures[0] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
-					  GS_R8, 1, NULL, GS_RENDER_TARGET);
-		video->convert_textures[1] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8G8,
-			1, NULL, GS_RENDER_TARGET);
+			gs_texture_create(info->width, info->height, GS_R8, 1,
+					  NULL, GS_RENDER_TARGET);
+		video->convert_textures[1] =
+			gs_texture_create(info->width / 2, info->height / 2,
+					  GS_R8G8, 1, NULL, GS_RENDER_TARGET);
 		if (!video->convert_textures[0] || !video->convert_textures[1])
 			success = false;
 		break;
 	case VIDEO_FORMAT_I444:
 		video->convert_textures[0] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
-					  GS_R8, 1, NULL, GS_RENDER_TARGET);
+			gs_texture_create(info->width, info->height, GS_R8, 1,
+					  NULL, GS_RENDER_TARGET);
 		video->convert_textures[1] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
-					  GS_R8, 1, NULL, GS_RENDER_TARGET);
+			gs_texture_create(info->width, info->height, GS_R8, 1,
+					  NULL, GS_RENDER_TARGET);
 		video->convert_textures[2] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
-					  GS_R8, 1, NULL, GS_RENDER_TARGET);
+			gs_texture_create(info->width, info->height, GS_R8, 1,
+					  NULL, GS_RENDER_TARGET);
 		if (!video->convert_textures[0] ||
 		    !video->convert_textures[1] || !video->convert_textures[2])
 			success = false;
 		break;
 	case VIDEO_FORMAT_I010:
 		video->convert_textures[0] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
+			gs_texture_create(info->width, info->height, GS_R16, 1,
+					  NULL, GS_RENDER_TARGET);
+		video->convert_textures[1] =
+			gs_texture_create(info->width / 2, info->height / 2,
+					  GS_R16, 1, NULL, GS_RENDER_TARGET);
+		video->convert_textures[2] =
+			gs_texture_create(info->width / 2, info->height / 2,
 					  GS_R16, 1, NULL, GS_RENDER_TARGET);
-		video->convert_textures[1] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R16,
-			1, NULL, GS_RENDER_TARGET);
-		video->convert_textures[2] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R16,
-			1, NULL, GS_RENDER_TARGET);
 		if (!video->convert_textures[0] ||
 		    !video->convert_textures[1] || !video->convert_textures[2])
 			success = false;
 		break;
 	case VIDEO_FORMAT_P010:
 		video->convert_textures[0] =
-			gs_texture_create(ovi->output_width, ovi->output_height,
-					  GS_R16, 1, NULL, GS_RENDER_TARGET);
-		video->convert_textures[1] = gs_texture_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_RG16,
-			1, NULL, GS_RENDER_TARGET);
+			gs_texture_create(info->width, info->height, GS_R16, 1,
+					  NULL, GS_RENDER_TARGET);
+		video->convert_textures[1] =
+			gs_texture_create(info->width / 2, info->height / 2,
+					  GS_RG16, 1, NULL, GS_RENDER_TARGET);
 		if (!video->convert_textures[0] || !video->convert_textures[1])
 			success = false;
 		break;
@@ -257,72 +255,71 @@ static bool obs_init_gpu_conversion(struct obs_video_info *ovi)
 	return success;
 }
 
-static bool obs_init_gpu_copy_surfaces(struct obs_video_info *ovi, size_t i)
+static bool obs_init_gpu_copy_surfaces(struct obs_core_video_mix *video,
+				       size_t i)
 {
-	struct obs_core_video *video = &obs->video;
-
 	const struct video_output_info *info =
 		video_output_get_info(video->video);
 	switch (info->format) {
 	case VIDEO_FORMAT_I420:
 		video->copy_surfaces[i][0] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R8);
+			info->width, info->height, GS_R8);
 		if (!video->copy_surfaces[i][0])
 			return false;
 		video->copy_surfaces[i][1] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8);
+			info->width / 2, info->height / 2, GS_R8);
 		if (!video->copy_surfaces[i][1])
 			return false;
 		video->copy_surfaces[i][2] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8);
+			info->width / 2, info->height / 2, GS_R8);
 		if (!video->copy_surfaces[i][2])
 			return false;
 		break;
 	case VIDEO_FORMAT_NV12:
 		video->copy_surfaces[i][0] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R8);
+			info->width, info->height, GS_R8);
 		if (!video->copy_surfaces[i][0])
 			return false;
 		video->copy_surfaces[i][1] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R8G8);
+			info->width / 2, info->height / 2, GS_R8G8);
 		if (!video->copy_surfaces[i][1])
 			return false;
 		break;
 	case VIDEO_FORMAT_I444:
 		video->copy_surfaces[i][0] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R8);
+			info->width, info->height, GS_R8);
 		if (!video->copy_surfaces[i][0])
 			return false;
 		video->copy_surfaces[i][1] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R8);
+			info->width, info->height, GS_R8);
 		if (!video->copy_surfaces[i][1])
 			return false;
 		video->copy_surfaces[i][2] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R8);
+			info->width, info->height, GS_R8);
 		if (!video->copy_surfaces[i][2])
 			return false;
 		break;
 	case VIDEO_FORMAT_I010:
 		video->copy_surfaces[i][0] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R16);
+			info->width, info->height, GS_R16);
 		if (!video->copy_surfaces[i][0])
 			return false;
 		video->copy_surfaces[i][1] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R16);
+			info->width / 2, info->height / 2, GS_R16);
 		if (!video->copy_surfaces[i][1])
 			return false;
 		video->copy_surfaces[i][2] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_R16);
+			info->width / 2, info->height / 2, GS_R16);
 		if (!video->copy_surfaces[i][2])
 			return false;
 		break;
 	case VIDEO_FORMAT_P010:
 		video->copy_surfaces[i][0] = gs_stagesurface_create(
-			ovi->output_width, ovi->output_height, GS_R16);
+			info->width, info->height, GS_R16);
 		if (!video->copy_surfaces[i][0])
 			return false;
 		video->copy_surfaces[i][1] = gs_stagesurface_create(
-			ovi->output_width / 2, ovi->output_height / 2, GS_RG16);
+			info->width / 2, info->height / 2, GS_RG16);
 		if (!video->copy_surfaces[i][1])
 			return false;
 		break;
@@ -333,9 +330,10 @@ static bool obs_init_gpu_copy_surfaces(struct obs_video_info *ovi, size_t i)
 	return true;
 }
 
-static bool obs_init_textures(struct obs_video_info *ovi)
+static bool obs_init_textures(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
+	const struct video_output_info *info =
+		video_output_get_info(video->video);
 
 	bool success = true;
 
@@ -343,16 +341,16 @@ static bool obs_init_textures(struct obs_video_info *ovi)
 #ifdef _WIN32
 		if (video->using_nv12_tex) {
 			video->copy_surfaces_encode[i] =
-				gs_stagesurface_create_nv12(ovi->output_width,
-							    ovi->output_height);
+				gs_stagesurface_create_nv12(info->width,
+							    info->height);
 			if (!video->copy_surfaces_encode[i]) {
 				success = false;
 				break;
 			}
 		} else if (video->using_p010_tex) {
 			video->copy_surfaces_encode[i] =
-				gs_stagesurface_create_p010(ovi->output_width,
-							    ovi->output_height);
+				gs_stagesurface_create_p010(info->width,
+							    info->height);
 			if (!video->copy_surfaces_encode[i]) {
 				success = false;
 				break;
@@ -361,13 +359,13 @@ static bool obs_init_textures(struct obs_video_info *ovi)
 #endif
 
 		if (video->gpu_conversion) {
-			if (!obs_init_gpu_copy_surfaces(ovi, i)) {
+			if (!obs_init_gpu_copy_surfaces(video, i)) {
 				success = false;
 				break;
 			}
 		} else {
 			video->copy_surfaces[i][0] = gs_stagesurface_create(
-				ovi->output_width, ovi->output_height, GS_RGBA);
+				info->width, info->height, GS_RGBA);
 			if (!video->copy_surfaces[i][0]) {
 				success = false;
 				break;
@@ -376,7 +374,7 @@ static bool obs_init_textures(struct obs_video_info *ovi)
 	}
 
 	enum gs_color_format format = GS_RGBA;
-	switch (ovi->output_format) {
+	switch (info->format) {
 	case VIDEO_FORMAT_I010:
 	case VIDEO_FORMAT_P010:
 	case VIDEO_FORMAT_I210:
@@ -386,28 +384,27 @@ static bool obs_init_textures(struct obs_video_info *ovi)
 	}
 
 	enum gs_color_space space = GS_CS_SRGB;
-	switch (ovi->colorspace) {
+	switch (info->colorspace) {
 	case VIDEO_CS_2100_PQ:
 	case VIDEO_CS_2100_HLG:
 		space = GS_CS_709_EXTENDED;
 		break;
 	default:
-		switch (ovi->output_format) {
+		switch (info->format) {
 		case VIDEO_FORMAT_I010:
 		case VIDEO_FORMAT_P010:
 			space = GS_CS_SRGB_16F;
 		}
 	}
 
-	video->render_texture = gs_texture_create(ovi->base_width,
-						  ovi->base_height, format, 1,
-						  NULL, GS_RENDER_TARGET);
+	video->render_texture =
+		gs_texture_create(obs->video.base_width, obs->video.base_height,
+				  format, 1, NULL, GS_RENDER_TARGET);
 	if (!video->render_texture)
 		success = false;
 
-	video->output_texture = gs_texture_create(ovi->output_width,
-						  ovi->output_height, format, 1,
-						  NULL, GS_RENDER_TARGET);
+	video->output_texture = gs_texture_create(
+		info->width, info->height, format, 1, NULL, GS_RENDER_TARGET);
 	if (!video->output_texture)
 		success = false;
 
@@ -558,15 +555,15 @@ static int obs_init_graphics(struct obs_video_info *ovi)
 	return success ? OBS_VIDEO_SUCCESS : OBS_VIDEO_FAIL;
 }
 
-static inline void set_video_matrix(struct obs_core_video *video,
-				    struct obs_video_info *ovi)
+static inline void set_video_matrix(struct obs_core_video_mix *video,
+				    struct video_output_info *info)
 {
 	struct matrix4 mat;
 	struct vec4 r_row;
 
-	if (format_is_yuv(ovi->output_format)) {
+	if (format_is_yuv(info->format)) {
 		video_format_get_parameters_for_format(
-			ovi->colorspace, ovi->range, ovi->output_format,
+			info->colorspace, info->range, info->format,
 			(float *)&mat, NULL, NULL);
 		matrix4_inv(&mat, &mat);
 
@@ -581,24 +578,23 @@ static inline void set_video_matrix(struct obs_core_video *video,
 	memcpy(video->color_matrix, &mat, sizeof(float) * 16);
 }
 
-static int obs_init_video(struct obs_video_info *ovi)
+int obs_init_video_mix(struct obs_video_info *ovi,
+		       struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
 	struct video_output_info vi;
-	int errorcode;
+
+	pthread_mutex_init_value(&video->gpu_encoder_mutex);
 
 	make_video_info(&vi, ovi);
-	video->base_width = ovi->base_width;
-	video->base_height = ovi->base_height;
-	video->output_width = ovi->output_width;
-	video->output_height = ovi->output_height;
 	video->gpu_conversion = ovi->gpu_conversion;
 	video->scale_type = ovi->scale_type;
+	video->gpu_was_active = false;
+	video->raw_was_active = false;
+	video->was_active = false;
 
-	set_video_matrix(video, ovi);
-
-	errorcode = video_output_open(&video->video, &vi);
+	set_video_matrix(video, &vi);
 
+	int errorcode = video_output_open(&video->video, &vi);
 	if (errorcode != VIDEO_OUTPUT_SUCCESS) {
 		if (errorcode == VIDEO_OUTPUT_INVALIDPARAM) {
 			blog(LOG_ERROR, "Invalid video parameters specified");
@@ -609,20 +605,37 @@ static int obs_init_video(struct obs_video_info *ovi)
 		return OBS_VIDEO_FAIL;
 	}
 
-	gs_enter_context(video->graphics);
+	if (pthread_mutex_init(&video->gpu_encoder_mutex, NULL) < 0)
+		return OBS_VIDEO_FAIL;
 
-	if (ovi->gpu_conversion && !obs_init_gpu_conversion(ovi))
+	gs_enter_context(obs->video.graphics);
+
+	if (video->gpu_conversion && !obs_init_gpu_conversion(video))
 		return OBS_VIDEO_FAIL;
-	if (!obs_init_textures(ovi))
+	if (!obs_init_textures(video))
 		return OBS_VIDEO_FAIL;
 
 	gs_leave_context();
 
-	if (pthread_mutex_init(&video->gpu_encoder_mutex, NULL) < 0)
-		return OBS_VIDEO_FAIL;
+	return OBS_VIDEO_SUCCESS;
+}
+
+static int obs_init_video(struct obs_video_info *ovi)
+{
+	struct obs_core_video *video = &obs->video;
+	video->base_width = ovi->base_width;
+	video->base_height = ovi->base_height;
+	video->video_frame_interval_ns =
+		util_mul_div64(1000000000ULL, ovi->fps_den, ovi->fps_num);
+	video->video_half_frame_interval_ns =
+		util_mul_div64(500000000ULL, ovi->fps_den, ovi->fps_num);
+
 	if (pthread_mutex_init(&video->task_mutex, NULL) < 0)
 		return OBS_VIDEO_FAIL;
+	if (pthread_mutex_init(&video->mixes_mutex, NULL) < 0)
+		return OBS_VIDEO_FAIL;
 
+	int errorcode;
 #ifdef __APPLE__
 	errorcode = pthread_create(&video->video_thread, NULL,
 				   obs_graphics_thread_autorelease, obs);
@@ -635,84 +648,90 @@ static int obs_init_video(struct obs_video_info *ovi)
 
 	video->thread_initialized = true;
 	video->ovi = *ovi;
+
+	if (!obs_view_add(&obs->data.main_view))
+		return OBS_VIDEO_FAIL;
+
 	return OBS_VIDEO_SUCCESS;
 }
 
 static void stop_video(void)
 {
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++)
+		video_output_stop(obs->video.mixes.array[i].video);
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
 	struct obs_core_video *video = &obs->video;
 	void *thread_retval;
 
-	if (video->video) {
-		video_output_stop(video->video);
-		if (video->thread_initialized) {
-			pthread_join(video->video_thread, &thread_retval);
-			video->thread_initialized = false;
-		}
+	if (video->thread_initialized) {
+		pthread_join(video->video_thread, &thread_retval);
+		video->thread_initialized = false;
 	}
 }
 
-static void obs_free_video(void)
+static void obs_free_render_textures(struct obs_core_video_mix *video)
 {
-	struct obs_core_video *video = &obs->video;
-
-	if (video->video) {
-		video_output_close(video->video);
-		video->video = NULL;
+	if (!obs->video.graphics)
+		return;
 
-		if (!video->graphics)
-			return;
+	gs_enter_context(obs->video.graphics);
 
-		gs_enter_context(video->graphics);
+	for (size_t c = 0; c < NUM_CHANNELS; c++) {
+		if (video->mapped_surfaces[c]) {
+			gs_stagesurface_unmap(video->mapped_surfaces[c]);
+			video->mapped_surfaces[c] = NULL;
+		}
+	}
 
+	for (size_t i = 0; i < NUM_TEXTURES; i++) {
 		for (size_t c = 0; c < NUM_CHANNELS; c++) {
-			if (video->mapped_surfaces[c]) {
-				gs_stagesurface_unmap(
-					video->mapped_surfaces[c]);
-				video->mapped_surfaces[c] = NULL;
+			if (video->copy_surfaces[i][c]) {
+				gs_stagesurface_destroy(
+					video->copy_surfaces[i][c]);
+				video->copy_surfaces[i][c] = NULL;
 			}
-		}
-
-		for (size_t i = 0; i < NUM_TEXTURES; i++) {
-			for (size_t c = 0; c < NUM_CHANNELS; c++) {
-				if (video->copy_surfaces[i][c]) {
-					gs_stagesurface_destroy(
-						video->copy_surfaces[i][c]);
-					video->copy_surfaces[i][c] = NULL;
-				}
 
-				video->active_copy_surfaces[i][c] = NULL;
-			}
+			video->active_copy_surfaces[i][c] = NULL;
+		}
 #ifdef _WIN32
-			if (video->copy_surfaces_encode[i]) {
-				gs_stagesurface_destroy(
-					video->copy_surfaces_encode[i]);
-				video->copy_surfaces_encode[i] = NULL;
-			}
-#endif
+		if (video->copy_surfaces_encode[i]) {
+			gs_stagesurface_destroy(video->copy_surfaces_encode[i]);
+			video->copy_surfaces_encode[i] = NULL;
 		}
+#endif
+	}
 
-		gs_texture_destroy(video->render_texture);
+	gs_texture_destroy(video->render_texture);
 
-		for (size_t c = 0; c < NUM_CHANNELS; c++) {
-			if (video->convert_textures[c]) {
-				gs_texture_destroy(video->convert_textures[c]);
-				video->convert_textures[c] = NULL;
-			}
+	for (size_t c = 0; c < NUM_CHANNELS; c++) {
+		if (video->convert_textures[c]) {
+			gs_texture_destroy(video->convert_textures[c]);
+			video->convert_textures[c] = NULL;
+		}
 #ifdef _WIN32
-			if (video->convert_textures_encode[c]) {
-				gs_texture_destroy(
-					video->convert_textures_encode[c]);
-				video->convert_textures_encode[c] = NULL;
-			}
-#endif
+		if (video->convert_textures_encode[c]) {
+			gs_texture_destroy(video->convert_textures_encode[c]);
+			video->convert_textures_encode[c] = NULL;
 		}
+#endif
+	}
 
-		gs_texture_destroy(video->output_texture);
-		video->render_texture = NULL;
-		video->output_texture = NULL;
+	gs_texture_destroy(video->output_texture);
+	video->render_texture = NULL;
+	video->output_texture = NULL;
 
-		gs_leave_context();
+	gs_leave_context();
+}
+
+void obs_free_video_mix(struct obs_core_video_mix *video)
+{
+	if (video->video) {
+		video_output_close(video->video);
+		video->video = NULL;
+
+		obs_free_render_textures(video);
 
 		circlebuf_free(&video->vframe_info_buffer);
 		circlebuf_free(&video->vframe_info_buffer_gpu);
@@ -726,15 +745,30 @@ static void obs_free_video(void)
 		pthread_mutex_init_value(&video->gpu_encoder_mutex);
 		da_free(video->gpu_encoders);
 
-		pthread_mutex_destroy(&video->task_mutex);
-		pthread_mutex_init_value(&video->task_mutex);
-		circlebuf_free(&video->tasks);
-
 		video->gpu_encoder_active = 0;
 		video->cur_texture = 0;
 	}
 }
 
+static void obs_free_video(void)
+{
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	size_t num = obs->video.mixes.num;
+	if (num)
+		blog(LOG_WARNING, "%d views remain at shutdown", num);
+	for (size_t i = 0; i < num; i++)
+		obs_free_video_mix(obs->video.mixes.array + i);
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	pthread_mutex_destroy(&obs->video.mixes_mutex);
+	pthread_mutex_init_value(&obs->video.mixes_mutex);
+	da_free(obs->video.mixes);
+
+	pthread_mutex_destroy(&obs->video.task_mutex);
+	pthread_mutex_init_value(&obs->video.task_mutex);
+	circlebuf_free(&obs->video.tasks);
+}
+
 static void obs_free_graphics(void)
 {
 	struct obs_core_video *video = &obs->video;
@@ -892,6 +926,7 @@ static void obs_free_data(void)
 
 	data->valid = false;
 
+	obs_view_remove(&data->main_view);
 	obs_main_view_free(&data->main_view);
 
 	blog(LOG_INFO, "Freeing OBS context data");
@@ -1057,8 +1092,8 @@ static bool obs_init(const char *locale, const char *module_config_path,
 
 	pthread_mutex_init_value(&obs->audio.monitoring_mutex);
 	pthread_mutex_init_value(&obs->audio.task_mutex);
-	pthread_mutex_init_value(&obs->video.gpu_encoder_mutex);
 	pthread_mutex_init_value(&obs->video.task_mutex);
+	pthread_mutex_init_value(&obs->video.mixes_mutex);
 
 	obs->name_store_owned = !store;
 	obs->name_store = store ? store : profiler_name_store_create();
@@ -1331,15 +1366,13 @@ int obs_reset_video(struct obs_video_info *ovi)
 		return OBS_VIDEO_FAIL;
 
 	/* don't allow changing of video settings if active. */
-	if (obs->video.video && obs_video_active())
+	if (obs_video_active())
 		return OBS_VIDEO_CURRENTLY_ACTIVE;
 
 	if (!size_valid(ovi->output_width, ovi->output_height) ||
 	    !size_valid(ovi->base_width, ovi->base_height))
 		return OBS_VIDEO_INVALID_PARAM;
 
-	struct obs_core_video *video = &obs->video;
-
 	stop_video();
 	obs_free_video();
 
@@ -1347,7 +1380,7 @@ int obs_reset_video(struct obs_video_info *ovi)
 	ovi->output_width &= 0xFFFFFFFC;
 	ovi->output_height &= 0xFFFFFFFE;
 
-	if (!video->graphics) {
+	if (!obs->video.graphics) {
 		int errorcode = obs_init_graphics(ovi);
 		if (errorcode != OBS_VIDEO_SUCCESS) {
 			obs_free_graphics();
@@ -1461,12 +1494,10 @@ bool obs_reset_audio(const struct obs_audio_info *oai)
 
 bool obs_get_video_info(struct obs_video_info *ovi)
 {
-	struct obs_core_video *video = &obs->video;
-
-	if (!video->graphics)
+	if (!obs->video.graphics)
 		return false;
 
-	*ovi = video->ovi;
+	*ovi = obs->video.ovi;
 	return true;
 }
 
@@ -1617,7 +1648,7 @@ audio_t *obs_get_audio(void)
 
 video_t *obs_get_video(void)
 {
-	return obs->video.video;
+	return obs->video.main_mix->video;
 }
 
 /* TODO: optimize this later so it's not just O(N) string lookups */
@@ -1974,12 +2005,12 @@ static void obs_render_main_texture_internal(enum gs_blend_type src_c,
 					     enum gs_blend_type src_a,
 					     enum gs_blend_type dest_a)
 {
-	struct obs_core_video *video;
+	struct obs_core_video_mix *video;
 	gs_texture_t *tex;
 	gs_effect_t *effect;
 	gs_eparam_t *param;
 
-	video = &obs->video;
+	video = obs->video.main_mix;
 	if (!video->texture_rendered)
 		return;
 
@@ -2033,9 +2064,9 @@ void obs_render_main_texture_src_color_only(void)
 
 gs_texture_t *obs_get_main_texture(void)
 {
-	struct obs_core_video *video;
+	struct obs_core_video_mix *video;
 
-	video = &obs->video;
+	video = obs->video.main_mix;
 	if (!video->texture_rendered)
 		return NULL;
 
@@ -2678,12 +2709,31 @@ uint32_t obs_get_lagged_frames(void)
 	return obs->video.lagged_frames;
 }
 
+struct obs_core_video_mix *get_mix_for_video(video_t *v)
+{
+	struct obs_core_video_mix *result = NULL;
+
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++) {
+		struct obs_core_video_mix *mix = obs->video.mixes.array + i;
+
+		if (v == mix->video) {
+			result = mix;
+			break;
+		}
+	}
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	return result;
+}
+
 void start_raw_video(video_t *v, const struct video_scale_info *conversion,
 		     void (*callback)(void *param, struct video_data *frame),
 		     void *param)
 {
-	struct obs_core_video *video = &obs->video;
-	os_atomic_inc_long(&video->raw_active);
+	struct obs_core_video_mix *video = get_mix_for_video(v);
+	if (video)
+		os_atomic_inc_long(&video->raw_active);
 	video_output_connect(v, conversion, callback, param);
 }
 
@@ -2691,8 +2741,9 @@ void stop_raw_video(video_t *v,
 		    void (*callback)(void *param, struct video_data *frame),
 		    void *param)
 {
-	struct obs_core_video *video = &obs->video;
-	os_atomic_dec_long(&video->raw_active);
+	struct obs_core_video_mix *video = get_mix_for_video(v);
+	if (video)
+		os_atomic_dec_long(&video->raw_active);
 	video_output_disconnect(v, callback, param);
 }
 
@@ -2701,7 +2752,7 @@ void obs_add_raw_video_callback(const struct video_scale_info *conversion,
 						 struct video_data *frame),
 				void *param)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	start_raw_video(video->video, conversion, callback, param);
 }
 
@@ -2709,7 +2760,7 @@ void obs_remove_raw_video_callback(void (*callback)(void *param,
 						    struct video_data *frame),
 				   void *param)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	stop_raw_video(video->video, callback, param);
 }
 
@@ -2752,13 +2803,13 @@ obs_data_t *obs_get_private_data(void)
 	return private_data;
 }
 
-extern bool init_gpu_encoding(struct obs_core_video *video);
-extern void stop_gpu_encoding_thread(struct obs_core_video *video);
-extern void free_gpu_encoding(struct obs_core_video *video);
+extern bool init_gpu_encoding(struct obs_core_video_mix *video);
+extern void stop_gpu_encoding_thread(struct obs_core_video_mix *video);
+extern void free_gpu_encoding(struct obs_core_video_mix *video);
 
 bool start_gpu_encode(obs_encoder_t *encoder)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	bool success = true;
 
 	obs_enter_graphics();
@@ -2784,7 +2835,7 @@ bool start_gpu_encode(obs_encoder_t *encoder)
 
 void stop_gpu_encode(obs_encoder_t *encoder)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	bool call_free = false;
 
 	os_atomic_dec_long(&video->gpu_encoder_active);
@@ -2811,21 +2862,32 @@ void stop_gpu_encode(obs_encoder_t *encoder)
 
 bool obs_video_active(void)
 {
-	struct obs_core_video *video = &obs->video;
+	bool result = false;
+
+	pthread_mutex_lock(&obs->video.mixes_mutex);
+	for (size_t i = 0, num = obs->video.mixes.num; i < num; i++) {
+		struct obs_core_video_mix *video = obs->video.mixes.array + i;
 
-	return os_atomic_load_long(&video->raw_active) > 0 ||
-	       os_atomic_load_long(&video->gpu_encoder_active) > 0;
+		if (os_atomic_load_long(&video->raw_active) > 0 ||
+		    os_atomic_load_long(&video->gpu_encoder_active) > 0) {
+			result = true;
+			break;
+		}
+	}
+	pthread_mutex_unlock(&obs->video.mixes_mutex);
+
+	return result;
 }
 
 bool obs_nv12_tex_active(void)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	return video->using_nv12_tex;
 }
 
 bool obs_p010_tex_active(void)
 {
-	struct obs_core_video *video = &obs->video;
+	struct obs_core_video_mix *video = obs->video.main_mix;
 	return video->using_p010_tex;
 }
 

+ 6 - 0
libobs/obs.h

@@ -909,6 +909,12 @@ EXPORT obs_source_t *obs_view_get_source(obs_view_t *view, uint32_t channel);
 /** Renders the sources of this view context */
 EXPORT void obs_view_render(obs_view_t *view);
 
+/** Adds a view to the main render loop */
+EXPORT video_t *obs_view_add(obs_view_t *view);
+
+/** Removes a view from the main render loop */
+EXPORT void obs_view_remove(obs_view_t *view);
+
 /* ------------------------------------------------------------------------- */
 /* Display context */