1
0
Эх сурвалжийг харах

frontend: Prepare Qt UI autoconfig wizards for splits

PatTheMav 10 сар өмнө
parent
commit
c792be35a1

+ 0 - 0
UI/window-basic-auto-config.cpp → frontend/wizards/AutoConfig.cpp


+ 0 - 0
UI/window-basic-auto-config.hpp → frontend/wizards/AutoConfig.hpp


+ 1240 - 0
frontend/wizards/AutoConfigStartPage.cpp

@@ -0,0 +1,1240 @@
+#include <QMessageBox>
+#include <QScreen>
+
+#include <obs.hpp>
+#include <qt-wrappers.hpp>
+
+#include <nlohmann/json.hpp>
+
+#include "moc_window-basic-auto-config.cpp"
+#include "window-basic-main.hpp"
+#include "obs-app.hpp"
+#include "url-push-button.hpp"
+
+#include "goliveapi-postdata.hpp"
+#include "goliveapi-network.hpp"
+#include "multitrack-video-error.hpp"
+
+#include "ui_AutoConfigStartPage.h"
+#include "ui_AutoConfigVideoPage.h"
+#include "ui_AutoConfigStreamPage.h"
+
+#ifdef BROWSER_AVAILABLE
+#include <browser-panel.hpp>
+#endif
+
+#include "auth-oauth.hpp"
+#include "ui-config.h"
+#ifdef YOUTUBE_ENABLED
+#include "youtube-api-wrappers.hpp"
+#endif
+
+struct QCef;
+struct QCefCookieManager;
+
+extern QCef *cef;
+extern QCefCookieManager *panel_cookies;
+
+#define wiz reinterpret_cast<AutoConfig *>(wizard())
+
+/* ------------------------------------------------------------------------- */
+
+constexpr std::string_view OBSServiceFileName = "service.json";
+
+static OBSData OpenServiceSettings(std::string &type)
+{
+	const OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName);
+
+	if (!std::filesystem::exists(jsonFilePath)) {
+		return OBSData();
+	}
+
+	OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak");
+
+	obs_data_set_default_string(data, "type", "rtmp_common");
+	type = obs_data_get_string(data, "type");
+
+	OBSDataAutoRelease settings = obs_data_get_obj(data, "settings");
+
+	return settings.Get();
+}
+
+static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key)
+{
+	OBSData settings = OpenServiceSettings(type);
+
+	service = obs_data_get_string(settings, "service");
+	server = obs_data_get_string(settings, "server");
+	key = obs_data_get_string(settings, "key");
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage)
+{
+	ui->setupUi(this);
+	setTitle(QTStr("Basic.AutoConfig.StartPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle"));
+
+	OBSBasic *main = OBSBasic::Get();
+	if (main->VCamEnabled()) {
+		QRadioButton *prioritizeVCam =
+			new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this);
+		QBoxLayout *box = reinterpret_cast<QBoxLayout *>(layout());
+		box->insertWidget(2, prioritizeVCam);
+
+		connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam);
+	}
+}
+
+AutoConfigStartPage::~AutoConfigStartPage() {}
+
+int AutoConfigStartPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage;
+}
+
+void AutoConfigStartPage::on_prioritizeStreaming_clicked()
+{
+	wiz->type = AutoConfig::Type::Streaming;
+}
+
+void AutoConfigStartPage::on_prioritizeRecording_clicked()
+{
+	wiz->type = AutoConfig::Type::Recording;
+}
+
+void AutoConfigStartPage::PrioritizeVCam()
+{
+	wiz->type = AutoConfig::Type::VirtualCam;
+}
+
+/* ------------------------------------------------------------------------- */
+
+#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x
+#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent")
+#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display")
+#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent")
+#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS")
+#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes")
+
+AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage)
+{
+	ui->setupUi(this);
+
+	setTitle(QTStr("Basic.AutoConfig.VideoPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle"));
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den;
+
+	QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g');
+
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS);
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes);
+	ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent);
+	ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30);
+	ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60);
+	ui->fps->setCurrentIndex(0);
+
+	QString cxStr = QString::number(ovi.base_width);
+	QString cyStr = QString::number(ovi.base_height);
+
+	int encRes = int(ovi.base_width << 16) | int(ovi.base_height);
+
+	// Auto config only supports testing down to 240p, don't allow current
+	// resolution if it's lower than that.
+	if (ovi.base_height >= 240)
+		ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes);
+
+	QList<QScreen *> screens = QGuiApplication::screens();
+	for (int i = 0; i < screens.size(); i++) {
+		QScreen *screen = screens[i];
+		QSize as = screen->size();
+		int as_width = as.width();
+		int as_height = as.height();
+
+		// Calculate physical screen resolution based on the virtual screen resolution
+		// They might differ if scaling is enabled, e.g. for HiDPI screens
+		as_width = round(as_width * screen->devicePixelRatio());
+		as_height = round(as_height * screen->devicePixelRatio());
+
+		encRes = as_width << 16 | as_height;
+
+		QString str =
+			QTStr(RES_USE_DISPLAY)
+				.arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height));
+
+		ui->canvasRes->addItem(str, encRes);
+	}
+
+	auto addRes = [&](int cx, int cy) {
+		encRes = (cx << 16) | cy;
+		QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy));
+		ui->canvasRes->addItem(str, encRes);
+	};
+
+	addRes(1920, 1080);
+	addRes(1280, 720);
+
+	ui->canvasRes->setCurrentIndex(0);
+}
+
+AutoConfigVideoPage::~AutoConfigVideoPage() {}
+
+int AutoConfigVideoPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage;
+}
+
+bool AutoConfigVideoPage::validatePage()
+{
+	int encRes = ui->canvasRes->currentData().toInt();
+	wiz->baseResolutionCX = encRes >> 16;
+	wiz->baseResolutionCY = encRes & 0xFFFF;
+	wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt();
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	switch (wiz->fpsType) {
+	case AutoConfig::FPSType::PreferHighFPS:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = true;
+		break;
+	case AutoConfig::FPSType::PreferHighRes:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::UseCurrent:
+		wiz->specificFPSNum = ovi.fps_num;
+		wiz->specificFPSDen = ovi.fps_den;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps30:
+		wiz->specificFPSNum = 30;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps60:
+		wiz->specificFPSNum = 60;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	}
+
+	return true;
+}
+
+/* ------------------------------------------------------------------------- */
+
+enum class ListOpt : int {
+	ShowAll = 1,
+	Custom,
+};
+
+AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage)
+{
+	ui->setupUi(this);
+	ui->bitrateLabel->setVisible(false);
+	ui->bitrate->setVisible(false);
+	ui->connectAccount2->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+	ui->useMultitrackVideo->setVisible(false);
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	int vertSpacing = ui->topLayout->verticalSpacing();
+
+	QMargins m = ui->topLayout->contentsMargins();
+	m.setBottom(vertSpacing / 2);
+	ui->topLayout->setContentsMargins(m);
+
+	m = ui->loginPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->loginPageLayout->setContentsMargins(m);
+
+	m = ui->streamkeyPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->streamkeyPageLayout->setContentsMargins(m);
+
+	setTitle(QTStr("Basic.AutoConfig.StreamPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle"));
+
+	LoadServices(false);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink);
+
+	connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() {
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+	});
+
+	connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+}
+
+AutoConfigStreamPage::~AutoConfigStreamPage() {}
+
+bool AutoConfigStreamPage::isComplete() const
+{
+	return ready;
+}
+
+int AutoConfigStreamPage::nextId() const
+{
+	return AutoConfig::TestPage;
+}
+
+inline bool AutoConfigStreamPage::IsCustomService() const
+{
+	return ui->service->currentData().toInt() == (int)ListOpt::Custom;
+}
+
+bool AutoConfigStreamPage::validatePage()
+{
+	OBSDataAutoRelease service_settings = obs_data_create();
+
+	wiz->customServer = IsCustomService();
+
+	const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common";
+
+	if (!wiz->customServer) {
+		obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText()));
+	}
+
+	OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr);
+
+	int bitrate;
+	if (!ui->doBandwidthTest->isChecked()) {
+		bitrate = ui->bitrate->value();
+		wiz->idealBitrate = bitrate;
+	} else {
+		/* Default test target is 10 Mbps */
+		bitrate = 10000;
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(wiz->serviceName)) {
+			/* Adjust upper bound to YouTube limits
+			 * for resolutions above 1080p */
+			if (wiz->baseResolutionCY > 1440)
+				bitrate = 51000;
+			else if (wiz->baseResolutionCY > 1080)
+				bitrate = 18000;
+		}
+#endif
+	}
+
+	OBSDataAutoRelease settings = obs_data_create();
+	obs_data_set_int(settings, "bitrate", bitrate);
+	obs_service_apply_encoder_settings(service, settings, nullptr);
+
+	if (wiz->customServer) {
+		QString server = ui->customServer->text().trimmed();
+		wiz->server = wiz->serverName = QT_TO_UTF8(server);
+	} else {
+		wiz->serverName = QT_TO_UTF8(ui->server->currentText());
+		wiz->server = QT_TO_UTF8(ui->server->currentData().toString());
+	}
+
+	wiz->bandwidthTest = ui->doBandwidthTest->isChecked();
+	wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate");
+	wiz->idealBitrate = wiz->startingBitrate;
+	wiz->regionUS = ui->regionUS->isChecked();
+	wiz->regionEU = ui->regionEU->isChecked();
+	wiz->regionAsia = ui->regionAsia->isChecked();
+	wiz->regionOther = ui->regionOther->isChecked();
+	wiz->serviceName = QT_TO_UTF8(ui->service->currentText());
+	if (ui->preferHardware)
+		wiz->preferHardware = ui->preferHardware->isChecked();
+	wiz->key = QT_TO_UTF8(ui->key->text());
+
+	if (!wiz->customServer) {
+		if (wiz->serviceName == "Twitch")
+			wiz->service = AutoConfig::Service::Twitch;
+#ifdef YOUTUBE_ENABLED
+		else if (IsYouTubeService(wiz->serviceName))
+			wiz->service = AutoConfig::Service::YouTube;
+#endif
+		else if (wiz->serviceName == "Amazon IVS")
+			wiz->service = AutoConfig::Service::AmazonIVS;
+		else
+			wiz->service = AutoConfig::Service::Other;
+	} else {
+		wiz->service = AutoConfig::Service::Other;
+	}
+
+	if (wiz->service == AutoConfig::Service::Twitch) {
+		wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked();
+
+		if (wiz->testMultitrackVideo) {
+			auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt,
+							    std::nullopt, false);
+
+			OBSDataAutoRelease service_settings = obs_service_get_settings(service);
+			auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+			if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+				multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+			}
+
+			try {
+				auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service),
+								   postData, multitrack_video_name);
+
+				for (const auto &endpoint : config.ingest_endpoints) {
+					if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0)
+						continue;
+
+					std::string address = endpoint.url_template;
+					auto pos = address.find("/{stream_key}");
+					if (pos != address.npos)
+						address.erase(pos);
+
+					wiz->serviceConfigServers.push_back({address, address});
+				}
+
+				int multitrackVideoBitrate = 0;
+				for (auto &encoder_config : config.encoder_configurations) {
+					auto it = encoder_config.settings.find("bitrate");
+					if (it == encoder_config.settings.end())
+						continue;
+
+					if (!it->is_number_integer())
+						continue;
+
+					int bitrate = 0;
+					it->get_to(bitrate);
+					multitrackVideoBitrate += bitrate;
+				}
+
+				if (multitrackVideoBitrate > 0) {
+					wiz->startingBitrate = multitrackVideoBitrate;
+					wiz->idealBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.testSuccessful = true;
+				}
+			} catch (const MultitrackVideoError & /*err*/) {
+				// FIXME: do something sensible
+			}
+		}
+	}
+
+	if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube &&
+	    wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) {
+		QMessageBox::StandardButton button;
+#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x)
+		button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text"));
+#undef WARNING_TEXT
+
+		if (button == QMessageBox::No)
+			return false;
+	}
+
+	return true;
+}
+
+void AutoConfigStreamPage::on_show_clicked()
+{
+	if (ui->key->echoMode() == QLineEdit::Password) {
+		ui->key->setEchoMode(QLineEdit::Normal);
+		ui->show->setText(QTStr("Hide"));
+	} else {
+		ui->key->setEchoMode(QLineEdit::Password);
+		ui->show->setText(QTStr("Show"));
+	}
+}
+
+void AutoConfigStreamPage::OnOAuthStreamKeyConnected()
+{
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+
+	if (a) {
+		bool validKey = !a->key().empty();
+
+		if (validKey)
+			ui->key->setText(QT_UTF8(a->key().c_str()));
+
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+
+		ui->connectedAccountLabel->setVisible(false);
+		ui->connectedAccountText->setVisible(false);
+
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(a->service())) {
+			ui->key->clear();
+
+			ui->connectedAccountLabel->setVisible(true);
+			ui->connectedAccountText->setVisible(true);
+
+			ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title"));
+
+			YoutubeApiWrappers *ytAuth = reinterpret_cast<YoutubeApiWrappers *>(a);
+			ChannelDescription cd;
+			if (ytAuth->GetChannelDescription(cd)) {
+				ui->connectedAccountText->setText(cd.title);
+
+				/* Create throwaway stream key for bandwidth test */
+				if (ui->doBandwidthTest->isChecked()) {
+					StreamDescription stream = {"", "", "OBS Studio Test Stream"};
+					if (ytAuth->InsertStream(stream)) {
+						ui->key->setText(stream.name);
+					}
+				}
+			}
+		}
+#endif
+	}
+
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::OnAuthConnected()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	Auth::Type type = Auth::AuthType(service);
+
+	if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) {
+		OnOAuthStreamKeyConnected();
+	}
+}
+
+void AutoConfigStreamPage::on_connectAccount_clicked()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+	OAuth::DeleteCookies(service);
+
+	auth = OAuthStreamKey::Login(this, service);
+	if (!!auth) {
+		OnAuthConnected();
+
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+}
+
+#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title"
+#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text"
+
+void AutoConfigStreamPage::on_disconnectAccount_clicked()
+{
+	QMessageBox::StandardButton button;
+
+	button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT));
+
+	if (button == QMessageBox::No) {
+		return;
+	}
+
+	OBSBasic *main = OBSBasic::Get();
+
+	main->auth.reset();
+	auth.reset();
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+#ifdef BROWSER_AVAILABLE
+	OAuth::DeleteCookies(service);
+#endif
+
+	reset_service_ui_fields(service);
+
+	ui->streamKeyWidget->setVisible(true);
+	ui->streamKeyLabel->setVisible(true);
+	ui->key->setText("");
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	/* Restore key link when disconnecting account */
+	UpdateKeyLink();
+}
+
+void AutoConfigStreamPage::on_useStreamKey_clicked()
+{
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::on_preferHardware_clicked()
+{
+	auto *main = OBSBasic::Get();
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+
+	ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked());
+	ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked());
+	ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled);
+}
+
+static inline bool is_auth_service(const std::string &service)
+{
+	return Auth::AuthType(service) != Auth::Type::None;
+}
+
+static inline bool is_external_oauth(const std::string &service)
+{
+	return Auth::External(service);
+}
+
+void AutoConfigStreamPage::reset_service_ui_fields(std::string &service)
+{
+#ifdef YOUTUBE_ENABLED
+	// when account is already connected:
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+	if (a && service == a->service() && IsYouTubeService(a->service())) {
+		ui->connectedAccountLabel->setVisible(true);
+		ui->connectedAccountText->setVisible(true);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		return;
+	}
+#endif
+
+	bool external_oauth = is_external_oauth(service);
+	if (external_oauth) {
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(true);
+
+		ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+
+	} else if (cef) {
+		QString key = ui->key->text();
+		bool can_auth = is_auth_service(service);
+		int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey;
+
+		ui->stackedWidget->setCurrentIndex(page);
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->connectAccount2->setVisible(can_auth);
+		ui->useStreamKeyAdv->setVisible(false);
+	} else {
+		ui->connectAccount2->setVisible(false);
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+}
+
+void AutoConfigStreamPage::ServiceChanged()
+{
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+	if (showMore)
+		return;
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	bool regionBased = service == "Twitch";
+	bool testBandwidth = ui->doBandwidthTest->isChecked();
+	bool custom = IsCustomService();
+
+	bool ertmp_multitrack_video_available = service == "Twitch";
+
+	bool custom_disclaimer = false;
+	auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+	if (!custom) {
+		OBSDataAutoRelease service_settings = obs_data_create();
+		obs_data_set_string(service_settings, "service", service.c_str());
+		OBSServiceAutoRelease obs_service =
+			obs_service_create("rtmp_common", "temp service", service_settings, nullptr);
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+			multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+		}
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) {
+			ui->multitrackVideoInfo->setText(
+				obs_data_get_string(service_settings, "multitrack_video_disclaimer"));
+			custom_disclaimer = true;
+		}
+	}
+
+	if (!custom_disclaimer) {
+		ui->multitrackVideoInfo->setText(
+			QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str()));
+	}
+
+	ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setText(
+		QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name));
+	ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable);
+	ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable);
+
+	reset_service_ui_fields(service);
+
+	/* Test three closest servers if "Auto" is available for Twitch */
+	if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto))
+		regionBased = false;
+
+	ui->streamkeyPageLayout->removeWidget(ui->serverLabel);
+	ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget);
+
+	if (custom) {
+		ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(false);
+		ui->serverStackedWidget->setCurrentIndex(1);
+		ui->serverStackedWidget->setVisible(true);
+		ui->serverLabel->setVisible(true);
+	} else {
+		if (!testBandwidth)
+			ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(regionBased && testBandwidth);
+		ui->serverStackedWidget->setCurrentIndex(0);
+		ui->serverStackedWidget->setHidden(testBandwidth);
+		ui->serverLabel->setHidden(testBandwidth);
+	}
+
+	wiz->testRegions = regionBased && testBandwidth;
+
+	ui->bitrateLabel->setHidden(testBandwidth);
+	ui->bitrate->setHidden(testBandwidth);
+
+	OBSBasic *main = OBSBasic::Get();
+
+	if (main->auth) {
+		auto system_auth_service = main->auth->service();
+		bool service_check = service.find(system_auth_service) != std::string::npos;
+#ifdef YOUTUBE_ENABLED
+		service_check = service_check ? service_check
+					      : IsYouTubeService(system_auth_service) && IsYouTubeService(service);
+#endif
+		if (service_check) {
+			auth.reset();
+			auth = main->auth;
+			OnAuthConnected();
+		}
+	}
+
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::UpdateMoreInfoLink()
+{
+	if (IsCustomService()) {
+		ui->moreInfoButton->hide();
+		return;
+	}
+
+	QString serviceName = ui->service->currentText();
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	const char *more_info_link = obs_data_get_string(settings, "more_info_link");
+
+	if (!more_info_link || (*more_info_link == '\0')) {
+		ui->moreInfoButton->hide();
+	} else {
+		ui->moreInfoButton->setTargetUrl(QUrl(more_info_link));
+		ui->moreInfoButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateKeyLink()
+{
+	QString serviceName = ui->service->currentText();
+	QString customServer = ui->customServer->text().trimmed();
+	QString streamKeyLink;
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	streamKeyLink = obs_data_get_string(settings, "stream_key_link");
+
+	if (customServer.contains("fbcdn.net") && IsCustomService()) {
+		streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS";
+	}
+
+	if (serviceName == "Dacast") {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else if (!IsCustomService()) {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else {
+		/* add tooltips for stream key */
+		QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg";
+		QString lStr = "<html>%1 <img src='%2' style=' \
+				vertical-align: bottom;  \
+				' /></html>";
+
+		ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file));
+		ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip"));
+	}
+
+	if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) {
+		ui->streamKeyButton->hide();
+	} else {
+		ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink));
+		ui->streamKeyButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::LoadServices(bool showAll)
+{
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_bool(settings, "show_all", showAll);
+
+	obs_property_t *prop = obs_properties_get(props, "show_all");
+	obs_property_modified(prop, settings);
+
+	ui->service->blockSignals(true);
+	ui->service->clear();
+
+	QStringList names;
+
+	obs_property_t *services = obs_properties_get(props, "service");
+	size_t services_count = obs_property_list_item_count(services);
+	for (size_t i = 0; i < services_count; i++) {
+		const char *name = obs_property_list_item_string(services, i);
+		names.push_back(name);
+	}
+
+	if (showAll)
+		names.sort(Qt::CaseInsensitive);
+
+	for (QString &name : names)
+		ui->service->addItem(name);
+
+	if (!showAll) {
+		ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"),
+				     QVariant((int)ListOpt::ShowAll));
+	}
+
+	ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom));
+
+	if (!lastService.isEmpty()) {
+		int idx = ui->service->findText(lastService);
+		if (idx != -1)
+			ui->service->setCurrentIndex(idx);
+	}
+
+	obs_properties_destroy(props);
+
+	ui->service->blockSignals(false);
+}
+
+void AutoConfigStreamPage::UpdateServerList()
+{
+	QString serviceName = ui->service->currentText();
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+
+	if (showMore) {
+		LoadServices(true);
+		ui->service->showPopup();
+		return;
+	} else {
+		lastService = serviceName;
+	}
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	obs_property_t *servers = obs_properties_get(props, "server");
+
+	ui->server->clear();
+
+	size_t servers_count = obs_property_list_item_count(servers);
+	for (size_t i = 0; i < servers_count; i++) {
+		const char *name = obs_property_list_item_name(servers, i);
+		const char *server = obs_property_list_item_string(servers, i);
+		ui->server->addItem(name, server);
+	}
+
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateCompleted()
+{
+	const bool custom = IsCustomService();
+	if (ui->stackedWidget->currentIndex() == (int)Section::Connect ||
+	    (ui->key->text().isEmpty() && !auth && !custom)) {
+		ready = false;
+	} else {
+		if (custom) {
+			ready = !ui->customServer->text().isEmpty();
+		} else {
+			ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() ||
+				ui->regionAsia->isChecked() || ui->regionOther->isChecked();
+		}
+	}
+	emit completeChanged();
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
+{
+	EnableThreadedMessageBoxes(true);
+
+	calldata_t cd = {0};
+	calldata_set_int(&cd, "seconds", 5);
+
+	proc_handler_t *ph = obs_get_proc_handler();
+	proc_handler_call(ph, "twitch_ingests_refresh", &cd);
+	proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd);
+	calldata_free(&cd);
+
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(parent);
+	main->EnableOutputs(false);
+
+	installEventFilter(CreateShortcutFilter());
+
+	std::string serviceType;
+	GetServiceInfo(serviceType, serviceName, server, key);
+#if defined(_WIN32) || defined(__APPLE__)
+	setWizardStyle(QWizard::ModernStyle);
+#endif
+	streamPage = new AutoConfigStreamPage();
+
+	setPage(StartPage, new AutoConfigStartPage());
+	setPage(VideoPage, new AutoConfigVideoPage());
+	setPage(StreamPage, streamPage);
+	setPage(TestPage, new AutoConfigTestPage());
+	setWindowTitle(QTStr("Basic.AutoConfig"));
+	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	baseResolutionCX = ovi.base_width;
+	baseResolutionCY = ovi.base_height;
+
+	/* ----------------------------------------- */
+	/* check to see if Twitch's "auto" available */
+
+	OBSDataAutoRelease twitchSettings = obs_data_create();
+
+	obs_data_set_string(twitchSettings, "service", "Twitch");
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, twitchSettings);
+
+	obs_property_t *p = obs_properties_get(props, "server");
+	const char *first = obs_property_list_item_string(p, 0);
+	twitchAuto = strcmp(first, "auto") == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* check to see if Amazon IVS "auto" entries are available */
+
+	OBSDataAutoRelease amazonIVSSettings = obs_data_create();
+
+	obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS");
+
+	props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, amazonIVSSettings);
+
+	p = obs_properties_get(props, "server");
+	first = obs_property_list_item_string(p, 0);
+	amazonIVSAuto = strncmp(first, "auto", 4) == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* load service/servers                      */
+
+	customServer = serviceType == "rtmp_custom";
+
+	QComboBox *serviceList = streamPage->ui->service;
+
+	if (!serviceName.empty()) {
+		serviceList->blockSignals(true);
+
+		int count = serviceList->count();
+		bool found = false;
+
+		for (int i = 0; i < count; i++) {
+			QString name = serviceList->itemText(i);
+
+			if (name == serviceName.c_str()) {
+				serviceList->setCurrentIndex(i);
+				found = true;
+				break;
+			}
+		}
+
+		if (!found) {
+			serviceList->insertItem(0, serviceName.c_str());
+			serviceList->setCurrentIndex(0);
+		}
+
+		serviceList->blockSignals(false);
+	}
+
+	streamPage->UpdateServerList();
+	streamPage->UpdateKeyLink();
+	streamPage->UpdateMoreInfoLink();
+	streamPage->lastService.clear();
+
+	if (!customServer) {
+		QComboBox *serverList = streamPage->ui->server;
+		int idx = serverList->findData(QString(server.c_str()));
+		if (idx == -1)
+			idx = 0;
+
+		serverList->setCurrentIndex(idx);
+	} else {
+		streamPage->ui->customServer->setText(server.c_str());
+		int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom));
+		streamPage->ui->service->setCurrentIndex(idx);
+	}
+
+	if (!key.empty())
+		streamPage->ui->key->setText(key.c_str());
+
+	TestHardwareEncoding();
+
+	int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate");
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+	streamPage->ui->bitrate->setValue(bitrate);
+	streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled);
+	streamPage->ServiceChanged();
+
+	if (!hardwareEncodingAvailable) {
+		delete streamPage->ui->preferHardware;
+		streamPage->ui->preferHardware = nullptr;
+	} else {
+		/* Newer generations of NVENC have a high enough quality to
+		 * bitrate ratio that if NVENC is available, it makes sense to
+		 * just always prefer hardware encoding by default */
+		bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4;
+		streamPage->ui->preferHardware->setChecked(preferHardware);
+	}
+
+	setOptions(QWizard::WizardOptions());
+	setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings"));
+	setButtonText(QWizard::BackButton, QTStr("Back"));
+	setButtonText(QWizard::NextButton, QTStr("Next"));
+	setButtonText(QWizard::CancelButton, QTStr("Cancel"));
+}
+
+AutoConfig::~AutoConfig()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	main->EnableOutputs(true);
+	EnableThreadedMessageBoxes(false);
+}
+
+void AutoConfig::TestHardwareEncoding()
+{
+	size_t idx = 0;
+	const char *id;
+	while (obs_enum_encoder_types(idx++, &id)) {
+		if (strcmp(id, "ffmpeg_nvenc") == 0)
+			hardwareEncodingAvailable = nvencAvailable = true;
+		else if (strcmp(id, "obs_qsv11") == 0)
+			hardwareEncodingAvailable = qsvAvailable = true;
+		else if (strcmp(id, "h264_texture_amf") == 0)
+			hardwareEncodingAvailable = vceAvailable = true;
+#ifdef __APPLE__
+		else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0
+#ifndef __aarch64__
+			 && os_get_emulation_status() == true
+#endif
+		)
+			if (__builtin_available(macOS 13.0, *))
+				hardwareEncodingAvailable = appleAvailable = true;
+#endif
+	}
+}
+
+bool AutoConfig::CanTestServer(const char *server)
+{
+	if (!testRegions || (regionUS && regionEU && regionAsia && regionOther))
+		return true;
+
+	if (service == Service::Twitch) {
+		if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 ||
+		    astrcmp_n(server, "US Central:", 11) == 0) {
+			return regionUS;
+		} else if (astrcmp_n(server, "EU:", 3) == 0) {
+			return regionEU;
+		} else if (astrcmp_n(server, "Asia:", 5) == 0) {
+			return regionAsia;
+		} else if (regionOther) {
+			return true;
+		}
+	} else {
+		return true;
+	}
+
+	return false;
+}
+
+void AutoConfig::done(int result)
+{
+	QWizard::done(result);
+
+	if (result == QDialog::Accepted) {
+		if (type == Type::Streaming)
+			SaveStreamSettings();
+		SaveSettings();
+
+#ifdef YOUTUBE_ENABLED
+		if (YouTubeAppDock::IsYTServiceSelected()) {
+			OBSBasic *main = OBSBasic::Get();
+			main->NewYouTubeAppDock();
+		}
+#endif
+	}
+}
+
+inline const char *AutoConfig::GetEncoderId(Encoder enc)
+{
+	switch (enc) {
+	case Encoder::NVENC:
+		return SIMPLE_ENCODER_NVENC;
+	case Encoder::QSV:
+		return SIMPLE_ENCODER_QSV;
+	case Encoder::AMD:
+		return SIMPLE_ENCODER_AMD;
+	case Encoder::Apple:
+		return SIMPLE_ENCODER_APPLE_H264;
+	default:
+		return SIMPLE_ENCODER_X264;
+	}
+};
+
+void AutoConfig::SaveStreamSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	/* ---------------------------------- */
+	/* save service                       */
+
+	const char *service_id = customServer ? "rtmp_custom" : "rtmp_common";
+
+	obs_service_t *oldService = main->GetService();
+	OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService);
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	if (!customServer)
+		obs_data_set_string(settings, "service", serviceName.c_str());
+	obs_data_set_string(settings, "server", server.c_str());
+#ifdef YOUTUBE_ENABLED
+	if (!streamPage->auth || !IsYouTubeService(serviceName))
+		obs_data_set_string(settings, "key", key.c_str());
+#else
+	obs_data_set_string(settings, "key", key.c_str());
+#endif
+
+	OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData);
+
+	if (!newService)
+		return;
+
+	main->SetService(newService);
+	main->SaveService();
+	main->auth = streamPage->auth;
+	if (!!main->auth) {
+		main->auth->LoadUI();
+		main->SetBroadcastFlowEnabled(main->auth->broadcastFlow());
+	} else {
+		main->SetBroadcastFlowEnabled(false);
+	}
+
+	/* ---------------------------------- */
+	/* save stream settings               */
+
+	config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate);
+	config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder));
+	config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced");
+
+	config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful);
+
+	if (multitrackVideo.targetBitrate.has_value())
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate",
+			       *multitrackVideo.targetBitrate);
+	else
+		config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate");
+
+	if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() &&
+	    (static_cast<double>(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true);
+	} else if (multitrackVideo.bitrate.has_value()) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false);
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate",
+			       *multitrackVideo.bitrate);
+	}
+}
+
+void AutoConfig::SaveSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	if (recordingEncoder != Encoder::Stream)
+		config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder));
+
+	const char *quality = recordingQuality == Quality::High ? "Small" : "Stream";
+
+	config_set_string(main->Config(), "Output", "Mode", "Simple");
+	config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality);
+	config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX);
+	config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY);
+	config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX);
+	config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY);
+
+	if (fpsType != FPSType::UseCurrent) {
+		config_set_uint(main->Config(), "Video", "FPSType", 0);
+		config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str());
+	}
+
+	main->ResetVideo();
+	main->ResetOutputs();
+	config_save_safe(main->Config(), "tmp", nullptr);
+}

+ 288 - 0
frontend/wizards/AutoConfigStartPage.hpp

@@ -0,0 +1,288 @@
+#pragma once
+
+#include <QWizard>
+#include <QPointer>
+#include <QFormLayout>
+#include <QWizardPage>
+
+#include <condition_variable>
+#include <utility>
+#include <thread>
+#include <memory>
+#include <vector>
+#include <string>
+#include <mutex>
+#include <optional>
+
+class Ui_AutoConfigStartPage;
+class Ui_AutoConfigVideoPage;
+class Ui_AutoConfigStreamPage;
+class Ui_AutoConfigTestPage;
+
+class AutoConfigStreamPage;
+class Auth;
+
+class AutoConfig : public QWizard {
+	Q_OBJECT
+
+	friend class AutoConfigStartPage;
+	friend class AutoConfigVideoPage;
+	friend class AutoConfigStreamPage;
+	friend class AutoConfigTestPage;
+
+	enum class Type {
+		Invalid,
+		Streaming,
+		Recording,
+		VirtualCam,
+	};
+
+	enum class Service {
+		Twitch,
+		YouTube,
+		AmazonIVS,
+		Other,
+	};
+
+	enum class Encoder {
+		x264,
+		NVENC,
+		QSV,
+		AMD,
+		Apple,
+		Stream,
+	};
+
+	enum class Quality {
+		Stream,
+		High,
+	};
+
+	enum class FPSType : int {
+		PreferHighFPS,
+		PreferHighRes,
+		UseCurrent,
+		fps30,
+		fps60,
+	};
+
+	struct StreamServer {
+		std::string name;
+		std::string address;
+	};
+
+	static inline const char *GetEncoderId(Encoder enc);
+
+	AutoConfigStreamPage *streamPage = nullptr;
+
+	Service service = Service::Other;
+	Quality recordingQuality = Quality::Stream;
+	Encoder recordingEncoder = Encoder::Stream;
+	Encoder streamingEncoder = Encoder::x264;
+	Type type = Type::Streaming;
+	FPSType fpsType = FPSType::PreferHighFPS;
+	int idealBitrate = 2500;
+	struct {
+		std::optional<int> targetBitrate;
+		std::optional<int> bitrate;
+		bool testSuccessful = false;
+	} multitrackVideo;
+	int baseResolutionCX = 1920;
+	int baseResolutionCY = 1080;
+	int idealResolutionCX = 1280;
+	int idealResolutionCY = 720;
+	int idealFPSNum = 60;
+	int idealFPSDen = 1;
+	std::string serviceName;
+	std::string serverName;
+	std::string server;
+	std::vector<StreamServer> serviceConfigServers;
+	std::string key;
+
+	bool hardwareEncodingAvailable = false;
+	bool nvencAvailable = false;
+	bool qsvAvailable = false;
+	bool vceAvailable = false;
+	bool appleAvailable = false;
+
+	int startingBitrate = 2500;
+	bool customServer = false;
+	bool bandwidthTest = false;
+	bool testMultitrackVideo = false;
+	bool testRegions = true;
+	bool twitchAuto = false;
+	bool amazonIVSAuto = false;
+	bool regionUS = true;
+	bool regionEU = true;
+	bool regionAsia = true;
+	bool regionOther = true;
+	bool preferHighFPS = false;
+	bool preferHardware = false;
+	int specificFPSNum = 0;
+	int specificFPSDen = 0;
+
+	void TestHardwareEncoding();
+	bool CanTestServer(const char *server);
+
+	virtual void done(int result) override;
+
+	void SaveStreamSettings();
+	void SaveSettings();
+
+public:
+	AutoConfig(QWidget *parent);
+	~AutoConfig();
+
+	enum Page {
+		StartPage,
+		VideoPage,
+		StreamPage,
+		TestPage,
+	};
+};
+
+class AutoConfigStartPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigStartPage> ui;
+
+public:
+	AutoConfigStartPage(QWidget *parent = nullptr);
+	~AutoConfigStartPage();
+
+	virtual int nextId() const override;
+
+public slots:
+	void on_prioritizeStreaming_clicked();
+	void on_prioritizeRecording_clicked();
+	void PrioritizeVCam();
+};
+
+class AutoConfigVideoPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigVideoPage> ui;
+
+public:
+	AutoConfigVideoPage(QWidget *parent = nullptr);
+	~AutoConfigVideoPage();
+
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+};
+
+class AutoConfigStreamPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	enum class Section : int {
+		Connect,
+		StreamKey,
+	};
+
+	std::shared_ptr<Auth> auth;
+
+	std::unique_ptr<Ui_AutoConfigStreamPage> ui;
+	QString lastService;
+	bool ready = false;
+
+	void LoadServices(bool showAll);
+	inline bool IsCustomService() const;
+
+public:
+	AutoConfigStreamPage(QWidget *parent = nullptr);
+	~AutoConfigStreamPage();
+
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+
+	void OnAuthConnected();
+	void OnOAuthStreamKeyConnected();
+
+public slots:
+	void on_show_clicked();
+	void on_connectAccount_clicked();
+	void on_disconnectAccount_clicked();
+	void on_useStreamKey_clicked();
+	void on_preferHardware_clicked();
+	void ServiceChanged();
+	void UpdateKeyLink();
+	void UpdateMoreInfoLink();
+	void UpdateServerList();
+	void UpdateCompleted();
+
+	void reset_service_ui_fields(std::string &service);
+};
+
+class AutoConfigTestPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	QPointer<QFormLayout> results;
+
+	std::unique_ptr<Ui_AutoConfigTestPage> ui;
+	std::thread testThread;
+	std::condition_variable cv;
+	std::mutex m;
+	bool cancel = false;
+	bool started = false;
+
+	enum class Stage {
+		Starting,
+		BandwidthTest,
+		StreamEncoder,
+		RecordingEncoder,
+		Finished,
+	};
+
+	Stage stage = Stage::Starting;
+	bool softwareTested = false;
+
+	void StartBandwidthStage();
+	void StartStreamEncoderStage();
+	void StartRecordingEncoderStage();
+
+	void FindIdealHardwareResolution();
+	bool TestSoftwareEncoding();
+
+	void TestBandwidthThread();
+	void TestStreamEncoderThread();
+	void TestRecordingEncoderThread();
+
+	void FinalizeResults();
+
+	struct ServerInfo {
+		std::string name;
+		std::string address;
+		int bitrate = 0;
+		int ms = -1;
+
+		inline ServerInfo() {}
+
+		inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {}
+	};
+
+	void GetServers(std::vector<ServerInfo> &servers);
+
+public:
+	AutoConfigTestPage(QWidget *parent = nullptr);
+	~AutoConfigTestPage();
+
+	virtual void initializePage() override;
+	virtual void cleanupPage() override;
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+
+public slots:
+	void NextStage();
+	void UpdateMessage(QString message);
+	void Failure(QString message);
+	void Progress(int percentage);
+};

+ 1240 - 0
frontend/wizards/AutoConfigStreamPage.cpp

@@ -0,0 +1,1240 @@
+#include <QMessageBox>
+#include <QScreen>
+
+#include <obs.hpp>
+#include <qt-wrappers.hpp>
+
+#include <nlohmann/json.hpp>
+
+#include "moc_window-basic-auto-config.cpp"
+#include "window-basic-main.hpp"
+#include "obs-app.hpp"
+#include "url-push-button.hpp"
+
+#include "goliveapi-postdata.hpp"
+#include "goliveapi-network.hpp"
+#include "multitrack-video-error.hpp"
+
+#include "ui_AutoConfigStartPage.h"
+#include "ui_AutoConfigVideoPage.h"
+#include "ui_AutoConfigStreamPage.h"
+
+#ifdef BROWSER_AVAILABLE
+#include <browser-panel.hpp>
+#endif
+
+#include "auth-oauth.hpp"
+#include "ui-config.h"
+#ifdef YOUTUBE_ENABLED
+#include "youtube-api-wrappers.hpp"
+#endif
+
+struct QCef;
+struct QCefCookieManager;
+
+extern QCef *cef;
+extern QCefCookieManager *panel_cookies;
+
+#define wiz reinterpret_cast<AutoConfig *>(wizard())
+
+/* ------------------------------------------------------------------------- */
+
+constexpr std::string_view OBSServiceFileName = "service.json";
+
+static OBSData OpenServiceSettings(std::string &type)
+{
+	const OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName);
+
+	if (!std::filesystem::exists(jsonFilePath)) {
+		return OBSData();
+	}
+
+	OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak");
+
+	obs_data_set_default_string(data, "type", "rtmp_common");
+	type = obs_data_get_string(data, "type");
+
+	OBSDataAutoRelease settings = obs_data_get_obj(data, "settings");
+
+	return settings.Get();
+}
+
+static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key)
+{
+	OBSData settings = OpenServiceSettings(type);
+
+	service = obs_data_get_string(settings, "service");
+	server = obs_data_get_string(settings, "server");
+	key = obs_data_get_string(settings, "key");
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage)
+{
+	ui->setupUi(this);
+	setTitle(QTStr("Basic.AutoConfig.StartPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle"));
+
+	OBSBasic *main = OBSBasic::Get();
+	if (main->VCamEnabled()) {
+		QRadioButton *prioritizeVCam =
+			new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this);
+		QBoxLayout *box = reinterpret_cast<QBoxLayout *>(layout());
+		box->insertWidget(2, prioritizeVCam);
+
+		connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam);
+	}
+}
+
+AutoConfigStartPage::~AutoConfigStartPage() {}
+
+int AutoConfigStartPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage;
+}
+
+void AutoConfigStartPage::on_prioritizeStreaming_clicked()
+{
+	wiz->type = AutoConfig::Type::Streaming;
+}
+
+void AutoConfigStartPage::on_prioritizeRecording_clicked()
+{
+	wiz->type = AutoConfig::Type::Recording;
+}
+
+void AutoConfigStartPage::PrioritizeVCam()
+{
+	wiz->type = AutoConfig::Type::VirtualCam;
+}
+
+/* ------------------------------------------------------------------------- */
+
+#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x
+#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent")
+#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display")
+#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent")
+#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS")
+#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes")
+
+AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage)
+{
+	ui->setupUi(this);
+
+	setTitle(QTStr("Basic.AutoConfig.VideoPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle"));
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den;
+
+	QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g');
+
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS);
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes);
+	ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent);
+	ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30);
+	ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60);
+	ui->fps->setCurrentIndex(0);
+
+	QString cxStr = QString::number(ovi.base_width);
+	QString cyStr = QString::number(ovi.base_height);
+
+	int encRes = int(ovi.base_width << 16) | int(ovi.base_height);
+
+	// Auto config only supports testing down to 240p, don't allow current
+	// resolution if it's lower than that.
+	if (ovi.base_height >= 240)
+		ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes);
+
+	QList<QScreen *> screens = QGuiApplication::screens();
+	for (int i = 0; i < screens.size(); i++) {
+		QScreen *screen = screens[i];
+		QSize as = screen->size();
+		int as_width = as.width();
+		int as_height = as.height();
+
+		// Calculate physical screen resolution based on the virtual screen resolution
+		// They might differ if scaling is enabled, e.g. for HiDPI screens
+		as_width = round(as_width * screen->devicePixelRatio());
+		as_height = round(as_height * screen->devicePixelRatio());
+
+		encRes = as_width << 16 | as_height;
+
+		QString str =
+			QTStr(RES_USE_DISPLAY)
+				.arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height));
+
+		ui->canvasRes->addItem(str, encRes);
+	}
+
+	auto addRes = [&](int cx, int cy) {
+		encRes = (cx << 16) | cy;
+		QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy));
+		ui->canvasRes->addItem(str, encRes);
+	};
+
+	addRes(1920, 1080);
+	addRes(1280, 720);
+
+	ui->canvasRes->setCurrentIndex(0);
+}
+
+AutoConfigVideoPage::~AutoConfigVideoPage() {}
+
+int AutoConfigVideoPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage;
+}
+
+bool AutoConfigVideoPage::validatePage()
+{
+	int encRes = ui->canvasRes->currentData().toInt();
+	wiz->baseResolutionCX = encRes >> 16;
+	wiz->baseResolutionCY = encRes & 0xFFFF;
+	wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt();
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	switch (wiz->fpsType) {
+	case AutoConfig::FPSType::PreferHighFPS:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = true;
+		break;
+	case AutoConfig::FPSType::PreferHighRes:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::UseCurrent:
+		wiz->specificFPSNum = ovi.fps_num;
+		wiz->specificFPSDen = ovi.fps_den;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps30:
+		wiz->specificFPSNum = 30;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps60:
+		wiz->specificFPSNum = 60;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	}
+
+	return true;
+}
+
+/* ------------------------------------------------------------------------- */
+
+enum class ListOpt : int {
+	ShowAll = 1,
+	Custom,
+};
+
+AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage)
+{
+	ui->setupUi(this);
+	ui->bitrateLabel->setVisible(false);
+	ui->bitrate->setVisible(false);
+	ui->connectAccount2->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+	ui->useMultitrackVideo->setVisible(false);
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	int vertSpacing = ui->topLayout->verticalSpacing();
+
+	QMargins m = ui->topLayout->contentsMargins();
+	m.setBottom(vertSpacing / 2);
+	ui->topLayout->setContentsMargins(m);
+
+	m = ui->loginPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->loginPageLayout->setContentsMargins(m);
+
+	m = ui->streamkeyPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->streamkeyPageLayout->setContentsMargins(m);
+
+	setTitle(QTStr("Basic.AutoConfig.StreamPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle"));
+
+	LoadServices(false);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink);
+
+	connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() {
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+	});
+
+	connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+}
+
+AutoConfigStreamPage::~AutoConfigStreamPage() {}
+
+bool AutoConfigStreamPage::isComplete() const
+{
+	return ready;
+}
+
+int AutoConfigStreamPage::nextId() const
+{
+	return AutoConfig::TestPage;
+}
+
+inline bool AutoConfigStreamPage::IsCustomService() const
+{
+	return ui->service->currentData().toInt() == (int)ListOpt::Custom;
+}
+
+bool AutoConfigStreamPage::validatePage()
+{
+	OBSDataAutoRelease service_settings = obs_data_create();
+
+	wiz->customServer = IsCustomService();
+
+	const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common";
+
+	if (!wiz->customServer) {
+		obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText()));
+	}
+
+	OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr);
+
+	int bitrate;
+	if (!ui->doBandwidthTest->isChecked()) {
+		bitrate = ui->bitrate->value();
+		wiz->idealBitrate = bitrate;
+	} else {
+		/* Default test target is 10 Mbps */
+		bitrate = 10000;
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(wiz->serviceName)) {
+			/* Adjust upper bound to YouTube limits
+			 * for resolutions above 1080p */
+			if (wiz->baseResolutionCY > 1440)
+				bitrate = 51000;
+			else if (wiz->baseResolutionCY > 1080)
+				bitrate = 18000;
+		}
+#endif
+	}
+
+	OBSDataAutoRelease settings = obs_data_create();
+	obs_data_set_int(settings, "bitrate", bitrate);
+	obs_service_apply_encoder_settings(service, settings, nullptr);
+
+	if (wiz->customServer) {
+		QString server = ui->customServer->text().trimmed();
+		wiz->server = wiz->serverName = QT_TO_UTF8(server);
+	} else {
+		wiz->serverName = QT_TO_UTF8(ui->server->currentText());
+		wiz->server = QT_TO_UTF8(ui->server->currentData().toString());
+	}
+
+	wiz->bandwidthTest = ui->doBandwidthTest->isChecked();
+	wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate");
+	wiz->idealBitrate = wiz->startingBitrate;
+	wiz->regionUS = ui->regionUS->isChecked();
+	wiz->regionEU = ui->regionEU->isChecked();
+	wiz->regionAsia = ui->regionAsia->isChecked();
+	wiz->regionOther = ui->regionOther->isChecked();
+	wiz->serviceName = QT_TO_UTF8(ui->service->currentText());
+	if (ui->preferHardware)
+		wiz->preferHardware = ui->preferHardware->isChecked();
+	wiz->key = QT_TO_UTF8(ui->key->text());
+
+	if (!wiz->customServer) {
+		if (wiz->serviceName == "Twitch")
+			wiz->service = AutoConfig::Service::Twitch;
+#ifdef YOUTUBE_ENABLED
+		else if (IsYouTubeService(wiz->serviceName))
+			wiz->service = AutoConfig::Service::YouTube;
+#endif
+		else if (wiz->serviceName == "Amazon IVS")
+			wiz->service = AutoConfig::Service::AmazonIVS;
+		else
+			wiz->service = AutoConfig::Service::Other;
+	} else {
+		wiz->service = AutoConfig::Service::Other;
+	}
+
+	if (wiz->service == AutoConfig::Service::Twitch) {
+		wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked();
+
+		if (wiz->testMultitrackVideo) {
+			auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt,
+							    std::nullopt, false);
+
+			OBSDataAutoRelease service_settings = obs_service_get_settings(service);
+			auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+			if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+				multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+			}
+
+			try {
+				auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service),
+								   postData, multitrack_video_name);
+
+				for (const auto &endpoint : config.ingest_endpoints) {
+					if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0)
+						continue;
+
+					std::string address = endpoint.url_template;
+					auto pos = address.find("/{stream_key}");
+					if (pos != address.npos)
+						address.erase(pos);
+
+					wiz->serviceConfigServers.push_back({address, address});
+				}
+
+				int multitrackVideoBitrate = 0;
+				for (auto &encoder_config : config.encoder_configurations) {
+					auto it = encoder_config.settings.find("bitrate");
+					if (it == encoder_config.settings.end())
+						continue;
+
+					if (!it->is_number_integer())
+						continue;
+
+					int bitrate = 0;
+					it->get_to(bitrate);
+					multitrackVideoBitrate += bitrate;
+				}
+
+				if (multitrackVideoBitrate > 0) {
+					wiz->startingBitrate = multitrackVideoBitrate;
+					wiz->idealBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.testSuccessful = true;
+				}
+			} catch (const MultitrackVideoError & /*err*/) {
+				// FIXME: do something sensible
+			}
+		}
+	}
+
+	if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube &&
+	    wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) {
+		QMessageBox::StandardButton button;
+#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x)
+		button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text"));
+#undef WARNING_TEXT
+
+		if (button == QMessageBox::No)
+			return false;
+	}
+
+	return true;
+}
+
+void AutoConfigStreamPage::on_show_clicked()
+{
+	if (ui->key->echoMode() == QLineEdit::Password) {
+		ui->key->setEchoMode(QLineEdit::Normal);
+		ui->show->setText(QTStr("Hide"));
+	} else {
+		ui->key->setEchoMode(QLineEdit::Password);
+		ui->show->setText(QTStr("Show"));
+	}
+}
+
+void AutoConfigStreamPage::OnOAuthStreamKeyConnected()
+{
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+
+	if (a) {
+		bool validKey = !a->key().empty();
+
+		if (validKey)
+			ui->key->setText(QT_UTF8(a->key().c_str()));
+
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+
+		ui->connectedAccountLabel->setVisible(false);
+		ui->connectedAccountText->setVisible(false);
+
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(a->service())) {
+			ui->key->clear();
+
+			ui->connectedAccountLabel->setVisible(true);
+			ui->connectedAccountText->setVisible(true);
+
+			ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title"));
+
+			YoutubeApiWrappers *ytAuth = reinterpret_cast<YoutubeApiWrappers *>(a);
+			ChannelDescription cd;
+			if (ytAuth->GetChannelDescription(cd)) {
+				ui->connectedAccountText->setText(cd.title);
+
+				/* Create throwaway stream key for bandwidth test */
+				if (ui->doBandwidthTest->isChecked()) {
+					StreamDescription stream = {"", "", "OBS Studio Test Stream"};
+					if (ytAuth->InsertStream(stream)) {
+						ui->key->setText(stream.name);
+					}
+				}
+			}
+		}
+#endif
+	}
+
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::OnAuthConnected()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	Auth::Type type = Auth::AuthType(service);
+
+	if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) {
+		OnOAuthStreamKeyConnected();
+	}
+}
+
+void AutoConfigStreamPage::on_connectAccount_clicked()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+	OAuth::DeleteCookies(service);
+
+	auth = OAuthStreamKey::Login(this, service);
+	if (!!auth) {
+		OnAuthConnected();
+
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+}
+
+#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title"
+#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text"
+
+void AutoConfigStreamPage::on_disconnectAccount_clicked()
+{
+	QMessageBox::StandardButton button;
+
+	button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT));
+
+	if (button == QMessageBox::No) {
+		return;
+	}
+
+	OBSBasic *main = OBSBasic::Get();
+
+	main->auth.reset();
+	auth.reset();
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+#ifdef BROWSER_AVAILABLE
+	OAuth::DeleteCookies(service);
+#endif
+
+	reset_service_ui_fields(service);
+
+	ui->streamKeyWidget->setVisible(true);
+	ui->streamKeyLabel->setVisible(true);
+	ui->key->setText("");
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	/* Restore key link when disconnecting account */
+	UpdateKeyLink();
+}
+
+void AutoConfigStreamPage::on_useStreamKey_clicked()
+{
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::on_preferHardware_clicked()
+{
+	auto *main = OBSBasic::Get();
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+
+	ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked());
+	ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked());
+	ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled);
+}
+
+static inline bool is_auth_service(const std::string &service)
+{
+	return Auth::AuthType(service) != Auth::Type::None;
+}
+
+static inline bool is_external_oauth(const std::string &service)
+{
+	return Auth::External(service);
+}
+
+void AutoConfigStreamPage::reset_service_ui_fields(std::string &service)
+{
+#ifdef YOUTUBE_ENABLED
+	// when account is already connected:
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+	if (a && service == a->service() && IsYouTubeService(a->service())) {
+		ui->connectedAccountLabel->setVisible(true);
+		ui->connectedAccountText->setVisible(true);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		return;
+	}
+#endif
+
+	bool external_oauth = is_external_oauth(service);
+	if (external_oauth) {
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(true);
+
+		ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+
+	} else if (cef) {
+		QString key = ui->key->text();
+		bool can_auth = is_auth_service(service);
+		int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey;
+
+		ui->stackedWidget->setCurrentIndex(page);
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->connectAccount2->setVisible(can_auth);
+		ui->useStreamKeyAdv->setVisible(false);
+	} else {
+		ui->connectAccount2->setVisible(false);
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+}
+
+void AutoConfigStreamPage::ServiceChanged()
+{
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+	if (showMore)
+		return;
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	bool regionBased = service == "Twitch";
+	bool testBandwidth = ui->doBandwidthTest->isChecked();
+	bool custom = IsCustomService();
+
+	bool ertmp_multitrack_video_available = service == "Twitch";
+
+	bool custom_disclaimer = false;
+	auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+	if (!custom) {
+		OBSDataAutoRelease service_settings = obs_data_create();
+		obs_data_set_string(service_settings, "service", service.c_str());
+		OBSServiceAutoRelease obs_service =
+			obs_service_create("rtmp_common", "temp service", service_settings, nullptr);
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+			multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+		}
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) {
+			ui->multitrackVideoInfo->setText(
+				obs_data_get_string(service_settings, "multitrack_video_disclaimer"));
+			custom_disclaimer = true;
+		}
+	}
+
+	if (!custom_disclaimer) {
+		ui->multitrackVideoInfo->setText(
+			QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str()));
+	}
+
+	ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setText(
+		QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name));
+	ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable);
+	ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable);
+
+	reset_service_ui_fields(service);
+
+	/* Test three closest servers if "Auto" is available for Twitch */
+	if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto))
+		regionBased = false;
+
+	ui->streamkeyPageLayout->removeWidget(ui->serverLabel);
+	ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget);
+
+	if (custom) {
+		ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(false);
+		ui->serverStackedWidget->setCurrentIndex(1);
+		ui->serverStackedWidget->setVisible(true);
+		ui->serverLabel->setVisible(true);
+	} else {
+		if (!testBandwidth)
+			ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(regionBased && testBandwidth);
+		ui->serverStackedWidget->setCurrentIndex(0);
+		ui->serverStackedWidget->setHidden(testBandwidth);
+		ui->serverLabel->setHidden(testBandwidth);
+	}
+
+	wiz->testRegions = regionBased && testBandwidth;
+
+	ui->bitrateLabel->setHidden(testBandwidth);
+	ui->bitrate->setHidden(testBandwidth);
+
+	OBSBasic *main = OBSBasic::Get();
+
+	if (main->auth) {
+		auto system_auth_service = main->auth->service();
+		bool service_check = service.find(system_auth_service) != std::string::npos;
+#ifdef YOUTUBE_ENABLED
+		service_check = service_check ? service_check
+					      : IsYouTubeService(system_auth_service) && IsYouTubeService(service);
+#endif
+		if (service_check) {
+			auth.reset();
+			auth = main->auth;
+			OnAuthConnected();
+		}
+	}
+
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::UpdateMoreInfoLink()
+{
+	if (IsCustomService()) {
+		ui->moreInfoButton->hide();
+		return;
+	}
+
+	QString serviceName = ui->service->currentText();
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	const char *more_info_link = obs_data_get_string(settings, "more_info_link");
+
+	if (!more_info_link || (*more_info_link == '\0')) {
+		ui->moreInfoButton->hide();
+	} else {
+		ui->moreInfoButton->setTargetUrl(QUrl(more_info_link));
+		ui->moreInfoButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateKeyLink()
+{
+	QString serviceName = ui->service->currentText();
+	QString customServer = ui->customServer->text().trimmed();
+	QString streamKeyLink;
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	streamKeyLink = obs_data_get_string(settings, "stream_key_link");
+
+	if (customServer.contains("fbcdn.net") && IsCustomService()) {
+		streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS";
+	}
+
+	if (serviceName == "Dacast") {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else if (!IsCustomService()) {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else {
+		/* add tooltips for stream key */
+		QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg";
+		QString lStr = "<html>%1 <img src='%2' style=' \
+				vertical-align: bottom;  \
+				' /></html>";
+
+		ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file));
+		ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip"));
+	}
+
+	if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) {
+		ui->streamKeyButton->hide();
+	} else {
+		ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink));
+		ui->streamKeyButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::LoadServices(bool showAll)
+{
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_bool(settings, "show_all", showAll);
+
+	obs_property_t *prop = obs_properties_get(props, "show_all");
+	obs_property_modified(prop, settings);
+
+	ui->service->blockSignals(true);
+	ui->service->clear();
+
+	QStringList names;
+
+	obs_property_t *services = obs_properties_get(props, "service");
+	size_t services_count = obs_property_list_item_count(services);
+	for (size_t i = 0; i < services_count; i++) {
+		const char *name = obs_property_list_item_string(services, i);
+		names.push_back(name);
+	}
+
+	if (showAll)
+		names.sort(Qt::CaseInsensitive);
+
+	for (QString &name : names)
+		ui->service->addItem(name);
+
+	if (!showAll) {
+		ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"),
+				     QVariant((int)ListOpt::ShowAll));
+	}
+
+	ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom));
+
+	if (!lastService.isEmpty()) {
+		int idx = ui->service->findText(lastService);
+		if (idx != -1)
+			ui->service->setCurrentIndex(idx);
+	}
+
+	obs_properties_destroy(props);
+
+	ui->service->blockSignals(false);
+}
+
+void AutoConfigStreamPage::UpdateServerList()
+{
+	QString serviceName = ui->service->currentText();
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+
+	if (showMore) {
+		LoadServices(true);
+		ui->service->showPopup();
+		return;
+	} else {
+		lastService = serviceName;
+	}
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	obs_property_t *servers = obs_properties_get(props, "server");
+
+	ui->server->clear();
+
+	size_t servers_count = obs_property_list_item_count(servers);
+	for (size_t i = 0; i < servers_count; i++) {
+		const char *name = obs_property_list_item_name(servers, i);
+		const char *server = obs_property_list_item_string(servers, i);
+		ui->server->addItem(name, server);
+	}
+
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateCompleted()
+{
+	const bool custom = IsCustomService();
+	if (ui->stackedWidget->currentIndex() == (int)Section::Connect ||
+	    (ui->key->text().isEmpty() && !auth && !custom)) {
+		ready = false;
+	} else {
+		if (custom) {
+			ready = !ui->customServer->text().isEmpty();
+		} else {
+			ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() ||
+				ui->regionAsia->isChecked() || ui->regionOther->isChecked();
+		}
+	}
+	emit completeChanged();
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
+{
+	EnableThreadedMessageBoxes(true);
+
+	calldata_t cd = {0};
+	calldata_set_int(&cd, "seconds", 5);
+
+	proc_handler_t *ph = obs_get_proc_handler();
+	proc_handler_call(ph, "twitch_ingests_refresh", &cd);
+	proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd);
+	calldata_free(&cd);
+
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(parent);
+	main->EnableOutputs(false);
+
+	installEventFilter(CreateShortcutFilter());
+
+	std::string serviceType;
+	GetServiceInfo(serviceType, serviceName, server, key);
+#if defined(_WIN32) || defined(__APPLE__)
+	setWizardStyle(QWizard::ModernStyle);
+#endif
+	streamPage = new AutoConfigStreamPage();
+
+	setPage(StartPage, new AutoConfigStartPage());
+	setPage(VideoPage, new AutoConfigVideoPage());
+	setPage(StreamPage, streamPage);
+	setPage(TestPage, new AutoConfigTestPage());
+	setWindowTitle(QTStr("Basic.AutoConfig"));
+	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	baseResolutionCX = ovi.base_width;
+	baseResolutionCY = ovi.base_height;
+
+	/* ----------------------------------------- */
+	/* check to see if Twitch's "auto" available */
+
+	OBSDataAutoRelease twitchSettings = obs_data_create();
+
+	obs_data_set_string(twitchSettings, "service", "Twitch");
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, twitchSettings);
+
+	obs_property_t *p = obs_properties_get(props, "server");
+	const char *first = obs_property_list_item_string(p, 0);
+	twitchAuto = strcmp(first, "auto") == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* check to see if Amazon IVS "auto" entries are available */
+
+	OBSDataAutoRelease amazonIVSSettings = obs_data_create();
+
+	obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS");
+
+	props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, amazonIVSSettings);
+
+	p = obs_properties_get(props, "server");
+	first = obs_property_list_item_string(p, 0);
+	amazonIVSAuto = strncmp(first, "auto", 4) == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* load service/servers                      */
+
+	customServer = serviceType == "rtmp_custom";
+
+	QComboBox *serviceList = streamPage->ui->service;
+
+	if (!serviceName.empty()) {
+		serviceList->blockSignals(true);
+
+		int count = serviceList->count();
+		bool found = false;
+
+		for (int i = 0; i < count; i++) {
+			QString name = serviceList->itemText(i);
+
+			if (name == serviceName.c_str()) {
+				serviceList->setCurrentIndex(i);
+				found = true;
+				break;
+			}
+		}
+
+		if (!found) {
+			serviceList->insertItem(0, serviceName.c_str());
+			serviceList->setCurrentIndex(0);
+		}
+
+		serviceList->blockSignals(false);
+	}
+
+	streamPage->UpdateServerList();
+	streamPage->UpdateKeyLink();
+	streamPage->UpdateMoreInfoLink();
+	streamPage->lastService.clear();
+
+	if (!customServer) {
+		QComboBox *serverList = streamPage->ui->server;
+		int idx = serverList->findData(QString(server.c_str()));
+		if (idx == -1)
+			idx = 0;
+
+		serverList->setCurrentIndex(idx);
+	} else {
+		streamPage->ui->customServer->setText(server.c_str());
+		int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom));
+		streamPage->ui->service->setCurrentIndex(idx);
+	}
+
+	if (!key.empty())
+		streamPage->ui->key->setText(key.c_str());
+
+	TestHardwareEncoding();
+
+	int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate");
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+	streamPage->ui->bitrate->setValue(bitrate);
+	streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled);
+	streamPage->ServiceChanged();
+
+	if (!hardwareEncodingAvailable) {
+		delete streamPage->ui->preferHardware;
+		streamPage->ui->preferHardware = nullptr;
+	} else {
+		/* Newer generations of NVENC have a high enough quality to
+		 * bitrate ratio that if NVENC is available, it makes sense to
+		 * just always prefer hardware encoding by default */
+		bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4;
+		streamPage->ui->preferHardware->setChecked(preferHardware);
+	}
+
+	setOptions(QWizard::WizardOptions());
+	setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings"));
+	setButtonText(QWizard::BackButton, QTStr("Back"));
+	setButtonText(QWizard::NextButton, QTStr("Next"));
+	setButtonText(QWizard::CancelButton, QTStr("Cancel"));
+}
+
+AutoConfig::~AutoConfig()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	main->EnableOutputs(true);
+	EnableThreadedMessageBoxes(false);
+}
+
+void AutoConfig::TestHardwareEncoding()
+{
+	size_t idx = 0;
+	const char *id;
+	while (obs_enum_encoder_types(idx++, &id)) {
+		if (strcmp(id, "ffmpeg_nvenc") == 0)
+			hardwareEncodingAvailable = nvencAvailable = true;
+		else if (strcmp(id, "obs_qsv11") == 0)
+			hardwareEncodingAvailable = qsvAvailable = true;
+		else if (strcmp(id, "h264_texture_amf") == 0)
+			hardwareEncodingAvailable = vceAvailable = true;
+#ifdef __APPLE__
+		else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0
+#ifndef __aarch64__
+			 && os_get_emulation_status() == true
+#endif
+		)
+			if (__builtin_available(macOS 13.0, *))
+				hardwareEncodingAvailable = appleAvailable = true;
+#endif
+	}
+}
+
+bool AutoConfig::CanTestServer(const char *server)
+{
+	if (!testRegions || (regionUS && regionEU && regionAsia && regionOther))
+		return true;
+
+	if (service == Service::Twitch) {
+		if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 ||
+		    astrcmp_n(server, "US Central:", 11) == 0) {
+			return regionUS;
+		} else if (astrcmp_n(server, "EU:", 3) == 0) {
+			return regionEU;
+		} else if (astrcmp_n(server, "Asia:", 5) == 0) {
+			return regionAsia;
+		} else if (regionOther) {
+			return true;
+		}
+	} else {
+		return true;
+	}
+
+	return false;
+}
+
+void AutoConfig::done(int result)
+{
+	QWizard::done(result);
+
+	if (result == QDialog::Accepted) {
+		if (type == Type::Streaming)
+			SaveStreamSettings();
+		SaveSettings();
+
+#ifdef YOUTUBE_ENABLED
+		if (YouTubeAppDock::IsYTServiceSelected()) {
+			OBSBasic *main = OBSBasic::Get();
+			main->NewYouTubeAppDock();
+		}
+#endif
+	}
+}
+
+inline const char *AutoConfig::GetEncoderId(Encoder enc)
+{
+	switch (enc) {
+	case Encoder::NVENC:
+		return SIMPLE_ENCODER_NVENC;
+	case Encoder::QSV:
+		return SIMPLE_ENCODER_QSV;
+	case Encoder::AMD:
+		return SIMPLE_ENCODER_AMD;
+	case Encoder::Apple:
+		return SIMPLE_ENCODER_APPLE_H264;
+	default:
+		return SIMPLE_ENCODER_X264;
+	}
+};
+
+void AutoConfig::SaveStreamSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	/* ---------------------------------- */
+	/* save service                       */
+
+	const char *service_id = customServer ? "rtmp_custom" : "rtmp_common";
+
+	obs_service_t *oldService = main->GetService();
+	OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService);
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	if (!customServer)
+		obs_data_set_string(settings, "service", serviceName.c_str());
+	obs_data_set_string(settings, "server", server.c_str());
+#ifdef YOUTUBE_ENABLED
+	if (!streamPage->auth || !IsYouTubeService(serviceName))
+		obs_data_set_string(settings, "key", key.c_str());
+#else
+	obs_data_set_string(settings, "key", key.c_str());
+#endif
+
+	OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData);
+
+	if (!newService)
+		return;
+
+	main->SetService(newService);
+	main->SaveService();
+	main->auth = streamPage->auth;
+	if (!!main->auth) {
+		main->auth->LoadUI();
+		main->SetBroadcastFlowEnabled(main->auth->broadcastFlow());
+	} else {
+		main->SetBroadcastFlowEnabled(false);
+	}
+
+	/* ---------------------------------- */
+	/* save stream settings               */
+
+	config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate);
+	config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder));
+	config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced");
+
+	config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful);
+
+	if (multitrackVideo.targetBitrate.has_value())
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate",
+			       *multitrackVideo.targetBitrate);
+	else
+		config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate");
+
+	if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() &&
+	    (static_cast<double>(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true);
+	} else if (multitrackVideo.bitrate.has_value()) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false);
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate",
+			       *multitrackVideo.bitrate);
+	}
+}
+
+void AutoConfig::SaveSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	if (recordingEncoder != Encoder::Stream)
+		config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder));
+
+	const char *quality = recordingQuality == Quality::High ? "Small" : "Stream";
+
+	config_set_string(main->Config(), "Output", "Mode", "Simple");
+	config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality);
+	config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX);
+	config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY);
+	config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX);
+	config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY);
+
+	if (fpsType != FPSType::UseCurrent) {
+		config_set_uint(main->Config(), "Video", "FPSType", 0);
+		config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str());
+	}
+
+	main->ResetVideo();
+	main->ResetOutputs();
+	config_save_safe(main->Config(), "tmp", nullptr);
+}

+ 288 - 0
frontend/wizards/AutoConfigStreamPage.hpp

@@ -0,0 +1,288 @@
+#pragma once
+
+#include <QWizard>
+#include <QPointer>
+#include <QFormLayout>
+#include <QWizardPage>
+
+#include <condition_variable>
+#include <utility>
+#include <thread>
+#include <memory>
+#include <vector>
+#include <string>
+#include <mutex>
+#include <optional>
+
+class Ui_AutoConfigStartPage;
+class Ui_AutoConfigVideoPage;
+class Ui_AutoConfigStreamPage;
+class Ui_AutoConfigTestPage;
+
+class AutoConfigStreamPage;
+class Auth;
+
+class AutoConfig : public QWizard {
+	Q_OBJECT
+
+	friend class AutoConfigStartPage;
+	friend class AutoConfigVideoPage;
+	friend class AutoConfigStreamPage;
+	friend class AutoConfigTestPage;
+
+	enum class Type {
+		Invalid,
+		Streaming,
+		Recording,
+		VirtualCam,
+	};
+
+	enum class Service {
+		Twitch,
+		YouTube,
+		AmazonIVS,
+		Other,
+	};
+
+	enum class Encoder {
+		x264,
+		NVENC,
+		QSV,
+		AMD,
+		Apple,
+		Stream,
+	};
+
+	enum class Quality {
+		Stream,
+		High,
+	};
+
+	enum class FPSType : int {
+		PreferHighFPS,
+		PreferHighRes,
+		UseCurrent,
+		fps30,
+		fps60,
+	};
+
+	struct StreamServer {
+		std::string name;
+		std::string address;
+	};
+
+	static inline const char *GetEncoderId(Encoder enc);
+
+	AutoConfigStreamPage *streamPage = nullptr;
+
+	Service service = Service::Other;
+	Quality recordingQuality = Quality::Stream;
+	Encoder recordingEncoder = Encoder::Stream;
+	Encoder streamingEncoder = Encoder::x264;
+	Type type = Type::Streaming;
+	FPSType fpsType = FPSType::PreferHighFPS;
+	int idealBitrate = 2500;
+	struct {
+		std::optional<int> targetBitrate;
+		std::optional<int> bitrate;
+		bool testSuccessful = false;
+	} multitrackVideo;
+	int baseResolutionCX = 1920;
+	int baseResolutionCY = 1080;
+	int idealResolutionCX = 1280;
+	int idealResolutionCY = 720;
+	int idealFPSNum = 60;
+	int idealFPSDen = 1;
+	std::string serviceName;
+	std::string serverName;
+	std::string server;
+	std::vector<StreamServer> serviceConfigServers;
+	std::string key;
+
+	bool hardwareEncodingAvailable = false;
+	bool nvencAvailable = false;
+	bool qsvAvailable = false;
+	bool vceAvailable = false;
+	bool appleAvailable = false;
+
+	int startingBitrate = 2500;
+	bool customServer = false;
+	bool bandwidthTest = false;
+	bool testMultitrackVideo = false;
+	bool testRegions = true;
+	bool twitchAuto = false;
+	bool amazonIVSAuto = false;
+	bool regionUS = true;
+	bool regionEU = true;
+	bool regionAsia = true;
+	bool regionOther = true;
+	bool preferHighFPS = false;
+	bool preferHardware = false;
+	int specificFPSNum = 0;
+	int specificFPSDen = 0;
+
+	void TestHardwareEncoding();
+	bool CanTestServer(const char *server);
+
+	virtual void done(int result) override;
+
+	void SaveStreamSettings();
+	void SaveSettings();
+
+public:
+	AutoConfig(QWidget *parent);
+	~AutoConfig();
+
+	enum Page {
+		StartPage,
+		VideoPage,
+		StreamPage,
+		TestPage,
+	};
+};
+
+class AutoConfigStartPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigStartPage> ui;
+
+public:
+	AutoConfigStartPage(QWidget *parent = nullptr);
+	~AutoConfigStartPage();
+
+	virtual int nextId() const override;
+
+public slots:
+	void on_prioritizeStreaming_clicked();
+	void on_prioritizeRecording_clicked();
+	void PrioritizeVCam();
+};
+
+class AutoConfigVideoPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigVideoPage> ui;
+
+public:
+	AutoConfigVideoPage(QWidget *parent = nullptr);
+	~AutoConfigVideoPage();
+
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+};
+
+class AutoConfigStreamPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	enum class Section : int {
+		Connect,
+		StreamKey,
+	};
+
+	std::shared_ptr<Auth> auth;
+
+	std::unique_ptr<Ui_AutoConfigStreamPage> ui;
+	QString lastService;
+	bool ready = false;
+
+	void LoadServices(bool showAll);
+	inline bool IsCustomService() const;
+
+public:
+	AutoConfigStreamPage(QWidget *parent = nullptr);
+	~AutoConfigStreamPage();
+
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+
+	void OnAuthConnected();
+	void OnOAuthStreamKeyConnected();
+
+public slots:
+	void on_show_clicked();
+	void on_connectAccount_clicked();
+	void on_disconnectAccount_clicked();
+	void on_useStreamKey_clicked();
+	void on_preferHardware_clicked();
+	void ServiceChanged();
+	void UpdateKeyLink();
+	void UpdateMoreInfoLink();
+	void UpdateServerList();
+	void UpdateCompleted();
+
+	void reset_service_ui_fields(std::string &service);
+};
+
+class AutoConfigTestPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	QPointer<QFormLayout> results;
+
+	std::unique_ptr<Ui_AutoConfigTestPage> ui;
+	std::thread testThread;
+	std::condition_variable cv;
+	std::mutex m;
+	bool cancel = false;
+	bool started = false;
+
+	enum class Stage {
+		Starting,
+		BandwidthTest,
+		StreamEncoder,
+		RecordingEncoder,
+		Finished,
+	};
+
+	Stage stage = Stage::Starting;
+	bool softwareTested = false;
+
+	void StartBandwidthStage();
+	void StartStreamEncoderStage();
+	void StartRecordingEncoderStage();
+
+	void FindIdealHardwareResolution();
+	bool TestSoftwareEncoding();
+
+	void TestBandwidthThread();
+	void TestStreamEncoderThread();
+	void TestRecordingEncoderThread();
+
+	void FinalizeResults();
+
+	struct ServerInfo {
+		std::string name;
+		std::string address;
+		int bitrate = 0;
+		int ms = -1;
+
+		inline ServerInfo() {}
+
+		inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {}
+	};
+
+	void GetServers(std::vector<ServerInfo> &servers);
+
+public:
+	AutoConfigTestPage(QWidget *parent = nullptr);
+	~AutoConfigTestPage();
+
+	virtual void initializePage() override;
+	virtual void cleanupPage() override;
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+
+public slots:
+	void NextStage();
+	void UpdateMessage(QString message);
+	void Failure(QString message);
+	void Progress(int percentage);
+};

+ 0 - 0
UI/window-basic-auto-config-test.cpp → frontend/wizards/AutoConfigTestPage.cpp


+ 78 - 0
frontend/wizards/AutoConfigTestPage.hpp

@@ -0,0 +1,78 @@
+#pragma once
+
+#include <QPointer>
+#include <QWizardPage>
+
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+class QFormLayout;
+class Ui_AutoConfigTestPage;
+
+class AutoConfigTestPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	QPointer<QFormLayout> results;
+
+	std::unique_ptr<Ui_AutoConfigTestPage> ui;
+	std::thread testThread;
+	std::condition_variable cv;
+	std::mutex m;
+	bool cancel = false;
+	bool started = false;
+
+	enum class Stage {
+		Starting,
+		BandwidthTest,
+		StreamEncoder,
+		RecordingEncoder,
+		Finished,
+	};
+
+	Stage stage = Stage::Starting;
+	bool softwareTested = false;
+
+	void StartBandwidthStage();
+	void StartStreamEncoderStage();
+	void StartRecordingEncoderStage();
+
+	void FindIdealHardwareResolution();
+	bool TestSoftwareEncoding();
+
+	void TestBandwidthThread();
+	void TestStreamEncoderThread();
+	void TestRecordingEncoderThread();
+
+	void FinalizeResults();
+
+	struct ServerInfo {
+		std::string name;
+		std::string address;
+		int bitrate = 0;
+		int ms = -1;
+
+		inline ServerInfo() {}
+
+		inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {}
+	};
+
+	void GetServers(std::vector<ServerInfo> &servers);
+
+public:
+	AutoConfigTestPage(QWidget *parent = nullptr);
+	~AutoConfigTestPage();
+
+	virtual void initializePage() override;
+	virtual void cleanupPage() override;
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+
+public slots:
+	void NextStage();
+	void UpdateMessage(QString message);
+	void Failure(QString message);
+	void Progress(int percentage);
+};

+ 1240 - 0
frontend/wizards/AutoConfigVideoPage.cpp

@@ -0,0 +1,1240 @@
+#include <QMessageBox>
+#include <QScreen>
+
+#include <obs.hpp>
+#include <qt-wrappers.hpp>
+
+#include <nlohmann/json.hpp>
+
+#include "moc_window-basic-auto-config.cpp"
+#include "window-basic-main.hpp"
+#include "obs-app.hpp"
+#include "url-push-button.hpp"
+
+#include "goliveapi-postdata.hpp"
+#include "goliveapi-network.hpp"
+#include "multitrack-video-error.hpp"
+
+#include "ui_AutoConfigStartPage.h"
+#include "ui_AutoConfigVideoPage.h"
+#include "ui_AutoConfigStreamPage.h"
+
+#ifdef BROWSER_AVAILABLE
+#include <browser-panel.hpp>
+#endif
+
+#include "auth-oauth.hpp"
+#include "ui-config.h"
+#ifdef YOUTUBE_ENABLED
+#include "youtube-api-wrappers.hpp"
+#endif
+
+struct QCef;
+struct QCefCookieManager;
+
+extern QCef *cef;
+extern QCefCookieManager *panel_cookies;
+
+#define wiz reinterpret_cast<AutoConfig *>(wizard())
+
+/* ------------------------------------------------------------------------- */
+
+constexpr std::string_view OBSServiceFileName = "service.json";
+
+static OBSData OpenServiceSettings(std::string &type)
+{
+	const OBSBasic *basic = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const OBSProfile &currentProfile = basic->GetCurrentProfile();
+
+	const std::filesystem::path jsonFilePath = currentProfile.path / std::filesystem::u8path(OBSServiceFileName);
+
+	if (!std::filesystem::exists(jsonFilePath)) {
+		return OBSData();
+	}
+
+	OBSDataAutoRelease data = obs_data_create_from_json_file_safe(jsonFilePath.u8string().c_str(), "bak");
+
+	obs_data_set_default_string(data, "type", "rtmp_common");
+	type = obs_data_get_string(data, "type");
+
+	OBSDataAutoRelease settings = obs_data_get_obj(data, "settings");
+
+	return settings.Get();
+}
+
+static void GetServiceInfo(std::string &type, std::string &service, std::string &server, std::string &key)
+{
+	OBSData settings = OpenServiceSettings(type);
+
+	service = obs_data_get_string(settings, "service");
+	server = obs_data_get_string(settings, "server");
+	key = obs_data_get_string(settings, "key");
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfigStartPage::AutoConfigStartPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStartPage)
+{
+	ui->setupUi(this);
+	setTitle(QTStr("Basic.AutoConfig.StartPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StartPage.SubTitle"));
+
+	OBSBasic *main = OBSBasic::Get();
+	if (main->VCamEnabled()) {
+		QRadioButton *prioritizeVCam =
+			new QRadioButton(QTStr("Basic.AutoConfig.StartPage.PrioritizeVirtualCam"), this);
+		QBoxLayout *box = reinterpret_cast<QBoxLayout *>(layout());
+		box->insertWidget(2, prioritizeVCam);
+
+		connect(prioritizeVCam, &QPushButton::clicked, this, &AutoConfigStartPage::PrioritizeVCam);
+	}
+}
+
+AutoConfigStartPage::~AutoConfigStartPage() {}
+
+int AutoConfigStartPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::VirtualCam ? AutoConfig::TestPage : AutoConfig::VideoPage;
+}
+
+void AutoConfigStartPage::on_prioritizeStreaming_clicked()
+{
+	wiz->type = AutoConfig::Type::Streaming;
+}
+
+void AutoConfigStartPage::on_prioritizeRecording_clicked()
+{
+	wiz->type = AutoConfig::Type::Recording;
+}
+
+void AutoConfigStartPage::PrioritizeVCam()
+{
+	wiz->type = AutoConfig::Type::VirtualCam;
+}
+
+/* ------------------------------------------------------------------------- */
+
+#define RES_TEXT(x) "Basic.AutoConfig.VideoPage." x
+#define RES_USE_CURRENT RES_TEXT("BaseResolution.UseCurrent")
+#define RES_USE_DISPLAY RES_TEXT("BaseResolution.Display")
+#define FPS_USE_CURRENT RES_TEXT("FPS.UseCurrent")
+#define FPS_PREFER_HIGH_FPS RES_TEXT("FPS.PreferHighFPS")
+#define FPS_PREFER_HIGH_RES RES_TEXT("FPS.PreferHighRes")
+
+AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigVideoPage)
+{
+	ui->setupUi(this);
+
+	setTitle(QTStr("Basic.AutoConfig.VideoPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.VideoPage.SubTitle"));
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	long double fpsVal = (long double)ovi.fps_num / (long double)ovi.fps_den;
+
+	QString fpsStr = (ovi.fps_den > 1) ? QString::number(fpsVal, 'f', 2) : QString::number(fpsVal, 'g');
+
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_FPS), (int)AutoConfig::FPSType::PreferHighFPS);
+	ui->fps->addItem(QTStr(FPS_PREFER_HIGH_RES), (int)AutoConfig::FPSType::PreferHighRes);
+	ui->fps->addItem(QTStr(FPS_USE_CURRENT).arg(fpsStr), (int)AutoConfig::FPSType::UseCurrent);
+	ui->fps->addItem(QStringLiteral("30"), (int)AutoConfig::FPSType::fps30);
+	ui->fps->addItem(QStringLiteral("60"), (int)AutoConfig::FPSType::fps60);
+	ui->fps->setCurrentIndex(0);
+
+	QString cxStr = QString::number(ovi.base_width);
+	QString cyStr = QString::number(ovi.base_height);
+
+	int encRes = int(ovi.base_width << 16) | int(ovi.base_height);
+
+	// Auto config only supports testing down to 240p, don't allow current
+	// resolution if it's lower than that.
+	if (ovi.base_height >= 240)
+		ui->canvasRes->addItem(QTStr(RES_USE_CURRENT).arg(cxStr, cyStr), (int)encRes);
+
+	QList<QScreen *> screens = QGuiApplication::screens();
+	for (int i = 0; i < screens.size(); i++) {
+		QScreen *screen = screens[i];
+		QSize as = screen->size();
+		int as_width = as.width();
+		int as_height = as.height();
+
+		// Calculate physical screen resolution based on the virtual screen resolution
+		// They might differ if scaling is enabled, e.g. for HiDPI screens
+		as_width = round(as_width * screen->devicePixelRatio());
+		as_height = round(as_height * screen->devicePixelRatio());
+
+		encRes = as_width << 16 | as_height;
+
+		QString str =
+			QTStr(RES_USE_DISPLAY)
+				.arg(QString::number(i + 1), QString::number(as_width), QString::number(as_height));
+
+		ui->canvasRes->addItem(str, encRes);
+	}
+
+	auto addRes = [&](int cx, int cy) {
+		encRes = (cx << 16) | cy;
+		QString str = QString("%1x%2").arg(QString::number(cx), QString::number(cy));
+		ui->canvasRes->addItem(str, encRes);
+	};
+
+	addRes(1920, 1080);
+	addRes(1280, 720);
+
+	ui->canvasRes->setCurrentIndex(0);
+}
+
+AutoConfigVideoPage::~AutoConfigVideoPage() {}
+
+int AutoConfigVideoPage::nextId() const
+{
+	return wiz->type == AutoConfig::Type::Recording ? AutoConfig::TestPage : AutoConfig::StreamPage;
+}
+
+bool AutoConfigVideoPage::validatePage()
+{
+	int encRes = ui->canvasRes->currentData().toInt();
+	wiz->baseResolutionCX = encRes >> 16;
+	wiz->baseResolutionCY = encRes & 0xFFFF;
+	wiz->fpsType = (AutoConfig::FPSType)ui->fps->currentData().toInt();
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	switch (wiz->fpsType) {
+	case AutoConfig::FPSType::PreferHighFPS:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = true;
+		break;
+	case AutoConfig::FPSType::PreferHighRes:
+		wiz->specificFPSNum = 0;
+		wiz->specificFPSDen = 0;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::UseCurrent:
+		wiz->specificFPSNum = ovi.fps_num;
+		wiz->specificFPSDen = ovi.fps_den;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps30:
+		wiz->specificFPSNum = 30;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	case AutoConfig::FPSType::fps60:
+		wiz->specificFPSNum = 60;
+		wiz->specificFPSDen = 1;
+		wiz->preferHighFPS = false;
+		break;
+	}
+
+	return true;
+}
+
+/* ------------------------------------------------------------------------- */
+
+enum class ListOpt : int {
+	ShowAll = 1,
+	Custom,
+};
+
+AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage)
+{
+	ui->setupUi(this);
+	ui->bitrateLabel->setVisible(false);
+	ui->bitrate->setVisible(false);
+	ui->connectAccount2->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+	ui->useMultitrackVideo->setVisible(false);
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	int vertSpacing = ui->topLayout->verticalSpacing();
+
+	QMargins m = ui->topLayout->contentsMargins();
+	m.setBottom(vertSpacing / 2);
+	ui->topLayout->setContentsMargins(m);
+
+	m = ui->loginPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->loginPageLayout->setContentsMargins(m);
+
+	m = ui->streamkeyPageLayout->contentsMargins();
+	m.setTop(vertSpacing / 2);
+	ui->streamkeyPageLayout->setContentsMargins(m);
+
+	setTitle(QTStr("Basic.AutoConfig.StreamPage"));
+	setSubTitle(QTStr("Basic.AutoConfig.StreamPage.SubTitle"));
+
+	LoadServices(false);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::ServiceChanged);
+	connect(ui->customServer, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->customServer, &QLineEdit::editingFinished, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->doBandwidthTest, &QCheckBox::toggled, this, &AutoConfigStreamPage::ServiceChanged);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateServerList);
+
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateKeyLink);
+	connect(ui->service, &QComboBox::currentIndexChanged, this, &AutoConfigStreamPage::UpdateMoreInfoLink);
+
+	connect(ui->useStreamKeyAdv, &QPushButton::clicked, [&]() {
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+	});
+
+	connect(ui->key, &QLineEdit::textChanged, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionUS, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionEU, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionAsia, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+	connect(ui->regionOther, &QCheckBox::toggled, this, &AutoConfigStreamPage::UpdateCompleted);
+}
+
+AutoConfigStreamPage::~AutoConfigStreamPage() {}
+
+bool AutoConfigStreamPage::isComplete() const
+{
+	return ready;
+}
+
+int AutoConfigStreamPage::nextId() const
+{
+	return AutoConfig::TestPage;
+}
+
+inline bool AutoConfigStreamPage::IsCustomService() const
+{
+	return ui->service->currentData().toInt() == (int)ListOpt::Custom;
+}
+
+bool AutoConfigStreamPage::validatePage()
+{
+	OBSDataAutoRelease service_settings = obs_data_create();
+
+	wiz->customServer = IsCustomService();
+
+	const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common";
+
+	if (!wiz->customServer) {
+		obs_data_set_string(service_settings, "service", QT_TO_UTF8(ui->service->currentText()));
+	}
+
+	OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", service_settings, nullptr);
+
+	int bitrate;
+	if (!ui->doBandwidthTest->isChecked()) {
+		bitrate = ui->bitrate->value();
+		wiz->idealBitrate = bitrate;
+	} else {
+		/* Default test target is 10 Mbps */
+		bitrate = 10000;
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(wiz->serviceName)) {
+			/* Adjust upper bound to YouTube limits
+			 * for resolutions above 1080p */
+			if (wiz->baseResolutionCY > 1440)
+				bitrate = 51000;
+			else if (wiz->baseResolutionCY > 1080)
+				bitrate = 18000;
+		}
+#endif
+	}
+
+	OBSDataAutoRelease settings = obs_data_create();
+	obs_data_set_int(settings, "bitrate", bitrate);
+	obs_service_apply_encoder_settings(service, settings, nullptr);
+
+	if (wiz->customServer) {
+		QString server = ui->customServer->text().trimmed();
+		wiz->server = wiz->serverName = QT_TO_UTF8(server);
+	} else {
+		wiz->serverName = QT_TO_UTF8(ui->server->currentText());
+		wiz->server = QT_TO_UTF8(ui->server->currentData().toString());
+	}
+
+	wiz->bandwidthTest = ui->doBandwidthTest->isChecked();
+	wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate");
+	wiz->idealBitrate = wiz->startingBitrate;
+	wiz->regionUS = ui->regionUS->isChecked();
+	wiz->regionEU = ui->regionEU->isChecked();
+	wiz->regionAsia = ui->regionAsia->isChecked();
+	wiz->regionOther = ui->regionOther->isChecked();
+	wiz->serviceName = QT_TO_UTF8(ui->service->currentText());
+	if (ui->preferHardware)
+		wiz->preferHardware = ui->preferHardware->isChecked();
+	wiz->key = QT_TO_UTF8(ui->key->text());
+
+	if (!wiz->customServer) {
+		if (wiz->serviceName == "Twitch")
+			wiz->service = AutoConfig::Service::Twitch;
+#ifdef YOUTUBE_ENABLED
+		else if (IsYouTubeService(wiz->serviceName))
+			wiz->service = AutoConfig::Service::YouTube;
+#endif
+		else if (wiz->serviceName == "Amazon IVS")
+			wiz->service = AutoConfig::Service::AmazonIVS;
+		else
+			wiz->service = AutoConfig::Service::Other;
+	} else {
+		wiz->service = AutoConfig::Service::Other;
+	}
+
+	if (wiz->service == AutoConfig::Service::Twitch) {
+		wiz->testMultitrackVideo = ui->useMultitrackVideo->isChecked();
+
+		if (wiz->testMultitrackVideo) {
+			auto postData = constructGoLivePost(QString::fromStdString(wiz->key), std::nullopt,
+							    std::nullopt, false);
+
+			OBSDataAutoRelease service_settings = obs_service_get_settings(service);
+			auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+			if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+				multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+			}
+
+			try {
+				auto config = DownloadGoLiveConfig(this, MultitrackVideoAutoConfigURL(service),
+								   postData, multitrack_video_name);
+
+				for (const auto &endpoint : config.ingest_endpoints) {
+					if (qstrnicmp("RTMP", endpoint.protocol.c_str(), 4) != 0)
+						continue;
+
+					std::string address = endpoint.url_template;
+					auto pos = address.find("/{stream_key}");
+					if (pos != address.npos)
+						address.erase(pos);
+
+					wiz->serviceConfigServers.push_back({address, address});
+				}
+
+				int multitrackVideoBitrate = 0;
+				for (auto &encoder_config : config.encoder_configurations) {
+					auto it = encoder_config.settings.find("bitrate");
+					if (it == encoder_config.settings.end())
+						continue;
+
+					if (!it->is_number_integer())
+						continue;
+
+					int bitrate = 0;
+					it->get_to(bitrate);
+					multitrackVideoBitrate += bitrate;
+				}
+
+				if (multitrackVideoBitrate > 0) {
+					wiz->startingBitrate = multitrackVideoBitrate;
+					wiz->idealBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.targetBitrate = multitrackVideoBitrate;
+					wiz->multitrackVideo.testSuccessful = true;
+				}
+			} catch (const MultitrackVideoError & /*err*/) {
+				// FIXME: do something sensible
+			}
+		}
+	}
+
+	if (wiz->service != AutoConfig::Service::Twitch && wiz->service != AutoConfig::Service::YouTube &&
+	    wiz->service != AutoConfig::Service::AmazonIVS && wiz->bandwidthTest) {
+		QMessageBox::StandardButton button;
+#define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x)
+		button = OBSMessageBox::question(this, WARNING_TEXT("Title"), WARNING_TEXT("Text"));
+#undef WARNING_TEXT
+
+		if (button == QMessageBox::No)
+			return false;
+	}
+
+	return true;
+}
+
+void AutoConfigStreamPage::on_show_clicked()
+{
+	if (ui->key->echoMode() == QLineEdit::Password) {
+		ui->key->setEchoMode(QLineEdit::Normal);
+		ui->show->setText(QTStr("Hide"));
+	} else {
+		ui->key->setEchoMode(QLineEdit::Password);
+		ui->show->setText(QTStr("Show"));
+	}
+}
+
+void AutoConfigStreamPage::OnOAuthStreamKeyConnected()
+{
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+
+	if (a) {
+		bool validKey = !a->key().empty();
+
+		if (validKey)
+			ui->key->setText(QT_UTF8(a->key().c_str()));
+
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(false);
+
+		ui->connectedAccountLabel->setVisible(false);
+		ui->connectedAccountText->setVisible(false);
+
+#ifdef YOUTUBE_ENABLED
+		if (IsYouTubeService(a->service())) {
+			ui->key->clear();
+
+			ui->connectedAccountLabel->setVisible(true);
+			ui->connectedAccountText->setVisible(true);
+
+			ui->connectedAccountText->setText(QTStr("Auth.LoadingChannel.Title"));
+
+			YoutubeApiWrappers *ytAuth = reinterpret_cast<YoutubeApiWrappers *>(a);
+			ChannelDescription cd;
+			if (ytAuth->GetChannelDescription(cd)) {
+				ui->connectedAccountText->setText(cd.title);
+
+				/* Create throwaway stream key for bandwidth test */
+				if (ui->doBandwidthTest->isChecked()) {
+					StreamDescription stream = {"", "", "OBS Studio Test Stream"};
+					if (ytAuth->InsertStream(stream)) {
+						ui->key->setText(stream.name);
+					}
+				}
+			}
+		}
+#endif
+	}
+
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::OnAuthConnected()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	Auth::Type type = Auth::AuthType(service);
+
+	if (type == Auth::Type::OAuth_StreamKey || type == Auth::Type::OAuth_LinkedAccount) {
+		OnOAuthStreamKeyConnected();
+	}
+}
+
+void AutoConfigStreamPage::on_connectAccount_clicked()
+{
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+	OAuth::DeleteCookies(service);
+
+	auth = OAuthStreamKey::Login(this, service);
+	if (!!auth) {
+		OnAuthConnected();
+
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+}
+
+#define DISCONNECT_COMFIRM_TITLE "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title"
+#define DISCONNECT_COMFIRM_TEXT "Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text"
+
+void AutoConfigStreamPage::on_disconnectAccount_clicked()
+{
+	QMessageBox::StandardButton button;
+
+	button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), QTStr(DISCONNECT_COMFIRM_TEXT));
+
+	if (button == QMessageBox::No) {
+		return;
+	}
+
+	OBSBasic *main = OBSBasic::Get();
+
+	main->auth.reset();
+	auth.reset();
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+
+#ifdef BROWSER_AVAILABLE
+	OAuth::DeleteCookies(service);
+#endif
+
+	reset_service_ui_fields(service);
+
+	ui->streamKeyWidget->setVisible(true);
+	ui->streamKeyLabel->setVisible(true);
+	ui->key->setText("");
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+
+	/* Restore key link when disconnecting account */
+	UpdateKeyLink();
+}
+
+void AutoConfigStreamPage::on_useStreamKey_clicked()
+{
+	ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::on_preferHardware_clicked()
+{
+	auto *main = OBSBasic::Get();
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+
+	ui->useMultitrackVideo->setEnabled(ui->preferHardware->isChecked());
+	ui->multitrackVideoInfo->setEnabled(ui->preferHardware->isChecked());
+	ui->useMultitrackVideo->setChecked(ui->preferHardware->isChecked() && multitrackVideoEnabled);
+}
+
+static inline bool is_auth_service(const std::string &service)
+{
+	return Auth::AuthType(service) != Auth::Type::None;
+}
+
+static inline bool is_external_oauth(const std::string &service)
+{
+	return Auth::External(service);
+}
+
+void AutoConfigStreamPage::reset_service_ui_fields(std::string &service)
+{
+#ifdef YOUTUBE_ENABLED
+	// when account is already connected:
+	OAuthStreamKey *a = reinterpret_cast<OAuthStreamKey *>(auth.get());
+	if (a && service == a->service() && IsYouTubeService(a->service())) {
+		ui->connectedAccountLabel->setVisible(true);
+		ui->connectedAccountText->setVisible(true);
+		ui->connectAccount2->setVisible(false);
+		ui->disconnectAccount->setVisible(true);
+		return;
+	}
+#endif
+
+	bool external_oauth = is_external_oauth(service);
+	if (external_oauth) {
+		ui->streamKeyWidget->setVisible(false);
+		ui->streamKeyLabel->setVisible(false);
+		ui->connectAccount2->setVisible(true);
+		ui->useStreamKeyAdv->setVisible(true);
+
+		ui->stackedWidget->setCurrentIndex((int)Section::StreamKey);
+
+	} else if (cef) {
+		QString key = ui->key->text();
+		bool can_auth = is_auth_service(service);
+		int page = can_auth && key.isEmpty() ? (int)Section::Connect : (int)Section::StreamKey;
+
+		ui->stackedWidget->setCurrentIndex(page);
+		ui->streamKeyWidget->setVisible(true);
+		ui->streamKeyLabel->setVisible(true);
+		ui->connectAccount2->setVisible(can_auth);
+		ui->useStreamKeyAdv->setVisible(false);
+	} else {
+		ui->connectAccount2->setVisible(false);
+		ui->useStreamKeyAdv->setVisible(false);
+	}
+
+	ui->connectedAccountLabel->setVisible(false);
+	ui->connectedAccountText->setVisible(false);
+	ui->disconnectAccount->setVisible(false);
+}
+
+void AutoConfigStreamPage::ServiceChanged()
+{
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+	if (showMore)
+		return;
+
+	std::string service = QT_TO_UTF8(ui->service->currentText());
+	bool regionBased = service == "Twitch";
+	bool testBandwidth = ui->doBandwidthTest->isChecked();
+	bool custom = IsCustomService();
+
+	bool ertmp_multitrack_video_available = service == "Twitch";
+
+	bool custom_disclaimer = false;
+	auto multitrack_video_name = QTStr("Basic.Settings.Stream.MultitrackVideoLabel");
+	if (!custom) {
+		OBSDataAutoRelease service_settings = obs_data_create();
+		obs_data_set_string(service_settings, "service", service.c_str());
+		OBSServiceAutoRelease obs_service =
+			obs_service_create("rtmp_common", "temp service", service_settings, nullptr);
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_name")) {
+			multitrack_video_name = obs_data_get_string(service_settings, "multitrack_video_name");
+		}
+
+		if (obs_data_has_user_value(service_settings, "multitrack_video_disclaimer")) {
+			ui->multitrackVideoInfo->setText(
+				obs_data_get_string(service_settings, "multitrack_video_disclaimer"));
+			custom_disclaimer = true;
+		}
+	}
+
+	if (!custom_disclaimer) {
+		ui->multitrackVideoInfo->setText(
+			QTStr("MultitrackVideo.Info").arg(multitrack_video_name, service.c_str()));
+	}
+
+	ui->multitrackVideoInfo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setVisible(ertmp_multitrack_video_available);
+	ui->useMultitrackVideo->setText(
+		QTStr("Basic.AutoConfig.StreamPage.UseMultitrackVideo").arg(multitrack_video_name));
+	ui->multitrackVideoInfo->setEnabled(wiz->hardwareEncodingAvailable);
+	ui->useMultitrackVideo->setEnabled(wiz->hardwareEncodingAvailable);
+
+	reset_service_ui_fields(service);
+
+	/* Test three closest servers if "Auto" is available for Twitch */
+	if ((service == "Twitch" && wiz->twitchAuto) || (service == "Amazon IVS" && wiz->amazonIVSAuto))
+		regionBased = false;
+
+	ui->streamkeyPageLayout->removeWidget(ui->serverLabel);
+	ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget);
+
+	if (custom) {
+		ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(false);
+		ui->serverStackedWidget->setCurrentIndex(1);
+		ui->serverStackedWidget->setVisible(true);
+		ui->serverLabel->setVisible(true);
+	} else {
+		if (!testBandwidth)
+			ui->streamkeyPageLayout->insertRow(2, ui->serverLabel, ui->serverStackedWidget);
+
+		ui->region->setVisible(regionBased && testBandwidth);
+		ui->serverStackedWidget->setCurrentIndex(0);
+		ui->serverStackedWidget->setHidden(testBandwidth);
+		ui->serverLabel->setHidden(testBandwidth);
+	}
+
+	wiz->testRegions = regionBased && testBandwidth;
+
+	ui->bitrateLabel->setHidden(testBandwidth);
+	ui->bitrate->setHidden(testBandwidth);
+
+	OBSBasic *main = OBSBasic::Get();
+
+	if (main->auth) {
+		auto system_auth_service = main->auth->service();
+		bool service_check = service.find(system_auth_service) != std::string::npos;
+#ifdef YOUTUBE_ENABLED
+		service_check = service_check ? service_check
+					      : IsYouTubeService(system_auth_service) && IsYouTubeService(service);
+#endif
+		if (service_check) {
+			auth.reset();
+			auth = main->auth;
+			OnAuthConnected();
+		}
+	}
+
+	UpdateCompleted();
+}
+
+void AutoConfigStreamPage::UpdateMoreInfoLink()
+{
+	if (IsCustomService()) {
+		ui->moreInfoButton->hide();
+		return;
+	}
+
+	QString serviceName = ui->service->currentText();
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	const char *more_info_link = obs_data_get_string(settings, "more_info_link");
+
+	if (!more_info_link || (*more_info_link == '\0')) {
+		ui->moreInfoButton->hide();
+	} else {
+		ui->moreInfoButton->setTargetUrl(QUrl(more_info_link));
+		ui->moreInfoButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateKeyLink()
+{
+	QString serviceName = ui->service->currentText();
+	QString customServer = ui->customServer->text().trimmed();
+	QString streamKeyLink;
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	streamKeyLink = obs_data_get_string(settings, "stream_key_link");
+
+	if (customServer.contains("fbcdn.net") && IsCustomService()) {
+		streamKeyLink = "https://www.facebook.com/live/producer?ref=OBS";
+	}
+
+	if (serviceName == "Dacast") {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.EncoderKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else if (!IsCustomService()) {
+		ui->streamKeyLabel->setText(QTStr("Basic.AutoConfig.StreamPage.StreamKey"));
+		ui->streamKeyLabel->setToolTip("");
+	} else {
+		/* add tooltips for stream key */
+		QString file = !App()->IsThemeDark() ? ":/res/images/help.svg" : ":/res/images/help_light.svg";
+		QString lStr = "<html>%1 <img src='%2' style=' \
+				vertical-align: bottom;  \
+				' /></html>";
+
+		ui->streamKeyLabel->setText(lStr.arg(QTStr("Basic.AutoConfig.StreamPage.StreamKey"), file));
+		ui->streamKeyLabel->setToolTip(QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip"));
+	}
+
+	if (QString(streamKeyLink).isNull() || QString(streamKeyLink).isEmpty()) {
+		ui->streamKeyButton->hide();
+	} else {
+		ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink));
+		ui->streamKeyButton->show();
+	}
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::LoadServices(bool showAll)
+{
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_bool(settings, "show_all", showAll);
+
+	obs_property_t *prop = obs_properties_get(props, "show_all");
+	obs_property_modified(prop, settings);
+
+	ui->service->blockSignals(true);
+	ui->service->clear();
+
+	QStringList names;
+
+	obs_property_t *services = obs_properties_get(props, "service");
+	size_t services_count = obs_property_list_item_count(services);
+	for (size_t i = 0; i < services_count; i++) {
+		const char *name = obs_property_list_item_string(services, i);
+		names.push_back(name);
+	}
+
+	if (showAll)
+		names.sort(Qt::CaseInsensitive);
+
+	for (QString &name : names)
+		ui->service->addItem(name);
+
+	if (!showAll) {
+		ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"),
+				     QVariant((int)ListOpt::ShowAll));
+	}
+
+	ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom));
+
+	if (!lastService.isEmpty()) {
+		int idx = ui->service->findText(lastService);
+		if (idx != -1)
+			ui->service->setCurrentIndex(idx);
+	}
+
+	obs_properties_destroy(props);
+
+	ui->service->blockSignals(false);
+}
+
+void AutoConfigStreamPage::UpdateServerList()
+{
+	QString serviceName = ui->service->currentText();
+	bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll;
+
+	if (showMore) {
+		LoadServices(true);
+		ui->service->showPopup();
+		return;
+	} else {
+		lastService = serviceName;
+	}
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_property_t *services = obs_properties_get(props, "service");
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
+	obs_property_modified(services, settings);
+
+	obs_property_t *servers = obs_properties_get(props, "server");
+
+	ui->server->clear();
+
+	size_t servers_count = obs_property_list_item_count(servers);
+	for (size_t i = 0; i < servers_count; i++) {
+		const char *name = obs_property_list_item_name(servers, i);
+		const char *server = obs_property_list_item_string(servers, i);
+		ui->server->addItem(name, server);
+	}
+
+	obs_properties_destroy(props);
+}
+
+void AutoConfigStreamPage::UpdateCompleted()
+{
+	const bool custom = IsCustomService();
+	if (ui->stackedWidget->currentIndex() == (int)Section::Connect ||
+	    (ui->key->text().isEmpty() && !auth && !custom)) {
+		ready = false;
+	} else {
+		if (custom) {
+			ready = !ui->customServer->text().isEmpty();
+		} else {
+			ready = !wiz->testRegions || ui->regionUS->isChecked() || ui->regionEU->isChecked() ||
+				ui->regionAsia->isChecked() || ui->regionOther->isChecked();
+		}
+	}
+	emit completeChanged();
+}
+
+/* ------------------------------------------------------------------------- */
+
+AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
+{
+	EnableThreadedMessageBoxes(true);
+
+	calldata_t cd = {0};
+	calldata_set_int(&cd, "seconds", 5);
+
+	proc_handler_t *ph = obs_get_proc_handler();
+	proc_handler_call(ph, "twitch_ingests_refresh", &cd);
+	proc_handler_call(ph, "amazon_ivs_ingests_refresh", &cd);
+	calldata_free(&cd);
+
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(parent);
+	main->EnableOutputs(false);
+
+	installEventFilter(CreateShortcutFilter());
+
+	std::string serviceType;
+	GetServiceInfo(serviceType, serviceName, server, key);
+#if defined(_WIN32) || defined(__APPLE__)
+	setWizardStyle(QWizard::ModernStyle);
+#endif
+	streamPage = new AutoConfigStreamPage();
+
+	setPage(StartPage, new AutoConfigStartPage());
+	setPage(VideoPage, new AutoConfigVideoPage());
+	setPage(StreamPage, streamPage);
+	setPage(TestPage, new AutoConfigTestPage());
+	setWindowTitle(QTStr("Basic.AutoConfig"));
+	setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
+
+	obs_video_info ovi;
+	obs_get_video_info(&ovi);
+
+	baseResolutionCX = ovi.base_width;
+	baseResolutionCY = ovi.base_height;
+
+	/* ----------------------------------------- */
+	/* check to see if Twitch's "auto" available */
+
+	OBSDataAutoRelease twitchSettings = obs_data_create();
+
+	obs_data_set_string(twitchSettings, "service", "Twitch");
+
+	obs_properties_t *props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, twitchSettings);
+
+	obs_property_t *p = obs_properties_get(props, "server");
+	const char *first = obs_property_list_item_string(p, 0);
+	twitchAuto = strcmp(first, "auto") == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* check to see if Amazon IVS "auto" entries are available */
+
+	OBSDataAutoRelease amazonIVSSettings = obs_data_create();
+
+	obs_data_set_string(amazonIVSSettings, "service", "Amazon IVS");
+
+	props = obs_get_service_properties("rtmp_common");
+	obs_properties_apply_settings(props, amazonIVSSettings);
+
+	p = obs_properties_get(props, "server");
+	first = obs_property_list_item_string(p, 0);
+	amazonIVSAuto = strncmp(first, "auto", 4) == 0;
+
+	obs_properties_destroy(props);
+
+	/* ----------------------------------------- */
+	/* load service/servers                      */
+
+	customServer = serviceType == "rtmp_custom";
+
+	QComboBox *serviceList = streamPage->ui->service;
+
+	if (!serviceName.empty()) {
+		serviceList->blockSignals(true);
+
+		int count = serviceList->count();
+		bool found = false;
+
+		for (int i = 0; i < count; i++) {
+			QString name = serviceList->itemText(i);
+
+			if (name == serviceName.c_str()) {
+				serviceList->setCurrentIndex(i);
+				found = true;
+				break;
+			}
+		}
+
+		if (!found) {
+			serviceList->insertItem(0, serviceName.c_str());
+			serviceList->setCurrentIndex(0);
+		}
+
+		serviceList->blockSignals(false);
+	}
+
+	streamPage->UpdateServerList();
+	streamPage->UpdateKeyLink();
+	streamPage->UpdateMoreInfoLink();
+	streamPage->lastService.clear();
+
+	if (!customServer) {
+		QComboBox *serverList = streamPage->ui->server;
+		int idx = serverList->findData(QString(server.c_str()));
+		if (idx == -1)
+			idx = 0;
+
+		serverList->setCurrentIndex(idx);
+	} else {
+		streamPage->ui->customServer->setText(server.c_str());
+		int idx = streamPage->ui->service->findData(QVariant((int)ListOpt::Custom));
+		streamPage->ui->service->setCurrentIndex(idx);
+	}
+
+	if (!key.empty())
+		streamPage->ui->key->setText(key.c_str());
+
+	TestHardwareEncoding();
+
+	int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate");
+	bool multitrackVideoEnabled = config_has_user_value(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      ? config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")
+					      : true;
+	streamPage->ui->bitrate->setValue(bitrate);
+	streamPage->ui->useMultitrackVideo->setChecked(hardwareEncodingAvailable && multitrackVideoEnabled);
+	streamPage->ServiceChanged();
+
+	if (!hardwareEncodingAvailable) {
+		delete streamPage->ui->preferHardware;
+		streamPage->ui->preferHardware = nullptr;
+	} else {
+		/* Newer generations of NVENC have a high enough quality to
+		 * bitrate ratio that if NVENC is available, it makes sense to
+		 * just always prefer hardware encoding by default */
+		bool preferHardware = nvencAvailable || appleAvailable || os_get_physical_cores() <= 4;
+		streamPage->ui->preferHardware->setChecked(preferHardware);
+	}
+
+	setOptions(QWizard::WizardOptions());
+	setButtonText(QWizard::FinishButton, QTStr("Basic.AutoConfig.ApplySettings"));
+	setButtonText(QWizard::BackButton, QTStr("Back"));
+	setButtonText(QWizard::NextButton, QTStr("Next"));
+	setButtonText(QWizard::CancelButton, QTStr("Cancel"));
+}
+
+AutoConfig::~AutoConfig()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	main->EnableOutputs(true);
+	EnableThreadedMessageBoxes(false);
+}
+
+void AutoConfig::TestHardwareEncoding()
+{
+	size_t idx = 0;
+	const char *id;
+	while (obs_enum_encoder_types(idx++, &id)) {
+		if (strcmp(id, "ffmpeg_nvenc") == 0)
+			hardwareEncodingAvailable = nvencAvailable = true;
+		else if (strcmp(id, "obs_qsv11") == 0)
+			hardwareEncodingAvailable = qsvAvailable = true;
+		else if (strcmp(id, "h264_texture_amf") == 0)
+			hardwareEncodingAvailable = vceAvailable = true;
+#ifdef __APPLE__
+		else if (strcmp(id, "com.apple.videotoolbox.videoencoder.ave.avc") == 0
+#ifndef __aarch64__
+			 && os_get_emulation_status() == true
+#endif
+		)
+			if (__builtin_available(macOS 13.0, *))
+				hardwareEncodingAvailable = appleAvailable = true;
+#endif
+	}
+}
+
+bool AutoConfig::CanTestServer(const char *server)
+{
+	if (!testRegions || (regionUS && regionEU && regionAsia && regionOther))
+		return true;
+
+	if (service == Service::Twitch) {
+		if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 ||
+		    astrcmp_n(server, "US Central:", 11) == 0) {
+			return regionUS;
+		} else if (astrcmp_n(server, "EU:", 3) == 0) {
+			return regionEU;
+		} else if (astrcmp_n(server, "Asia:", 5) == 0) {
+			return regionAsia;
+		} else if (regionOther) {
+			return true;
+		}
+	} else {
+		return true;
+	}
+
+	return false;
+}
+
+void AutoConfig::done(int result)
+{
+	QWizard::done(result);
+
+	if (result == QDialog::Accepted) {
+		if (type == Type::Streaming)
+			SaveStreamSettings();
+		SaveSettings();
+
+#ifdef YOUTUBE_ENABLED
+		if (YouTubeAppDock::IsYTServiceSelected()) {
+			OBSBasic *main = OBSBasic::Get();
+			main->NewYouTubeAppDock();
+		}
+#endif
+	}
+}
+
+inline const char *AutoConfig::GetEncoderId(Encoder enc)
+{
+	switch (enc) {
+	case Encoder::NVENC:
+		return SIMPLE_ENCODER_NVENC;
+	case Encoder::QSV:
+		return SIMPLE_ENCODER_QSV;
+	case Encoder::AMD:
+		return SIMPLE_ENCODER_AMD;
+	case Encoder::Apple:
+		return SIMPLE_ENCODER_APPLE_H264;
+	default:
+		return SIMPLE_ENCODER_X264;
+	}
+};
+
+void AutoConfig::SaveStreamSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	/* ---------------------------------- */
+	/* save service                       */
+
+	const char *service_id = customServer ? "rtmp_custom" : "rtmp_common";
+
+	obs_service_t *oldService = main->GetService();
+	OBSDataAutoRelease hotkeyData = obs_hotkeys_save_service(oldService);
+
+	OBSDataAutoRelease settings = obs_data_create();
+
+	if (!customServer)
+		obs_data_set_string(settings, "service", serviceName.c_str());
+	obs_data_set_string(settings, "server", server.c_str());
+#ifdef YOUTUBE_ENABLED
+	if (!streamPage->auth || !IsYouTubeService(serviceName))
+		obs_data_set_string(settings, "key", key.c_str());
+#else
+	obs_data_set_string(settings, "key", key.c_str());
+#endif
+
+	OBSServiceAutoRelease newService = obs_service_create(service_id, "default_service", settings, hotkeyData);
+
+	if (!newService)
+		return;
+
+	main->SetService(newService);
+	main->SaveService();
+	main->auth = streamPage->auth;
+	if (!!main->auth) {
+		main->auth->LoadUI();
+		main->SetBroadcastFlowEnabled(main->auth->broadcastFlow());
+	} else {
+		main->SetBroadcastFlowEnabled(false);
+	}
+
+	/* ---------------------------------- */
+	/* save stream settings               */
+
+	config_set_int(main->Config(), "SimpleOutput", "VBitrate", idealBitrate);
+	config_set_string(main->Config(), "SimpleOutput", "StreamEncoder", GetEncoderId(streamingEncoder));
+	config_remove_value(main->Config(), "SimpleOutput", "UseAdvanced");
+
+	config_set_bool(main->Config(), "Stream1", "EnableMultitrackVideo", multitrackVideo.testSuccessful);
+
+	if (multitrackVideo.targetBitrate.has_value())
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoTargetBitrate",
+			       *multitrackVideo.targetBitrate);
+	else
+		config_remove_value(main->Config(), "Stream1", "MultitrackVideoTargetBitrate");
+
+	if (multitrackVideo.bitrate.has_value() && multitrackVideo.targetBitrate.has_value() &&
+	    (static_cast<double>(*multitrackVideo.bitrate) / *multitrackVideo.targetBitrate) >= 0.90) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true);
+	} else if (multitrackVideo.bitrate.has_value()) {
+		config_set_bool(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", false);
+		config_set_int(main->Config(), "Stream1", "MultitrackVideoMaximumAggregateBitrate",
+			       *multitrackVideo.bitrate);
+	}
+}
+
+void AutoConfig::SaveSettings()
+{
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+
+	if (recordingEncoder != Encoder::Stream)
+		config_set_string(main->Config(), "SimpleOutput", "RecEncoder", GetEncoderId(recordingEncoder));
+
+	const char *quality = recordingQuality == Quality::High ? "Small" : "Stream";
+
+	config_set_string(main->Config(), "Output", "Mode", "Simple");
+	config_set_string(main->Config(), "SimpleOutput", "RecQuality", quality);
+	config_set_int(main->Config(), "Video", "BaseCX", baseResolutionCX);
+	config_set_int(main->Config(), "Video", "BaseCY", baseResolutionCY);
+	config_set_int(main->Config(), "Video", "OutputCX", idealResolutionCX);
+	config_set_int(main->Config(), "Video", "OutputCY", idealResolutionCY);
+
+	if (fpsType != FPSType::UseCurrent) {
+		config_set_uint(main->Config(), "Video", "FPSType", 0);
+		config_set_string(main->Config(), "Video", "FPSCommon", std::to_string(idealFPSNum).c_str());
+	}
+
+	main->ResetVideo();
+	main->ResetOutputs();
+	config_save_safe(main->Config(), "tmp", nullptr);
+}

+ 288 - 0
frontend/wizards/AutoConfigVideoPage.hpp

@@ -0,0 +1,288 @@
+#pragma once
+
+#include <QWizard>
+#include <QPointer>
+#include <QFormLayout>
+#include <QWizardPage>
+
+#include <condition_variable>
+#include <utility>
+#include <thread>
+#include <memory>
+#include <vector>
+#include <string>
+#include <mutex>
+#include <optional>
+
+class Ui_AutoConfigStartPage;
+class Ui_AutoConfigVideoPage;
+class Ui_AutoConfigStreamPage;
+class Ui_AutoConfigTestPage;
+
+class AutoConfigStreamPage;
+class Auth;
+
+class AutoConfig : public QWizard {
+	Q_OBJECT
+
+	friend class AutoConfigStartPage;
+	friend class AutoConfigVideoPage;
+	friend class AutoConfigStreamPage;
+	friend class AutoConfigTestPage;
+
+	enum class Type {
+		Invalid,
+		Streaming,
+		Recording,
+		VirtualCam,
+	};
+
+	enum class Service {
+		Twitch,
+		YouTube,
+		AmazonIVS,
+		Other,
+	};
+
+	enum class Encoder {
+		x264,
+		NVENC,
+		QSV,
+		AMD,
+		Apple,
+		Stream,
+	};
+
+	enum class Quality {
+		Stream,
+		High,
+	};
+
+	enum class FPSType : int {
+		PreferHighFPS,
+		PreferHighRes,
+		UseCurrent,
+		fps30,
+		fps60,
+	};
+
+	struct StreamServer {
+		std::string name;
+		std::string address;
+	};
+
+	static inline const char *GetEncoderId(Encoder enc);
+
+	AutoConfigStreamPage *streamPage = nullptr;
+
+	Service service = Service::Other;
+	Quality recordingQuality = Quality::Stream;
+	Encoder recordingEncoder = Encoder::Stream;
+	Encoder streamingEncoder = Encoder::x264;
+	Type type = Type::Streaming;
+	FPSType fpsType = FPSType::PreferHighFPS;
+	int idealBitrate = 2500;
+	struct {
+		std::optional<int> targetBitrate;
+		std::optional<int> bitrate;
+		bool testSuccessful = false;
+	} multitrackVideo;
+	int baseResolutionCX = 1920;
+	int baseResolutionCY = 1080;
+	int idealResolutionCX = 1280;
+	int idealResolutionCY = 720;
+	int idealFPSNum = 60;
+	int idealFPSDen = 1;
+	std::string serviceName;
+	std::string serverName;
+	std::string server;
+	std::vector<StreamServer> serviceConfigServers;
+	std::string key;
+
+	bool hardwareEncodingAvailable = false;
+	bool nvencAvailable = false;
+	bool qsvAvailable = false;
+	bool vceAvailable = false;
+	bool appleAvailable = false;
+
+	int startingBitrate = 2500;
+	bool customServer = false;
+	bool bandwidthTest = false;
+	bool testMultitrackVideo = false;
+	bool testRegions = true;
+	bool twitchAuto = false;
+	bool amazonIVSAuto = false;
+	bool regionUS = true;
+	bool regionEU = true;
+	bool regionAsia = true;
+	bool regionOther = true;
+	bool preferHighFPS = false;
+	bool preferHardware = false;
+	int specificFPSNum = 0;
+	int specificFPSDen = 0;
+
+	void TestHardwareEncoding();
+	bool CanTestServer(const char *server);
+
+	virtual void done(int result) override;
+
+	void SaveStreamSettings();
+	void SaveSettings();
+
+public:
+	AutoConfig(QWidget *parent);
+	~AutoConfig();
+
+	enum Page {
+		StartPage,
+		VideoPage,
+		StreamPage,
+		TestPage,
+	};
+};
+
+class AutoConfigStartPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigStartPage> ui;
+
+public:
+	AutoConfigStartPage(QWidget *parent = nullptr);
+	~AutoConfigStartPage();
+
+	virtual int nextId() const override;
+
+public slots:
+	void on_prioritizeStreaming_clicked();
+	void on_prioritizeRecording_clicked();
+	void PrioritizeVCam();
+};
+
+class AutoConfigVideoPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	std::unique_ptr<Ui_AutoConfigVideoPage> ui;
+
+public:
+	AutoConfigVideoPage(QWidget *parent = nullptr);
+	~AutoConfigVideoPage();
+
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+};
+
+class AutoConfigStreamPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	enum class Section : int {
+		Connect,
+		StreamKey,
+	};
+
+	std::shared_ptr<Auth> auth;
+
+	std::unique_ptr<Ui_AutoConfigStreamPage> ui;
+	QString lastService;
+	bool ready = false;
+
+	void LoadServices(bool showAll);
+	inline bool IsCustomService() const;
+
+public:
+	AutoConfigStreamPage(QWidget *parent = nullptr);
+	~AutoConfigStreamPage();
+
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+	virtual bool validatePage() override;
+
+	void OnAuthConnected();
+	void OnOAuthStreamKeyConnected();
+
+public slots:
+	void on_show_clicked();
+	void on_connectAccount_clicked();
+	void on_disconnectAccount_clicked();
+	void on_useStreamKey_clicked();
+	void on_preferHardware_clicked();
+	void ServiceChanged();
+	void UpdateKeyLink();
+	void UpdateMoreInfoLink();
+	void UpdateServerList();
+	void UpdateCompleted();
+
+	void reset_service_ui_fields(std::string &service);
+};
+
+class AutoConfigTestPage : public QWizardPage {
+	Q_OBJECT
+
+	friend class AutoConfig;
+
+	QPointer<QFormLayout> results;
+
+	std::unique_ptr<Ui_AutoConfigTestPage> ui;
+	std::thread testThread;
+	std::condition_variable cv;
+	std::mutex m;
+	bool cancel = false;
+	bool started = false;
+
+	enum class Stage {
+		Starting,
+		BandwidthTest,
+		StreamEncoder,
+		RecordingEncoder,
+		Finished,
+	};
+
+	Stage stage = Stage::Starting;
+	bool softwareTested = false;
+
+	void StartBandwidthStage();
+	void StartStreamEncoderStage();
+	void StartRecordingEncoderStage();
+
+	void FindIdealHardwareResolution();
+	bool TestSoftwareEncoding();
+
+	void TestBandwidthThread();
+	void TestStreamEncoderThread();
+	void TestRecordingEncoderThread();
+
+	void FinalizeResults();
+
+	struct ServerInfo {
+		std::string name;
+		std::string address;
+		int bitrate = 0;
+		int ms = -1;
+
+		inline ServerInfo() {}
+
+		inline ServerInfo(const char *name_, const char *address_) : name(name_), address(address_) {}
+	};
+
+	void GetServers(std::vector<ServerInfo> &servers);
+
+public:
+	AutoConfigTestPage(QWidget *parent = nullptr);
+	~AutoConfigTestPage();
+
+	virtual void initializePage() override;
+	virtual void cleanupPage() override;
+	virtual bool isComplete() const override;
+	virtual int nextId() const override;
+
+public slots:
+	void NextStage();
+	void UpdateMessage(QString message);
+	void Failure(QString message);
+	void Progress(int percentage);
+};

+ 1261 - 0
frontend/wizards/TestMode.hpp

@@ -0,0 +1,1261 @@
+#include <chrono>
+
+#include <QFormLayout>
+
+#include <obs.hpp>
+#include <util/platform.h>
+#include <util/util_uint64.h>
+#include <graphics/vec4.h>
+#include <graphics/graphics.h>
+#include <graphics/math-extra.h>
+#include <qt-wrappers.hpp>
+
+#include "window-basic-auto-config.hpp"
+#include "window-basic-main.hpp"
+#include "obs-app.hpp"
+
+#include "ui_AutoConfigTestPage.h"
+
+#define wiz reinterpret_cast<AutoConfig *>(wizard())
+
+using namespace std;
+
+/* ------------------------------------------------------------------------- */
+
+class TestMode {
+	obs_video_info ovi;
+	OBSSource source[6];
+
+	static void render_rand(void *, uint32_t cx, uint32_t cy)
+	{
+		gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID);
+		gs_eparam_t *randomvals[3] = {gs_effect_get_param_by_name(solid, "randomvals1"),
+					      gs_effect_get_param_by_name(solid, "randomvals2"),
+					      gs_effect_get_param_by_name(solid, "randomvals3")};
+
+		struct vec4 r;
+
+		for (int i = 0; i < 3; i++) {
+			vec4_set(&r, rand_float(true) * 100.0f, rand_float(true) * 100.0f,
+				 rand_float(true) * 50000.0f + 10000.0f, 0.0f);
+			gs_effect_set_vec4(randomvals[i], &r);
+		}
+
+		while (gs_effect_loop(solid, "Random"))
+			gs_draw_sprite(nullptr, 0, cx, cy);
+	}
+
+public:
+	inline TestMode()
+	{
+		obs_get_video_info(&ovi);
+		obs_add_main_render_callback(render_rand, this);
+
+		for (uint32_t i = 0; i < 6; i++) {
+			source[i] = obs_get_output_source(i);
+			obs_source_release(source[i]);
+			obs_set_output_source(i, nullptr);
+		}
+	}
+
+	inline ~TestMode()
+	{
+		for (uint32_t i = 0; i < 6; i++)
+			obs_set_output_source(i, source[i]);
+
+		obs_remove_main_render_callback(render_rand, this);
+		obs_reset_video(&ovi);
+	}
+
+	inline void SetVideo(int cx, int cy, int fps_num, int fps_den)
+	{
+		obs_video_info newOVI = ovi;
+
+		newOVI.output_width = (uint32_t)cx;
+		newOVI.output_height = (uint32_t)cy;
+		newOVI.fps_num = (uint32_t)fps_num;
+		newOVI.fps_den = (uint32_t)fps_den;
+
+		obs_reset_video(&newOVI);
+	}
+};
+
+/* ------------------------------------------------------------------------- */
+
+#define TEST_STR(x) "Basic.AutoConfig.TestPage." x
+#define SUBTITLE_TESTING TEST_STR("SubTitle.Testing")
+#define SUBTITLE_COMPLETE TEST_STR("SubTitle.Complete")
+#define TEST_BW TEST_STR("TestingBandwidth")
+#define TEST_BW_NO_OUTPUT TEST_STR("TestingBandwidth.NoOutput")
+#define TEST_BW_CONNECTING TEST_STR("TestingBandwidth.Connecting")
+#define TEST_BW_CONNECT_FAIL TEST_STR("TestingBandwidth.ConnectFailed")
+#define TEST_BW_SERVER TEST_STR("TestingBandwidth.Server")
+#define TEST_RES_VAL TEST_STR("TestingRes.Resolution")
+#define TEST_RES_FAIL TEST_STR("TestingRes.Fail")
+#define TEST_SE TEST_STR("TestingStreamEncoder")
+#define TEST_RE TEST_STR("TestingRecordingEncoder")
+#define TEST_RESULT_SE TEST_STR("Result.StreamingEncoder")
+#define TEST_RESULT_RE TEST_STR("Result.RecordingEncoder")
+
+void AutoConfigTestPage::StartBandwidthStage()
+{
+	ui->progressLabel->setText(QTStr(TEST_BW));
+	testThread = std::thread([this]() { TestBandwidthThread(); });
+}
+
+void AutoConfigTestPage::StartStreamEncoderStage()
+{
+	ui->progressLabel->setText(QTStr(TEST_SE));
+	testThread = std::thread([this]() { TestStreamEncoderThread(); });
+}
+
+void AutoConfigTestPage::StartRecordingEncoderStage()
+{
+	ui->progressLabel->setText(QTStr(TEST_RE));
+	testThread = std::thread([this]() { TestRecordingEncoderThread(); });
+}
+
+void AutoConfigTestPage::GetServers(std::vector<ServerInfo> &servers)
+{
+	OBSDataAutoRelease settings = obs_data_create();
+	obs_data_set_string(settings, "service", wiz->serviceName.c_str());
+
+	obs_properties_t *ppts = obs_get_service_properties("rtmp_common");
+	obs_property_t *p = obs_properties_get(ppts, "service");
+	obs_property_modified(p, settings);
+
+	p = obs_properties_get(ppts, "server");
+	size_t count = obs_property_list_item_count(p);
+	servers.reserve(count);
+
+	for (size_t i = 0; i < count; i++) {
+		const char *name = obs_property_list_item_name(p, i);
+		const char *server = obs_property_list_item_string(p, i);
+
+		if (wiz->CanTestServer(name)) {
+			ServerInfo info(name, server);
+			servers.push_back(info);
+		}
+	}
+
+	obs_properties_destroy(ppts);
+}
+
+static inline void string_depad_key(string &key)
+{
+	while (!key.empty()) {
+		char ch = key.back();
+		if (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r')
+			key.pop_back();
+		else
+			break;
+	}
+}
+
+const char *FindAudioEncoderFromCodec(const char *type);
+
+static inline bool can_use_output(const char *prot, const char *output, const char *prot_test1,
+				  const char *prot_test2 = nullptr)
+{
+	return (strcmp(prot, prot_test1) == 0 || (prot_test2 && strcmp(prot, prot_test2) == 0)) &&
+	       (obs_get_output_flags(output) & OBS_OUTPUT_SERVICE) != 0;
+}
+
+static bool return_first_id(void *data, const char *id)
+{
+	const char **output = (const char **)data;
+
+	*output = id;
+	return false;
+}
+
+void AutoConfigTestPage::TestBandwidthThread()
+{
+	bool connected = false;
+	bool stopped = false;
+
+	TestMode testMode;
+	testMode.SetVideo(128, 128, 60, 1);
+
+	QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, 0));
+
+	/*
+	 * create encoders
+	 * create output
+	 * test for 10 seconds
+	 */
+
+	QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral("")));
+
+	/* -----------------------------------*/
+	/* create obs objects                 */
+
+	const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common";
+
+	OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr);
+	OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr);
+	OBSServiceAutoRelease service = obs_service_create(serverType, "test_service", nullptr, nullptr);
+
+	/* -----------------------------------*/
+	/* configure settings                 */
+
+	// service: "service", "server", "key"
+	// vencoder: "bitrate", "rate_control",
+	//           obs_service_apply_encoder_settings
+	// aencoder: "bitrate"
+	// output: "bind_ip" via main config -> "Output", "BindIP"
+	//         obs_output_set_service
+
+	OBSDataAutoRelease service_settings = obs_data_create();
+	OBSDataAutoRelease vencoder_settings = obs_data_create();
+	OBSDataAutoRelease aencoder_settings = obs_data_create();
+	OBSDataAutoRelease output_settings = obs_data_create();
+
+	std::string key = wiz->key;
+	if (wiz->service == AutoConfig::Service::Twitch || wiz->service == AutoConfig::Service::AmazonIVS) {
+		string_depad_key(key);
+		key += "?bandwidthtest";
+	} else if (wiz->serviceName == "Restream.io" || wiz->serviceName == "Restream.io - RTMP") {
+		string_depad_key(key);
+		key += "?test=true";
+	}
+
+	obs_data_set_string(service_settings, "service", wiz->serviceName.c_str());
+	obs_data_set_string(service_settings, "key", key.c_str());
+
+	obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate);
+	obs_data_set_string(vencoder_settings, "rate_control", "CBR");
+	obs_data_set_string(vencoder_settings, "preset", "veryfast");
+	obs_data_set_int(vencoder_settings, "keyint_sec", 2);
+
+	obs_data_set_int(aencoder_settings, "bitrate", 32);
+
+	OBSBasic *main = reinterpret_cast<OBSBasic *>(App()->GetMainWindow());
+	const char *bind_ip = config_get_string(main->Config(), "Output", "BindIP");
+	obs_data_set_string(output_settings, "bind_ip", bind_ip);
+
+	const char *ip_family = config_get_string(main->Config(), "Output", "IPFamily");
+	obs_data_set_string(output_settings, "ip_family", ip_family);
+
+	/* -----------------------------------*/
+	/* determine which servers to test    */
+
+	std::vector<ServerInfo> servers;
+	if (wiz->customServer)
+		servers.emplace_back(wiz->server.c_str(), wiz->server.c_str());
+	else
+		GetServers(servers);
+
+	/* just use the first server if it only has one alternate server,
+	 * or if using Restream or Nimo TV due to their "auto" servers */
+	if (servers.size() < 3 || wiz->serviceName.substr(0, 11) == "Restream.io" || wiz->serviceName == "Nimo TV") {
+		servers.resize(1);
+
+	} else if ((wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) ||
+		   (wiz->service == AutoConfig::Service::AmazonIVS && wiz->amazonIVSAuto)) {
+		/* if using Twitch and "Auto" is available, test 3 closest
+		 * server */
+		servers.erase(servers.begin() + 1);
+		servers.resize(3);
+	} else if (wiz->service == AutoConfig::Service::YouTube) {
+		/* Only test first set of primary + backup servers */
+		servers.resize(2);
+	}
+
+	if (!wiz->serviceConfigServers.empty()) {
+		if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) {
+			// servers from Twitch service config replace the "auto" entry
+			servers.erase(servers.begin());
+		}
+
+		for (auto it = std::rbegin(wiz->serviceConfigServers); it != std::rend(wiz->serviceConfigServers);
+		     it++) {
+			auto same_server =
+				std::find_if(std::begin(servers), std::end(servers),
+					     [&](const ServerInfo &si) { return si.address == it->address; });
+			if (same_server != std::end(servers))
+				servers.erase(same_server);
+			servers.emplace(std::begin(servers), it->name.c_str(), it->address.c_str());
+		}
+
+		if (wiz->service == AutoConfig::Service::Twitch && wiz->twitchAuto) {
+			// see above, only test 3 servers
+			// rtmps urls are currently counted as separate servers
+			servers.resize(3);
+		}
+	}
+
+	/* -----------------------------------*/
+	/* apply service settings             */
+
+	obs_service_update(service, service_settings);
+	obs_service_apply_encoder_settings(service, vencoder_settings, aencoder_settings);
+
+	if (wiz->multitrackVideo.testSuccessful) {
+		obs_data_set_int(vencoder_settings, "bitrate", wiz->startingBitrate);
+	}
+
+	/* -----------------------------------*/
+	/* create output                      */
+
+	/* Check if the service has a preferred output type */
+	const char *output_type = obs_service_get_preferred_output_type(service);
+	if (!output_type || (obs_get_output_flags(output_type) & OBS_OUTPUT_SERVICE) == 0) {
+		/* Otherwise, prefer first-party output types */
+		const char *protocol = obs_service_get_protocol(service);
+
+		if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) {
+			output_type = "rtmp_output";
+		} else if (can_use_output(protocol, "ffmpeg_hls_muxer", "HLS")) {
+			output_type = "ffmpeg_hls_muxer";
+		} else if (can_use_output(protocol, "ffmpeg_mpegts_muxer", "SRT", "RIST")) {
+			output_type = "ffmpeg_mpegts_muxer";
+		}
+
+		/* If third-party protocol, use the first enumerated type */
+		if (!output_type)
+			obs_enum_output_types_with_protocol(protocol, &output_type, return_first_id);
+
+		/* If none, fail */
+		if (!output_type) {
+			QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_NO_OUTPUT)));
+			return;
+		}
+	}
+
+	OBSOutputAutoRelease output = obs_output_create(output_type, "test_stream", nullptr, nullptr);
+	obs_output_update(output, output_settings);
+
+	const char *audio_codec = obs_output_get_supported_audio_codecs(output);
+
+	if (strcmp(audio_codec, "aac") != 0) {
+		const char *id = FindAudioEncoderFromCodec(audio_codec);
+		aencoder = obs_audio_encoder_create(id, "test_audio", nullptr, 0, nullptr);
+	}
+
+	/* -----------------------------------*/
+	/* connect encoders/services/outputs  */
+
+	obs_encoder_update(vencoder, vencoder_settings);
+	obs_encoder_update(aencoder, aencoder_settings);
+	obs_encoder_set_video(vencoder, obs_get_video());
+	obs_encoder_set_audio(aencoder, obs_get_audio());
+
+	obs_output_set_video_encoder(output, vencoder);
+	obs_output_set_audio_encoder(output, aencoder, 0);
+	obs_output_set_reconnect_settings(output, 0, 0);
+
+	obs_output_set_service(output, service);
+
+	/* -----------------------------------*/
+	/* connect signals                    */
+
+	auto on_started = [&]() {
+		unique_lock<mutex> lock(m);
+		connected = true;
+		stopped = false;
+		cv.notify_one();
+	};
+
+	auto on_stopped = [&]() {
+		unique_lock<mutex> lock(m);
+		connected = false;
+		stopped = true;
+		cv.notify_one();
+	};
+
+	using on_started_t = decltype(on_started);
+	using on_stopped_t = decltype(on_stopped);
+
+	auto pre_on_started = [](void *data, calldata_t *) {
+		on_started_t &on_started = *reinterpret_cast<on_started_t *>(data);
+		on_started();
+	};
+
+	auto pre_on_stopped = [](void *data, calldata_t *) {
+		on_stopped_t &on_stopped = *reinterpret_cast<on_stopped_t *>(data);
+		on_stopped();
+	};
+
+	signal_handler *sh = obs_output_get_signal_handler(output);
+	signal_handler_connect(sh, "start", pre_on_started, &on_started);
+	signal_handler_connect(sh, "stop", pre_on_stopped, &on_stopped);
+
+	/* -----------------------------------*/
+	/* test servers                       */
+
+	bool success = false;
+
+	for (size_t i = 0; i < servers.size(); i++) {
+		auto &server = servers[i];
+
+		connected = false;
+		stopped = false;
+
+		int per = int((i + 1) * 100 / servers.size());
+		QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per));
+		QMetaObject::invokeMethod(this, "UpdateMessage",
+					  Q_ARG(QString, QTStr(TEST_BW_CONNECTING).arg(server.name.c_str())));
+
+		obs_data_set_string(service_settings, "server", server.address.c_str());
+		obs_service_update(service, service_settings);
+
+		if (!obs_output_start(output))
+			continue;
+
+		unique_lock<mutex> ul(m);
+		if (cancel) {
+			ul.unlock();
+			obs_output_force_stop(output);
+			return;
+		}
+		if (!stopped && !connected)
+			cv.wait(ul);
+		if (cancel) {
+			ul.unlock();
+			obs_output_force_stop(output);
+			return;
+		}
+		if (!connected)
+			continue;
+
+		QMetaObject::invokeMethod(this, "UpdateMessage",
+					  Q_ARG(QString, QTStr(TEST_BW_SERVER).arg(server.name.c_str())));
+
+		/* ignore first 2.5 seconds due to possible buffering skewing
+		 * the result */
+		cv.wait_for(ul, chrono::milliseconds(2500));
+		if (stopped)
+			continue;
+		if (cancel) {
+			ul.unlock();
+			obs_output_force_stop(output);
+			return;
+		}
+
+		/* continue test */
+		int start_bytes = (int)obs_output_get_total_bytes(output);
+		uint64_t t_start = os_gettime_ns();
+
+		cv.wait_for(ul, chrono::seconds(10));
+		if (stopped)
+			continue;
+		if (cancel) {
+			ul.unlock();
+			obs_output_force_stop(output);
+			return;
+		}
+
+		obs_output_stop(output);
+		cv.wait(ul);
+
+		uint64_t total_time = os_gettime_ns() - t_start;
+		if (total_time == 0)
+			total_time = 1;
+
+		int total_bytes = (int)obs_output_get_total_bytes(output) - start_bytes;
+		uint64_t bitrate = util_mul_div64(total_bytes, 8ULL * 1000000000ULL / 1000ULL, total_time);
+		if (obs_output_get_frames_dropped(output) || (int)bitrate < (wiz->startingBitrate * 75 / 100)) {
+			server.bitrate = (int)bitrate * 70 / 100;
+		} else {
+			server.bitrate = wiz->startingBitrate;
+		}
+
+		server.ms = obs_output_get_connect_time_ms(output);
+		success = true;
+	}
+
+	if (!success) {
+		QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_BW_CONNECT_FAIL)));
+		return;
+	}
+
+	int bestBitrate = 0;
+	int bestMS = 0x7FFFFFFF;
+	string bestServer;
+	string bestServerName;
+
+	for (auto &server : servers) {
+		bool close = abs(server.bitrate - bestBitrate) < 400;
+
+		if ((!close && server.bitrate > bestBitrate) || (close && server.ms < bestMS)) {
+			bestServer = server.address;
+			bestServerName = server.name;
+			bestBitrate = server.bitrate;
+			bestMS = server.ms;
+		}
+	}
+
+	wiz->server = std::move(bestServer);
+	wiz->serverName = std::move(bestServerName);
+	wiz->idealBitrate = bestBitrate;
+
+	QMetaObject::invokeMethod(this, "NextStage");
+}
+
+/* this is used to estimate the lower bitrate limit for a given
+ * resolution/fps.  yes, it is a totally arbitrary equation that gets
+ * the closest to the expected values */
+static long double EstimateBitrateVal(int cx, int cy, int fps_num, int fps_den)
+{
+	long fps = (long double)fps_num / (long double)fps_den;
+	long double areaVal = pow((long double)(cx * cy), 0.85l);
+	return areaVal * sqrt(pow(fps, 1.1l));
+}
+
+static long double EstimateMinBitrate(int cx, int cy, int fps_num, int fps_den)
+{
+	long double val = EstimateBitrateVal(1920, 1080, 60, 1) / 5800.0l;
+	return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val;
+}
+
+static long double EstimateUpperBitrate(int cx, int cy, int fps_num, int fps_den)
+{
+	long double val = EstimateBitrateVal(1280, 720, 30, 1) / 3000.0l;
+	return EstimateBitrateVal(cx, cy, fps_num, fps_den) / val;
+}
+
+struct Result {
+	int cx;
+	int cy;
+	int fps_num;
+	int fps_den;
+
+	inline Result(int cx_, int cy_, int fps_num_, int fps_den_)
+		: cx(cx_),
+		  cy(cy_),
+		  fps_num(fps_num_),
+		  fps_den(fps_den_)
+	{
+	}
+};
+
+static void CalcBaseRes(int &baseCX, int &baseCY)
+{
+	const int maxBaseArea = 1920 * 1200;
+	const int clipResArea = 1920 * 1080;
+
+	/* if base resolution unusually high, recalculate to a more reasonable
+	 * value to start the downscaling at, based upon 1920x1080's area.
+	 *
+	 * for 16:9 resolutions this will always change the starting value to
+	 * 1920x1080 */
+	if ((baseCX * baseCY) > maxBaseArea) {
+		long double xyAspect = (long double)baseCX / (long double)baseCY;
+		baseCY = (int)sqrt((long double)clipResArea / xyAspect);
+		baseCX = (int)((long double)baseCY * xyAspect);
+	}
+}
+
+bool AutoConfigTestPage::TestSoftwareEncoding()
+{
+	TestMode testMode;
+	QMetaObject::invokeMethod(this, "UpdateMessage", Q_ARG(QString, QStringLiteral("")));
+
+	/* -----------------------------------*/
+	/* create obs objects                 */
+
+	OBSEncoderAutoRelease vencoder = obs_video_encoder_create("obs_x264", "test_x264", nullptr, nullptr);
+	OBSEncoderAutoRelease aencoder = obs_audio_encoder_create("ffmpeg_aac", "test_aac", nullptr, 0, nullptr);
+	OBSOutputAutoRelease output = obs_output_create("null_output", "null", nullptr, nullptr);
+
+	/* -----------------------------------*/
+	/* configure settings                 */
+
+	OBSDataAutoRelease aencoder_settings = obs_data_create();
+	OBSDataAutoRelease vencoder_settings = obs_data_create();
+	obs_data_set_int(aencoder_settings, "bitrate", 32);
+
+	if (wiz->type != AutoConfig::Type::Recording) {
+		obs_data_set_int(vencoder_settings, "keyint_sec", 2);
+		obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate);
+		obs_data_set_string(vencoder_settings, "rate_control", "CBR");
+		obs_data_set_string(vencoder_settings, "profile", "main");
+		obs_data_set_string(vencoder_settings, "preset", "veryfast");
+	} else {
+		obs_data_set_int(vencoder_settings, "crf", 20);
+		obs_data_set_string(vencoder_settings, "rate_control", "CRF");
+		obs_data_set_string(vencoder_settings, "profile", "high");
+		obs_data_set_string(vencoder_settings, "preset", "veryfast");
+	}
+
+	/* -----------------------------------*/
+	/* apply settings                     */
+
+	obs_encoder_update(vencoder, vencoder_settings);
+	obs_encoder_update(aencoder, aencoder_settings);
+
+	/* -----------------------------------*/
+	/* connect encoders/services/outputs  */
+
+	obs_output_set_video_encoder(output, vencoder);
+	obs_output_set_audio_encoder(output, aencoder, 0);
+
+	/* -----------------------------------*/
+	/* connect signals                    */
+
+	auto on_stopped = [&]() {
+		unique_lock<mutex> lock(m);
+		cv.notify_one();
+	};
+
+	using on_stopped_t = decltype(on_stopped);
+
+	auto pre_on_stopped = [](void *data, calldata_t *) {
+		on_stopped_t &on_stopped = *reinterpret_cast<on_stopped_t *>(data);
+		on_stopped();
+	};
+
+	signal_handler *sh = obs_output_get_signal_handler(output);
+	signal_handler_connect(sh, "deactivate", pre_on_stopped, &on_stopped);
+
+	/* -----------------------------------*/
+	/* calculate starting resolution      */
+
+	int baseCX = wiz->baseResolutionCX;
+	int baseCY = wiz->baseResolutionCY;
+	CalcBaseRes(baseCX, baseCY);
+
+	/* -----------------------------------*/
+	/* calculate starting test rates      */
+
+	int pcores = os_get_physical_cores();
+	int lcores = os_get_logical_cores();
+	int maxDataRate;
+	if (lcores > 8 || pcores > 4) {
+		/* superb */
+		maxDataRate = 1920 * 1200 * 60 + 1000;
+
+	} else if (lcores > 4 && pcores == 4) {
+		/* great */
+		maxDataRate = 1920 * 1080 * 60 + 1000;
+
+	} else if (pcores == 4) {
+		/* okay */
+		maxDataRate = 1920 * 1080 * 30 + 1000;
+
+	} else {
+		/* toaster */
+		maxDataRate = 960 * 540 * 30 + 1000;
+	}
+
+	/* -----------------------------------*/
+	/* perform tests                      */
+
+	vector<Result> results;
+	int i = 0;
+	int count = 1;
+
+	auto testRes = [&](int cy, int fps_num, int fps_den, bool force) {
+		int per = ++i * 100 / count;
+		QMetaObject::invokeMethod(this, "Progress", Q_ARG(int, per));
+
+		if (cy > baseCY)
+			return true;
+
+		/* no need for more than 3 tests max */
+		if (results.size() >= 3)
+			return true;
+
+		if (!fps_num || !fps_den) {
+			fps_num = wiz->specificFPSNum;
+			fps_den = wiz->specificFPSDen;
+		}
+
+		long double fps = ((long double)fps_num / (long double)fps_den);
+
+		int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy);
+
+		if (!force && wiz->type != AutoConfig::Type::Recording) {
+			int est = EstimateMinBitrate(cx, cy, fps_num, fps_den);
+			if (est > wiz->idealBitrate)
+				return true;
+		}
+
+		long double rate = (long double)cx * (long double)cy * fps;
+		if (!force && rate > maxDataRate)
+			return true;
+
+		testMode.SetVideo(cx, cy, fps_num, fps_den);
+
+		obs_encoder_set_video(vencoder, obs_get_video());
+		obs_encoder_set_audio(aencoder, obs_get_audio());
+		obs_encoder_update(vencoder, vencoder_settings);
+
+		obs_output_set_media(output, obs_get_video(), obs_get_audio());
+
+		QString cxStr = QString::number(cx);
+		QString cyStr = QString::number(cy);
+
+		QString fpsStr = (fps_den > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2);
+
+		QMetaObject::invokeMethod(this, "UpdateMessage",
+					  Q_ARG(QString, QTStr(TEST_RES_VAL).arg(cxStr, cyStr, fpsStr)));
+
+		unique_lock<mutex> ul(m);
+		if (cancel)
+			return false;
+
+		if (!obs_output_start(output)) {
+			QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, QTStr(TEST_RES_FAIL)));
+			return false;
+		}
+
+		cv.wait_for(ul, chrono::seconds(5));
+
+		obs_output_stop(output);
+		cv.wait(ul);
+
+		int skipped = (int)video_output_get_skipped_frames(obs_get_video());
+		if (force || skipped <= 10)
+			results.emplace_back(cx, cy, fps_num, fps_den);
+
+		return !cancel;
+	};
+
+	if (wiz->specificFPSNum && wiz->specificFPSDen) {
+		count = 7;
+		if (!testRes(2160, 0, 0, false))
+			return false;
+		if (!testRes(1440, 0, 0, false))
+			return false;
+		if (!testRes(1080, 0, 0, false))
+			return false;
+		if (!testRes(720, 0, 0, false))
+			return false;
+		if (!testRes(480, 0, 0, false))
+			return false;
+		if (!testRes(360, 0, 0, false))
+			return false;
+		if (!testRes(240, 0, 0, true))
+			return false;
+	} else {
+		count = 14;
+		if (!testRes(2160, 60, 1, false))
+			return false;
+		if (!testRes(2160, 30, 1, false))
+			return false;
+		if (!testRes(1440, 60, 1, false))
+			return false;
+		if (!testRes(1440, 30, 1, false))
+			return false;
+		if (!testRes(1080, 60, 1, false))
+			return false;
+		if (!testRes(1080, 30, 1, false))
+			return false;
+		if (!testRes(720, 60, 1, false))
+			return false;
+		if (!testRes(720, 30, 1, false))
+			return false;
+		if (!testRes(480, 60, 1, false))
+			return false;
+		if (!testRes(480, 30, 1, false))
+			return false;
+		if (!testRes(360, 60, 1, false))
+			return false;
+		if (!testRes(360, 30, 1, false))
+			return false;
+		if (!testRes(240, 60, 1, false))
+			return false;
+		if (!testRes(240, 30, 1, true))
+			return false;
+	}
+
+	/* -----------------------------------*/
+	/* find preferred settings            */
+
+	int minArea = 960 * 540 + 1000;
+
+	if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) {
+		Result &result1 = results[0];
+		Result &result2 = results[1];
+
+		if (result1.fps_num == 30 && result2.fps_num == 60) {
+			int nextArea = result2.cx * result2.cy;
+			if (nextArea >= minArea)
+				results.erase(results.begin());
+		}
+	}
+
+	Result result = results.front();
+	wiz->idealResolutionCX = result.cx;
+	wiz->idealResolutionCY = result.cy;
+	wiz->idealFPSNum = result.fps_num;
+	wiz->idealFPSDen = result.fps_den;
+
+	long double fUpperBitrate = EstimateUpperBitrate(result.cx, result.cy, result.fps_num, result.fps_den);
+
+	int upperBitrate = int(floor(fUpperBitrate / 50.0l) * 50.0l);
+
+	if (wiz->streamingEncoder != AutoConfig::Encoder::x264) {
+		upperBitrate *= 114;
+		upperBitrate /= 100;
+	}
+
+	if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful &&
+	    !wiz->multitrackVideo.bitrate.has_value())
+		wiz->multitrackVideo.bitrate = wiz->idealBitrate;
+
+	if (wiz->idealBitrate > upperBitrate)
+		wiz->idealBitrate = upperBitrate;
+
+	softwareTested = true;
+	return true;
+}
+
+void AutoConfigTestPage::FindIdealHardwareResolution()
+{
+	int baseCX = wiz->baseResolutionCX;
+	int baseCY = wiz->baseResolutionCY;
+	CalcBaseRes(baseCX, baseCY);
+
+	vector<Result> results;
+
+	int pcores = os_get_physical_cores();
+	int maxDataRate;
+	if (pcores >= 4) {
+		maxDataRate = 1920 * 1200 * 60 + 1000;
+	} else {
+		maxDataRate = 1280 * 720 * 30 + 1000;
+	}
+
+	auto testRes = [&](int cy, int fps_num, int fps_den, bool force) {
+		if (cy > baseCY)
+			return;
+
+		if (results.size() >= 3)
+			return;
+
+		if (!fps_num || !fps_den) {
+			fps_num = wiz->specificFPSNum;
+			fps_den = wiz->specificFPSDen;
+		}
+
+		long double fps = ((long double)fps_num / (long double)fps_den);
+
+		int cx = int(((long double)baseCX / (long double)baseCY) * (long double)cy);
+
+		long double rate = (long double)cx * (long double)cy * fps;
+		if (!force && rate > maxDataRate)
+			return;
+
+		AutoConfig::Encoder encType = wiz->streamingEncoder;
+		bool nvenc = encType == AutoConfig::Encoder::NVENC;
+
+		int minBitrate = EstimateMinBitrate(cx, cy, fps_num, fps_den);
+
+		/* most hardware encoders don't have a good quality to bitrate
+		 * ratio, so increase the minimum bitrate estimate for them.
+		 * NVENC currently is the exception because of the improvements
+		 * its made to its quality in recent generations. */
+		if (!nvenc)
+			minBitrate = minBitrate * 114 / 100;
+
+		if (wiz->type == AutoConfig::Type::Recording)
+			force = true;
+		if (force || wiz->idealBitrate >= minBitrate)
+			results.emplace_back(cx, cy, fps_num, fps_den);
+	};
+
+	if (wiz->specificFPSNum && wiz->specificFPSDen) {
+		testRes(2160, 0, 0, false);
+		testRes(1440, 0, 0, false);
+		testRes(1080, 0, 0, false);
+		testRes(720, 0, 0, false);
+		testRes(480, 0, 0, false);
+		testRes(360, 0, 0, false);
+		testRes(240, 0, 0, true);
+	} else {
+		testRes(2160, 60, 1, false);
+		testRes(2160, 30, 1, false);
+		testRes(1440, 60, 1, false);
+		testRes(1440, 30, 1, false);
+		testRes(1080, 60, 1, false);
+		testRes(1080, 30, 1, false);
+		testRes(720, 60, 1, false);
+		testRes(720, 30, 1, false);
+		testRes(480, 60, 1, false);
+		testRes(480, 30, 1, false);
+		testRes(360, 60, 1, false);
+		testRes(360, 30, 1, false);
+		testRes(240, 60, 1, false);
+		testRes(240, 30, 1, true);
+	}
+
+	int minArea = 960 * 540 + 1000;
+
+	if (!wiz->specificFPSNum && wiz->preferHighFPS && results.size() > 1) {
+		Result &result1 = results[0];
+		Result &result2 = results[1];
+
+		if (result1.fps_num == 30 && result2.fps_num == 60) {
+			int nextArea = result2.cx * result2.cy;
+			if (nextArea >= minArea)
+				results.erase(results.begin());
+		}
+	}
+
+	Result result = results.front();
+	wiz->idealResolutionCX = result.cx;
+	wiz->idealResolutionCY = result.cy;
+	wiz->idealFPSNum = result.fps_num;
+	wiz->idealFPSDen = result.fps_den;
+}
+
+void AutoConfigTestPage::TestStreamEncoderThread()
+{
+	bool preferHardware = wiz->preferHardware;
+	if (!softwareTested) {
+		if (!preferHardware || !wiz->hardwareEncodingAvailable) {
+			if (!TestSoftwareEncoding()) {
+				return;
+			}
+		}
+	}
+
+	if (!softwareTested) {
+		if (wiz->nvencAvailable)
+			wiz->streamingEncoder = AutoConfig::Encoder::NVENC;
+		else if (wiz->qsvAvailable)
+			wiz->streamingEncoder = AutoConfig::Encoder::QSV;
+		else if (wiz->appleAvailable)
+			wiz->streamingEncoder = AutoConfig::Encoder::Apple;
+		else
+			wiz->streamingEncoder = AutoConfig::Encoder::AMD;
+	} else {
+		wiz->streamingEncoder = AutoConfig::Encoder::x264;
+	}
+
+#ifdef __linux__
+	// On linux CBR rate control is not guaranteed so fallback to x264.
+	if (wiz->streamingEncoder == AutoConfig::Encoder::QSV) {
+		wiz->streamingEncoder = AutoConfig::Encoder::x264;
+		if (!TestSoftwareEncoding()) {
+			return;
+		}
+	}
+#endif
+
+	if (preferHardware && !softwareTested && wiz->hardwareEncodingAvailable)
+		FindIdealHardwareResolution();
+
+	QMetaObject::invokeMethod(this, "NextStage");
+}
+
+void AutoConfigTestPage::TestRecordingEncoderThread()
+{
+	if (!wiz->hardwareEncodingAvailable && !softwareTested) {
+		if (!TestSoftwareEncoding()) {
+			return;
+		}
+	}
+
+	if (wiz->type == AutoConfig::Type::Recording && wiz->hardwareEncodingAvailable)
+		FindIdealHardwareResolution();
+
+	wiz->recordingQuality = AutoConfig::Quality::High;
+
+	bool recordingOnly = wiz->type == AutoConfig::Type::Recording;
+
+	if (wiz->hardwareEncodingAvailable) {
+		if (wiz->nvencAvailable)
+			wiz->recordingEncoder = AutoConfig::Encoder::NVENC;
+		else if (wiz->qsvAvailable)
+			wiz->recordingEncoder = AutoConfig::Encoder::QSV;
+		else if (wiz->appleAvailable)
+			wiz->recordingEncoder = AutoConfig::Encoder::Apple;
+		else
+			wiz->recordingEncoder = AutoConfig::Encoder::AMD;
+	} else {
+		wiz->recordingEncoder = AutoConfig::Encoder::x264;
+	}
+
+	if (wiz->recordingEncoder != AutoConfig::Encoder::NVENC) {
+		if (!recordingOnly) {
+			wiz->recordingEncoder = AutoConfig::Encoder::Stream;
+			wiz->recordingQuality = AutoConfig::Quality::Stream;
+		}
+	}
+
+	QMetaObject::invokeMethod(this, "NextStage");
+}
+
+#define ENCODER_TEXT(x) "Basic.Settings.Output.Simple.Encoder." x
+#define ENCODER_SOFTWARE ENCODER_TEXT("Software")
+#define ENCODER_NVENC ENCODER_TEXT("Hardware.NVENC.H264")
+#define ENCODER_QSV ENCODER_TEXT("Hardware.QSV.H264")
+#define ENCODER_AMD ENCODER_TEXT("Hardware.AMD.H264")
+#define ENCODER_APPLE ENCODER_TEXT("Hardware.Apple.H264")
+
+#define QUALITY_SAME "Basic.Settings.Output.Simple.RecordingQuality.Stream"
+#define QUALITY_HIGH "Basic.Settings.Output.Simple.RecordingQuality.Small"
+
+void set_closest_res(int &cx, int &cy, struct obs_service_resolution *res_list, size_t count)
+{
+	int best_pixel_diff = 0x7FFFFFFF;
+	int start_cx = cx;
+	int start_cy = cy;
+
+	for (size_t i = 0; i < count; i++) {
+		struct obs_service_resolution &res = res_list[i];
+		int pixel_cx_diff = abs(start_cx - res.cx);
+		int pixel_cy_diff = abs(start_cy - res.cy);
+		int pixel_diff = pixel_cx_diff + pixel_cy_diff;
+
+		if (pixel_diff < best_pixel_diff) {
+			best_pixel_diff = pixel_diff;
+			cx = res.cx;
+			cy = res.cy;
+		}
+	}
+}
+
+void AutoConfigTestPage::FinalizeResults()
+{
+	ui->stackedWidget->setCurrentIndex(1);
+	setSubTitle(QTStr(SUBTITLE_COMPLETE));
+
+	QFormLayout *form = results;
+
+	auto encName = [](AutoConfig::Encoder enc) -> QString {
+		switch (enc) {
+		case AutoConfig::Encoder::x264:
+			return QTStr(ENCODER_SOFTWARE);
+		case AutoConfig::Encoder::NVENC:
+			return QTStr(ENCODER_NVENC);
+		case AutoConfig::Encoder::QSV:
+			return QTStr(ENCODER_QSV);
+		case AutoConfig::Encoder::AMD:
+			return QTStr(ENCODER_AMD);
+		case AutoConfig::Encoder::Apple:
+			return QTStr(ENCODER_APPLE);
+		case AutoConfig::Encoder::Stream:
+			return QTStr(QUALITY_SAME);
+		}
+
+		return QTStr(ENCODER_SOFTWARE);
+	};
+
+	auto newLabel = [this](const char *str) -> QLabel * {
+		return new QLabel(QTStr(str), this);
+	};
+
+	if (wiz->type == AutoConfig::Type::Streaming) {
+		const char *serverType = wiz->customServer ? "rtmp_custom" : "rtmp_common";
+
+		OBSServiceAutoRelease service = obs_service_create(serverType, "temp_service", nullptr, nullptr);
+
+		OBSDataAutoRelease service_settings = obs_data_create();
+		OBSDataAutoRelease vencoder_settings = obs_data_create();
+
+		if (wiz->testMultitrackVideo && wiz->multitrackVideo.testSuccessful &&
+		    !wiz->multitrackVideo.bitrate.has_value())
+			wiz->multitrackVideo.bitrate = wiz->idealBitrate;
+
+		obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate);
+
+		obs_data_set_string(service_settings, "service", wiz->serviceName.c_str());
+		obs_service_update(service, service_settings);
+		obs_service_apply_encoder_settings(service, vencoder_settings, nullptr);
+
+		BPtr<obs_service_resolution> res_list;
+		size_t res_count;
+		int maxFPS;
+		obs_service_get_supported_resolutions(service, &res_list, &res_count);
+		obs_service_get_max_fps(service, &maxFPS);
+
+		if (res_list) {
+			set_closest_res(wiz->idealResolutionCX, wiz->idealResolutionCY, res_list, res_count);
+		}
+		if (maxFPS) {
+			double idealFPS = (double)wiz->idealFPSNum / (double)wiz->idealFPSDen;
+			if (idealFPS > (double)maxFPS) {
+				wiz->idealFPSNum = maxFPS;
+				wiz->idealFPSDen = 1;
+			}
+		}
+
+		wiz->idealBitrate = (int)obs_data_get_int(vencoder_settings, "bitrate");
+
+		if (!wiz->customServer)
+			form->addRow(newLabel("Basic.AutoConfig.StreamPage.Service"),
+				     new QLabel(wiz->serviceName.c_str(), ui->finishPage));
+		form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"),
+			     new QLabel(wiz->serverName.c_str(), ui->finishPage));
+		form->addRow(newLabel("Basic.Settings.Stream.MultitrackVideoLabel"),
+			     newLabel(wiz->multitrackVideo.testSuccessful ? "Yes" : "No"));
+
+		if (wiz->multitrackVideo.testSuccessful) {
+			form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"), newLabel("Automatic"));
+			form->addRow(newLabel(TEST_RESULT_SE), newLabel("Automatic"));
+			form->addRow(newLabel("Basic.AutoConfig.TestPage.Result.StreamingResolution"),
+				     newLabel("Automatic"));
+		} else {
+			form->addRow(newLabel("Basic.Settings.Output.VideoBitrate"),
+				     new QLabel(QString::number(wiz->idealBitrate), ui->finishPage));
+			form->addRow(newLabel(TEST_RESULT_SE),
+				     new QLabel(encName(wiz->streamingEncoder), ui->finishPage));
+		}
+	}
+
+	QString baseRes =
+		QString("%1x%2").arg(QString::number(wiz->baseResolutionCX), QString::number(wiz->baseResolutionCY));
+	QString scaleRes =
+		QString("%1x%2").arg(QString::number(wiz->idealResolutionCX), QString::number(wiz->idealResolutionCY));
+
+	if (wiz->recordingEncoder != AutoConfig::Encoder::Stream ||
+	    wiz->recordingQuality != AutoConfig::Quality::Stream)
+		form->addRow(newLabel(TEST_RESULT_RE), new QLabel(encName(wiz->recordingEncoder), ui->finishPage));
+
+	QString recQuality;
+
+	switch (wiz->recordingQuality) {
+	case AutoConfig::Quality::High:
+		recQuality = QTStr(QUALITY_HIGH);
+		break;
+	case AutoConfig::Quality::Stream:
+		recQuality = QTStr(QUALITY_SAME);
+		break;
+	}
+
+	form->addRow(newLabel("Basic.Settings.Output.Simple.RecordingQuality"), new QLabel(recQuality, ui->finishPage));
+
+	long double fps = (long double)wiz->idealFPSNum / (long double)wiz->idealFPSDen;
+
+	QString fpsStr = (wiz->idealFPSDen > 1) ? QString::number(fps, 'f', 2) : QString::number(fps, 'g', 2);
+
+	form->addRow(newLabel("Basic.Settings.Video.BaseResolution"), new QLabel(baseRes, ui->finishPage));
+	form->addRow(newLabel("Basic.Settings.Video.ScaledResolution"), new QLabel(scaleRes, ui->finishPage));
+	form->addRow(newLabel("Basic.Settings.Video.FPS"), new QLabel(fpsStr, ui->finishPage));
+
+	// FIXME: form layout is super squished, probably need to set proper sizepolicy on all widgets?
+}
+
+#define STARTING_SEPARATOR "\n==== Auto-config wizard testing commencing ======\n"
+#define STOPPING_SEPARATOR "\n==== Auto-config wizard testing stopping ========\n"
+
+void AutoConfigTestPage::NextStage()
+{
+	if (testThread.joinable())
+		testThread.join();
+	if (cancel)
+		return;
+
+	ui->subProgressLabel->setText(QString());
+
+	/* make it skip to bandwidth stage if only set to config recording */
+	if (stage == Stage::Starting) {
+		if (!started) {
+			blog(LOG_INFO, STARTING_SEPARATOR);
+			started = true;
+		}
+
+		if (wiz->type != AutoConfig::Type::Streaming) {
+			stage = Stage::StreamEncoder;
+		} else if (!wiz->bandwidthTest) {
+			stage = Stage::BandwidthTest;
+		}
+	}
+
+	if (stage == Stage::Starting) {
+		stage = Stage::BandwidthTest;
+		StartBandwidthStage();
+
+	} else if (stage == Stage::BandwidthTest) {
+		stage = Stage::StreamEncoder;
+		StartStreamEncoderStage();
+
+	} else if (stage == Stage::StreamEncoder) {
+		stage = Stage::RecordingEncoder;
+		StartRecordingEncoderStage();
+
+	} else {
+		stage = Stage::Finished;
+		FinalizeResults();
+		emit completeChanged();
+	}
+}
+
+void AutoConfigTestPage::UpdateMessage(QString message)
+{
+	ui->subProgressLabel->setText(message);
+}
+
+void AutoConfigTestPage::Failure(QString message)
+{
+	ui->errorLabel->setText(message);
+	ui->stackedWidget->setCurrentIndex(2);
+}
+
+void AutoConfigTestPage::Progress(int percentage)
+{
+	ui->progressBar->setValue(percentage);
+}
+
+AutoConfigTestPage::AutoConfigTestPage(QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigTestPage)
+{
+	ui->setupUi(this);
+	setTitle(QTStr("Basic.AutoConfig.TestPage"));
+	setSubTitle(QTStr(SUBTITLE_TESTING));
+	setCommitPage(true);
+}
+
+AutoConfigTestPage::~AutoConfigTestPage()
+{
+	if (testThread.joinable()) {
+		{
+			unique_lock<mutex> ul(m);
+			cancel = true;
+			cv.notify_one();
+		}
+		testThread.join();
+	}
+
+	if (started)
+		blog(LOG_INFO, STOPPING_SEPARATOR);
+}
+
+void AutoConfigTestPage::initializePage()
+{
+	if (wiz->type == AutoConfig::Type::VirtualCam) {
+		wiz->idealResolutionCX = wiz->baseResolutionCX;
+		wiz->idealResolutionCY = wiz->baseResolutionCY;
+		wiz->idealFPSNum = 30;
+		wiz->idealFPSDen = 1;
+		stage = Stage::Finished;
+	} else {
+		stage = Stage::Starting;
+	}
+
+	setSubTitle(QTStr(SUBTITLE_TESTING));
+	softwareTested = false;
+	cancel = false;
+	DeleteLayout(results);
+	results = new QFormLayout();
+	results->setContentsMargins(0, 0, 0, 0);
+	ui->finishPageLayout->insertLayout(1, results);
+	ui->stackedWidget->setCurrentIndex(0);
+	NextStage();
+}
+
+void AutoConfigTestPage::cleanupPage()
+{
+	if (testThread.joinable()) {
+		{
+			unique_lock<mutex> ul(m);
+			cancel = true;
+			cv.notify_one();
+		}
+		testThread.join();
+	}
+}
+
+bool AutoConfigTestPage::isComplete() const
+{
+	return stage == Stage::Finished;
+}
+
+int AutoConfigTestPage::nextId() const
+{
+	return -1;
+}