diff --git a/UI/window-basic-auto-config.cpp b/frontend/wizards/AutoConfig.cpp similarity index 100% rename from UI/window-basic-auto-config.cpp rename to frontend/wizards/AutoConfig.cpp diff --git a/UI/window-basic-auto-config.hpp b/frontend/wizards/AutoConfig.hpp similarity index 100% rename from UI/window-basic-auto-config.hpp rename to frontend/wizards/AutoConfig.hpp diff --git a/frontend/wizards/AutoConfigStartPage.cpp b/frontend/wizards/AutoConfigStartPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#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 +#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(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = 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(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 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(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(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(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 = "%1 "; + + 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(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(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(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(*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(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); +} diff --git a/frontend/wizards/AutoConfigStartPage.hpp b/frontend/wizards/AutoConfigStartPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigStartPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 targetBitrate; + std::optional 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 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; + +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; + +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; + + std::unique_ptr 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 results; + + std::unique_ptr 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 &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); +}; diff --git a/frontend/wizards/AutoConfigStreamPage.cpp b/frontend/wizards/AutoConfigStreamPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigStreamPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#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 +#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(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = 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(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 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(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(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(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 = "%1 "; + + 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(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(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(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(*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(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); +} diff --git a/frontend/wizards/AutoConfigStreamPage.hpp b/frontend/wizards/AutoConfigStreamPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigStreamPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 targetBitrate; + std::optional 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 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; + +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; + +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; + + std::unique_ptr 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 results; + + std::unique_ptr 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 &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); +}; diff --git a/UI/window-basic-auto-config-test.cpp b/frontend/wizards/AutoConfigTestPage.cpp similarity index 100% rename from UI/window-basic-auto-config-test.cpp rename to frontend/wizards/AutoConfigTestPage.cpp diff --git a/frontend/wizards/AutoConfigTestPage.hpp b/frontend/wizards/AutoConfigTestPage.hpp new file mode 100644 index 000000000..5d2d414a6 --- /dev/null +++ b/frontend/wizards/AutoConfigTestPage.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include +#include +#include + +class QFormLayout; +class Ui_AutoConfigTestPage; + +class AutoConfigTestPage : public QWizardPage { + Q_OBJECT + + friend class AutoConfig; + + QPointer results; + + std::unique_ptr 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 &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); +}; diff --git a/frontend/wizards/AutoConfigVideoPage.cpp b/frontend/wizards/AutoConfigVideoPage.cpp new file mode 100644 index 000000000..6b1667609 --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.cpp @@ -0,0 +1,1240 @@ +#include +#include + +#include +#include + +#include + +#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 +#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(wizard()) + +/* ------------------------------------------------------------------------- */ + +constexpr std::string_view OBSServiceFileName = "service.json"; + +static OBSData OpenServiceSettings(std::string &type) +{ + const OBSBasic *basic = reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = 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(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 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(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(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(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 = "%1 "; + + 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(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(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(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(*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(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); +} diff --git a/frontend/wizards/AutoConfigVideoPage.hpp b/frontend/wizards/AutoConfigVideoPage.hpp new file mode 100644 index 000000000..7e31bdf89 --- /dev/null +++ b/frontend/wizards/AutoConfigVideoPage.hpp @@ -0,0 +1,288 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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 targetBitrate; + std::optional 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 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; + +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; + +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; + + std::unique_ptr 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 results; + + std::unique_ptr 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 &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); +}; diff --git a/frontend/wizards/TestMode.hpp b/frontend/wizards/TestMode.hpp new file mode 100644 index 000000000..b53f37aa2 --- /dev/null +++ b/frontend/wizards/TestMode.hpp @@ -0,0 +1,1261 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "window-basic-auto-config.hpp" +#include "window-basic-main.hpp" +#include "obs-app.hpp" + +#include "ui_AutoConfigTestPage.h" + +#define wiz reinterpret_cast(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 &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(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 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 lock(m); + connected = true; + stopped = false; + cv.notify_one(); + }; + + auto on_stopped = [&]() { + unique_lock 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(data); + on_started(); + }; + + auto pre_on_stopped = [](void *data, calldata_t *) { + on_stopped_t &on_stopped = *reinterpret_cast(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 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 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(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 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 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 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 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 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 ul(m); + cancel = true; + cv.notify_one(); + } + testThread.join(); + } +} + +bool AutoConfigTestPage::isComplete() const +{ + return stage == Stage::Finished; +} + +int AutoConfigTestPage::nextId() const +{ + return -1; +}