From 90faf150ecce1f52b199e971407466b84c6d035b Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 15 Jun 2023 21:12:19 +0200 Subject: [PATCH 01/65] Revert "UI: Enable WHIP service in UI" This reverts commit: - d31d271b43ed6a119fca4499431388b296ad5e75 - 353a4c860ddd2077aa2ab970964a23565d42bb12 - dd392188b8f60ebc7e600ea803eda5304b4aebf9 --- UI/data/locale/en-US.ini | 1 - UI/window-basic-main.cpp | 3 +- UI/window-basic-settings-stream.cpp | 76 +++++------------------------ UI/window-basic-settings.hpp | 1 - 4 files changed, 13 insertions(+), 68 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 4b2f4de90dbd0f..770263abb6b065 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -208,7 +208,6 @@ Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Stream Key" Basic.AutoConfig.StreamPage.StreamKey.ToolTip="RIST: enter the encryption passphrase.\nRTMP: enter the key provided by the service.\nSRT: enter the streamid if the service uses one." Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" -Basic.AutoConfig.StreamPage.BearerToken="Bearer Token" Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 9807cfd83378ec..581adf3f55115a 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1355,8 +1355,7 @@ bool OBSBasic::LoadService() return false; /* Enforce Opus on FTL if needed */ - if (strcmp(obs_service_get_protocol(service), "FTL") == 0 || - strcmp(obs_service_get_protocol(service), "WHIP") == 0) { + if (strcmp(obs_service_get_protocol(service), "FTL") == 0) { const char *option = config_get_string( basicConfig, "SimpleOutput", "StreamAudioEncoder"); if (strcmp(option, "opus") != 0) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 052d8dd37a9500..939afafc1ff812 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -29,7 +29,6 @@ extern QCefCookieManager *panel_cookies; enum class ListOpt : int { ShowAll = 1, Custom, - WHIP, }; enum class Section : int { @@ -42,11 +41,6 @@ inline bool OBSBasicSettings::IsCustomService() const return ui->service->currentData().toInt() == (int)ListOpt::Custom; } -inline bool OBSBasicSettings::IsWHIP() const -{ - return ui->service->currentData().toInt() == (int)ListOpt::WHIP; -} - void OBSBasicSettings::InitStreamPage() { ui->connectAccount2->setVisible(false); @@ -97,9 +91,6 @@ void OBSBasicSettings::LoadStream1Settings() obs_service_t *service_obj = main->GetService(); const char *type = obs_service_get_type(service_obj); - bool is_rtmp_custom = (strcmp(type, "rtmp_custom") == 0); - bool is_rtmp_common = (strcmp(type, "rtmp_common") == 0); - bool is_whip = (strcmp(type, "whip_custom") == 0); loading = true; @@ -109,14 +100,10 @@ void OBSBasicSettings::LoadStream1Settings() const char *server = obs_data_get_string(settings, "server"); const char *key = obs_data_get_string(settings, "key"); protocol = QT_UTF8(obs_service_get_protocol(service_obj)); - const char *bearer_token = - obs_data_get_string(settings, "bearer_token"); - - if (is_rtmp_custom || is_whip) - ui->customServer->setText(server); - if (is_rtmp_custom) { + if (strcmp(type, "rtmp_custom") == 0) { ui->service->setCurrentIndex(0); + ui->customServer->setText(server); lastServiceIdx = 0; lastCustomServer = ui->customServer->text(); @@ -170,7 +157,7 @@ void OBSBasicSettings::LoadStream1Settings() UpdateServerList(); - if (is_rtmp_common) { + if (strcmp(type, "rtmp_common") == 0) { int idx = ui->server->findData(server); if (idx == -1) { if (server && *server) @@ -180,10 +167,7 @@ void OBSBasicSettings::LoadStream1Settings() ui->server->setCurrentIndex(idx); } - if (is_whip) - ui->key->setText(bearer_token); - else - ui->key->setText(key); + ui->key->setText(key); lastService.clear(); ServiceChanged(); @@ -207,21 +191,14 @@ void OBSBasicSettings::LoadStream1Settings() void OBSBasicSettings::SaveStream1Settings() { bool customServer = IsCustomService(); - bool whip = IsWHIP(); - const char *service_id = "rtmp_common"; - - if (customServer) { - service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; - } + 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 && !whip) { + if (!customServer) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); @@ -262,14 +239,7 @@ void OBSBasicSettings::SaveStream1Settings() obs_data_set_bool(settings, "bwtest", false); } - if (whip) { - obs_data_set_string(settings, "service", "WHIP"); - obs_data_set_string(settings, "bearer_token", - QT_TO_UTF8(ui->key->text())); - } else { - obs_data_set_string(settings, "key", - QT_TO_UTF8(ui->key->text())); - } + obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); OBSServiceAutoRelease newService = obs_service_create( service_id, "default_service", settings, hotkeyData); @@ -292,7 +262,7 @@ void OBSBasicSettings::SaveStream1Settings() void OBSBasicSettings::UpdateMoreInfoLink() { - if (IsCustomService() || IsWHIP()) { + if (IsCustomService()) { ui->moreInfoButton->hide(); return; } @@ -342,9 +312,6 @@ void OBSBasicSettings::UpdateKeyLink() if (serviceName == "Dacast") { ui->streamKeyLabel->setText( QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - } else if (IsWHIP()) { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.BearerToken")); } else if (!IsCustomService()) { ui->streamKeyLabel->setText( QTStr("Basic.AutoConfig.StreamPage.StreamKey")); @@ -389,11 +356,6 @@ void OBSBasicSettings::LoadServices(bool showAll) for (QString &name : names) ui->service->addItem(name); - if (obs_is_output_protocol_registered("WHIP")) { - ui->service->addItem(QTStr("WHIP"), - QVariant((int)ListOpt::WHIP)); - } - if (!showAll) { ui->service->addItem( QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), @@ -522,7 +484,6 @@ void OBSBasicSettings::ServiceChanged() { std::string service = QT_TO_UTF8(ui->service->currentText()); bool custom = IsCustomService(); - bool whip = IsWHIP(); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); @@ -539,7 +500,7 @@ void OBSBasicSettings::ServiceChanged() ui->authPwLabel->setVisible(custom); ui->authPwWidget->setVisible(custom); - if (custom || whip) { + if (custom) { ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); @@ -667,18 +628,11 @@ void OBSBasicSettings::on_authPwShow_clicked() OBSService OBSBasicSettings::SpawnTempService() { bool custom = IsCustomService(); - bool whip = IsWHIP(); - const char *service_id = "rtmp_common"; - - if (custom) { - service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; - } + const char *service_id = custom ? "rtmp_custom" : "rtmp_common"; OBSDataAutoRelease settings = obs_data_create(); - if (!custom && !whip) { + if (!custom) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string( @@ -689,13 +643,7 @@ OBSService OBSBasicSettings::SpawnTempService() settings, "server", QT_TO_UTF8(ui->customServer->text().trimmed())); } - - if (whip) - obs_data_set_string(settings, "bearer_token", - QT_TO_UTF8(ui->key->text())); - else - obs_data_set_string(settings, "key", - QT_TO_UTF8(ui->key->text())); + obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); OBSServiceAutoRelease newService = obs_service_create( service_id, "temp_service", settings, nullptr); diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index ac99da9deaf7f1..33b5b107cc576a 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -258,7 +258,6 @@ class OBSBasicSettings : public QDialog { /* stream */ void InitStreamPage(); inline bool IsCustomService() const; - inline bool IsWHIP() const; void LoadServices(bool showAll); void OnOAuthStreamKeyConnected(); void OnAuthConnected(); From 5924bbbbaadff7a520e05e589ea62b906c280e17 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 22 May 2023 12:16:35 +0200 Subject: [PATCH 02/65] Revert "UI: Work around Qt dock restore crash" This reverts commit 3dcf68f8ed500a23154f502a85caa109afd04d81. --- UI/auth-restream.cpp | 4 +--- UI/auth-twitch.cpp | 8 ++------ UI/auth-youtube.cpp | 4 +--- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp index 9188da118c9684..db1167d453cbf8 100644 --- a/UI/auth-restream.cpp +++ b/UI/auth-restream.cpp @@ -213,9 +213,7 @@ void RestreamAuth::LoadUI() main->Config(), service(), "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); + main->restoreState(dockState); } uiLoaded = true; diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp index 386a0e9275cc9a..0ffc4d663da899 100644 --- a/UI/auth-twitch.cpp +++ b/UI/auth-twitch.cpp @@ -293,9 +293,7 @@ void TwitchAuth::LoadUI() main->Config(), service(), "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); + main->restoreState(dockState); } TryLoadSecondaryUIPanes(); @@ -421,9 +419,7 @@ void TwitchAuth::LoadSecondaryUIPanes() main->Config(), service(), "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); + main->restoreState(dockState); } } diff --git a/UI/auth-youtube.cpp b/UI/auth-youtube.cpp index 743c12ed577526..31e4673b76cdf6 100644 --- a/UI/auth-youtube.cpp +++ b/UI/auth-youtube.cpp @@ -179,9 +179,7 @@ void YoutubeAuth::LoadUI() main->Config(), service(), "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - - if (main->isVisible() || !main->isMaximized()) - main->restoreState(dockState); + main->restoreState(dockState); } #endif From f518e758dab0ac8dbb469711643aa7b05f18a416 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 22 May 2023 14:57:36 +0200 Subject: [PATCH 03/65] UI: Move dockstate to profile --- UI/auth-restream.cpp | 12 ++--- UI/auth-twitch.cpp | 15 +++--- UI/auth-youtube.cpp | 11 ++-- UI/window-basic-main-profiles.cpp | 63 +++++++++++++++++++++- UI/window-basic-main.cpp | 87 ++++++++++++++++--------------- 5 files changed, 124 insertions(+), 64 deletions(-) diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp index db1167d453cbf8..9d7c4be4d8b108 100644 --- a/UI/auth-restream.cpp +++ b/UI/auth-restream.cpp @@ -110,9 +110,6 @@ try { void RestreamAuth::SaveInternal() { - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); OAuthStreamKey::SaveInternal(); } @@ -208,12 +205,13 @@ void RestreamAuth::LoadUI() chat->setVisible(true); info->setVisible(true); channels->setVisible(true); - } else { + } else if (!config_has_user_value(main->Config(), "BasicWindow", + "DockState")) { const char *dockStateStr = config_get_string( main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - main->restoreState(dockState); + + config_set_string(main->Config(), "BasicWindow", "DockState", + dockStateStr); } uiLoaded = true; diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp index 0ffc4d663da899..177f78c1a8d782 100644 --- a/UI/auth-twitch.cpp +++ b/UI/auth-twitch.cpp @@ -168,10 +168,6 @@ void TwitchAuth::SaveInternal() config_set_string(main->Config(), service(), "Name", name.c_str()); config_set_string(main->Config(), service(), "UUID", uuid.c_str()); - if (uiLoaded) { - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); - } OAuthStreamKey::SaveInternal(); } @@ -288,12 +284,13 @@ void TwitchAuth::LoadUI() if (firstLoad) { chat->setVisible(true); - } else { + } else if (!config_has_user_value(main->Config(), "BasicWindow", + "DockState")) { const char *dockStateStr = config_get_string( main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - main->restoreState(dockState); + + config_set_string(main->Config(), "BasicWindow", "DockState", + dockStateStr); } TryLoadSecondaryUIPanes(); @@ -416,7 +413,7 @@ void TwitchAuth::LoadSecondaryUIPanes() } const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); + main->Config(), "BasicWindow", "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); main->restoreState(dockState); diff --git a/UI/auth-youtube.cpp b/UI/auth-youtube.cpp index 31e4673b76cdf6..de88a999c1a521 100644 --- a/UI/auth-youtube.cpp +++ b/UI/auth-youtube.cpp @@ -95,8 +95,6 @@ bool YoutubeAuth::RetryLogin() void YoutubeAuth::SaveInternal() { OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "DockState", - main->saveState().toBase64().constData()); const char *section_name = section.c_str(); config_set_string(main->Config(), section_name, "RefreshToken", @@ -174,12 +172,13 @@ void YoutubeAuth::LoadUI() if (firstLoad) { chat->setVisible(true); - } else { + } else if (!config_has_user_value(main->Config(), "BasicWindow", + "DockState")) { const char *dockStateStr = config_get_string( main->Config(), service(), "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - main->restoreState(dockState); + + config_set_string(main->Config(), "BasicWindow", "DockState", + dockStateStr); } #endif diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 4fcb8ef7dc0186..266d133691f5cb 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -321,6 +321,17 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, DuplicateCurrentCookieProfile(config); } + if (!rename) { + /* Save dock state before saving */ + config_set_string(basicConfig, "BasicWindow", "DockState", + saveState().toBase64().constData()); + + /* Save dock state on the new profile if duplication */ + if (!create_new) + config_set_string(config, "BasicWindow", "DockState", + saveState().toBase64().constData()); + } + config_set_string(config, "General", "Name", newName.c_str()); basicConfig.SaveSafe("tmp"); config.SaveSafe("tmp"); @@ -351,10 +362,25 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, wizard.exec(); } - if (api && !rename) { + if (rename) + return true; + + if (api) { api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED); } + + if (create_new) { + on_resetDocks_triggered(true); + } else { + /* Restore dock state post duplication + * Plugins might have removed and added back docks */ + const char *dockStateStr = config_get_string( + basicConfig, "BasicWindow", "DockState"); + + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + restoreState(dockState); + } return true; } @@ -638,6 +664,21 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) close(); } } + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) + on_resetDocks_triggered(true); + else { + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } } void OBSBasic::on_actionImportProfile_triggered() @@ -755,6 +796,11 @@ void OBSBasic::ChangeProfile() return; } + /* Save dock state before the changing event is emitted */ + config_set_string(basicConfig, "BasicWindow", "DockState", + saveState().toBase64().constData()); + basicConfig.SaveSafe("tmp"); + size_t path_len = path.size(); path += "/basic.ini"; @@ -813,6 +859,21 @@ void OBSBasic::ChangeProfile() close(); } } + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) + on_resetDocks_triggered(true); + else { + QByteArray dockState = QByteArray::fromBase64(dockStateStr); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } } void OBSBasic::CheckForSimpleModeX264Fallback() diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 581adf3f55115a..e33a0986877883 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2137,46 +2137,6 @@ void OBSBasic::OBSInit() } #endif - const char *dockStateStr = config_get_string( - App()->GlobalConfig(), "BasicWindow", "DockState"); - - if (!dockStateStr) { - on_resetDocks_triggered(true); - } else { - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); - } - - bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General", - "Pre23Defaults"); - if (pre23Defaults) { - bool resetDockLock23 = config_get_bool( - App()->GlobalConfig(), "General", "ResetDockLock23"); - if (!resetDockLock23) { - config_set_bool(App()->GlobalConfig(), "General", - "ResetDockLock23", true); - config_remove_value(App()->GlobalConfig(), - "BasicWindow", "DocksLocked"); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - } - } - - bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow", - "DocksLocked"); - on_lockDocks_toggled(docksLocked); - ui->lockDocks->blockSignals(true); - ui->lockDocks->setChecked(docksLocked); - ui->lockDocks->blockSignals(false); - - bool sideDocks = config_get_bool(App()->GlobalConfig(), "BasicWindow", - "SideDocks"); - on_sideDocks_toggled(sideDocks); - ui->sideDocks->blockSignals(true); - ui->sideDocks->setChecked(sideDocks); - ui->sideDocks->blockSignals(false); - SystemTray(true); TaskbarOverlayInit(); @@ -2323,6 +2283,50 @@ void OBSBasic::OnFirstLoad() if (showLogViewerOnStartup) on_actionViewCurrentLog_triggered(); + + /* Recover DockState from global config if profile has none */ + const char *dockStateStr = config_get_string( + config_has_user_value(basicConfig, "BasicWindow", "DockState") + ? basicConfig + : App()->GlobalConfig(), + "BasicWindow", "DockState"); + + if (!dockStateStr) { + on_resetDocks_triggered(true); + } else { + QByteArray dockState = + QByteArray::fromBase64(QByteArray(dockStateStr)); + if (!restoreState(dockState)) + on_resetDocks_triggered(true); + } + + bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General", + "Pre23Defaults"); + if (pre23Defaults) { + bool resetDockLock23 = config_get_bool( + App()->GlobalConfig(), "General", "ResetDockLock23"); + if (!resetDockLock23) { + config_set_bool(App()->GlobalConfig(), "General", + "ResetDockLock23", true); + config_remove_value(App()->GlobalConfig(), + "BasicWindow", "DocksLocked"); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + } + + bool docksLocked = config_get_bool(App()->GlobalConfig(), "BasicWindow", + "DocksLocked"); + on_lockDocks_toggled(docksLocked); + ui->lockDocks->blockSignals(true); + ui->lockDocks->setChecked(docksLocked); + ui->lockDocks->blockSignals(false); + + bool sideDocks = config_get_bool(App()->GlobalConfig(), "BasicWindow", + "SideDocks"); + on_sideDocks_toggled(sideDocks); + ui->sideDocks->blockSignals(true); + ui->sideDocks->setChecked(sideDocks); + ui->sideDocks->blockSignals(false); } #if defined(OBS_RELEASE_CANDIDATE) && OBS_RELEASE_CANDIDATE > 0 @@ -5068,8 +5072,9 @@ void OBSBasic::closeEvent(QCloseEvent *event) if (program) program->DestroyDisplay(); - config_set_string(App()->GlobalConfig(), "BasicWindow", "DockState", + config_set_string(basicConfig, "BasicWindow", "DockState", saveState().toBase64().constData()); + config_save_safe(basicConfig, "tmp", nullptr); #ifdef BROWSER_AVAILABLE if (cef) From 23c3a2b9d5cc55c0c8e9fef5ab31f46bbae43e9d Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 22 May 2023 16:12:53 +0200 Subject: [PATCH 04/65] UI: Work around Qt dock restore crash --- UI/auth-twitch.cpp | 2 +- UI/window-basic-main-profiles.cpp | 8 +++----- UI/window-basic-main.cpp | 27 +++++++++++++++++++++++++-- UI/window-basic-main.hpp | 3 +++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp index 177f78c1a8d782..c7a2dbe6f0cebf 100644 --- a/UI/auth-twitch.cpp +++ b/UI/auth-twitch.cpp @@ -416,7 +416,7 @@ void TwitchAuth::LoadSecondaryUIPanes() main->Config(), "BasicWindow", "DockState"); QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - main->restoreState(dockState); + main->RestoreState(dockState); } } diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 266d133691f5cb..da2334dca2c604 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -379,7 +379,7 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, basicConfig, "BasicWindow", "DockState"); QByteArray dockState = QByteArray::fromBase64(dockStateStr); - restoreState(dockState); + RestoreState(dockState); } return true; } @@ -676,8 +676,7 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) on_resetDocks_triggered(true); else { QByteArray dockState = QByteArray::fromBase64(dockStateStr); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); + RestoreState(dockState); } } @@ -871,8 +870,7 @@ void OBSBasic::ChangeProfile() on_resetDocks_triggered(true); else { QByteArray dockState = QByteArray::fromBase64(dockStateStr); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); + RestoreState(dockState); } } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index e33a0986877883..06f9ee5827e0db 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2296,8 +2296,7 @@ void OBSBasic::OnFirstLoad() } else { QByteArray dockState = QByteArray::fromBase64(QByteArray(dockStateStr)); - if (!restoreState(dockState)) - on_resetDocks_triggered(true); + RestoreState(dockState); } bool pre23Defaults = config_get_bool(App()->GlobalConfig(), "General", @@ -5146,6 +5145,20 @@ void OBSBasic::changeEvent(QEvent *event) if (previewEnabled) EnablePreviewDisplay(true); } + + if (!dockStateToRestore.isEmpty() && + (isVisible() || !isMaximized())) { + if (!restoreState(dockStateToRestore)) + on_resetDocks_triggered(true); + + dockStateToRestore.clear(); + } + } else if ((event->type() == QEvent::Show) && + !dockStateToRestore.isEmpty()) { + if (!restoreState(dockStateToRestore)) + on_resetDocks_triggered(true); + + dockStateToRestore.clear(); } } @@ -10930,3 +10943,13 @@ void OBSBasic::ThemeChanged() if (api) api->on_event(OBS_FRONTEND_EVENT_THEME_CHANGED); } + +void OBSBasic::RestoreState(const QByteArray &state) +{ + if (isVisible() || !isMaximized()) { + if (!restoreState(state)) + on_resetDocks_triggered(true); + } else { + dockStateToRestore = state; + } +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 470196e397bdc4..c2718ee8f98cff 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -544,6 +544,7 @@ class OBSBasic : public OBSMainWindow { QList visDlgPositions; QByteArray startingDockLayout; + QByteArray dockStateToRestore; obs_data_array_t *SaveProjectors(); void LoadSavedProjectors(obs_data_array_t *savedProjectors); @@ -1020,6 +1021,8 @@ private slots: QColor GetSelectionColor() const; + void RestoreState(const QByteArray &state); + protected: virtual void closeEvent(QCloseEvent *event) override; virtual bool nativeEvent(const QByteArray &eventType, void *message, From b98926f97f595046d0b5f56343c7b18f31a846f6 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 13 May 2023 16:32:35 +0200 Subject: [PATCH 05/65] UI,libobs,docs: Add audio track capability to the Service API --- UI/window-basic-main-outputs.cpp | 25 +++++-------------------- UI/window-basic-settings-stream.cpp | 4 +++- docs/sphinx/reference-services.rst | 17 +++++++++++++++++ libobs/obs-service.c | 11 +++++++++++ libobs/obs-service.h | 8 ++++++++ libobs/obs.h | 3 +++ plugins/rtmp-services/rtmp-common.c | 11 +++++++++++ 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index f7e62edc20575e..f4e548e8bc6cf0 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1141,8 +1141,6 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) return true; } -static inline bool ServiceSupportsVodTrack(const char *service); - static void clear_archive_encoder(obs_output_t *output, const char *expected_name) { @@ -1170,13 +1168,13 @@ void SimpleOutput::SetupVodTrack(obs_service_t *service) GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *name = obs_data_get_string(settings, "service"); - const char *id = obs_service_get_id(service); if (strcmp(id, "rtmp_custom") == 0) enable = enableForCustomServer ? enable : false; else - enable = advanced && enable && ServiceSupportsVodTrack(name); + enable = advanced && enable && + (obs_service_get_audio_track_cap(service) == + OBS_SERVICE_AUDIO_ARCHIVE_TRACK); if (enable) obs_output_set_audio_encoder(streamOutput, audioArchive, 1); @@ -1754,18 +1752,6 @@ void AdvancedOutput::Update() UpdateAudioSettings(); } -static inline bool ServiceSupportsVodTrack(const char *service) -{ - static const char *vodTrackServices[] = {"Twitch"}; - - for (const char *vodTrackService : vodTrackServices) { - if (astrcmpi(vodTrackService, service) == 0) - return true; - } - - return false; -} - inline void AdvancedOutput::SetupStreaming() { bool rescale = config_get_bool(main->Config(), "AdvOut", "Rescale"); @@ -2062,9 +2048,8 @@ inline void AdvancedOutput::SetupVodTrack(obs_service_t *service) vodTrackEnabled = enableForCustomServer ? vodTrackEnabled : false; } else { - OBSDataAutoRelease settings = obs_service_get_settings(service); - const char *service = obs_data_get_string(settings, "service"); - if (!ServiceSupportsVodTrack(service)) + if (obs_service_get_audio_track_cap(service) != + OBS_SERVICE_AUDIO_ARCHIVE_TRACK) vodTrackEnabled = false; } diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 939afafc1ff812..9d0d0bd8472b63 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -783,9 +783,11 @@ void OBSBasicSettings::on_useAuth_toggled() void OBSBasicSettings::UpdateVodTrackSetting() { + OBSService service = GetStream1Service(); bool enableForCustomServer = config_get_bool( GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); - bool enableVodTrack = ui->service->currentText() == "Twitch"; + bool enableVodTrack = obs_service_get_audio_track_cap(service) == + OBS_SERVICE_AUDIO_ARCHIVE_TRACK; bool wasEnabled = !!vodTrackCheckbox; if (enableForCustomServer && IsCustomService()) diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index d35a919ec5f565..c780e5f2af2ee3 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -202,6 +202,17 @@ Service Definition Structure .. versionadded:: 29.1 +.. member:: enum obs_service_audio_track_cap (*get_audio_track_cap)(void *data) + + Return the audio track capability of the service: + + - **OBS_SERVICE_AUDIO_SINGLE_TRACK** - Only a single audio track is used by the service + + - **OBS_SERVICE_AUDIO_ARCHIVE_TRACK** - A second audio track is accepted and + is meant to become the archive/VOD audio + + - **OBS_SERVICE_AUDIO_MULTI_TRACK** - Supports multiple audio tracks + General Service Functions ------------------------- @@ -416,6 +427,12 @@ General Service Functions .. versionadded:: 29.1 +--------------------- + +.. function:: enum obs_service_audio_track_cap obs_service_get_audio_track_cap(const obs_service_t *service) + + :return: The audio track capability of the service + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-service.c b/libobs/obs-service.c index f4d358bf6993e2..70bcf0200a1794 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -522,3 +522,14 @@ bool obs_service_can_try_to_connect(const obs_service_t *service) return true; return service->info.can_try_to_connect(service->context.data); } + +enum obs_service_audio_track_cap +obs_service_get_audio_track_cap(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_get_audio_track_cap")) + return OBS_SERVICE_AUDIO_SINGLE_TRACK; + + if (!service->info.get_audio_track_cap) + return OBS_SERVICE_AUDIO_SINGLE_TRACK; + return service->info.get_audio_track_cap(service->context.data); +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 698b1c43162a8e..4dc76a8d2e7b0e 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -45,6 +45,12 @@ enum obs_service_connect_info { OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN = 10, }; +enum obs_service_audio_track_cap { + OBS_SERVICE_AUDIO_SINGLE_TRACK = 0, + OBS_SERVICE_AUDIO_ARCHIVE_TRACK = 1, + OBS_SERVICE_AUDIO_MULTI_TRACK = 2, +}; + struct obs_service_info { /* required */ const char *id; @@ -109,6 +115,8 @@ struct obs_service_info { const char *(*get_connect_info)(void *data, uint32_t type); bool (*can_try_to_connect)(void *data); + + enum obs_service_audio_track_cap (*get_audio_track_cap)(void *data); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index 56d397bb6b9386..58240b5029893d 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2670,6 +2670,9 @@ EXPORT const char *obs_service_get_connect_info(const obs_service_t *service, EXPORT bool obs_service_can_try_to_connect(const obs_service_t *service); +EXPORT enum obs_service_audio_track_cap +obs_service_get_audio_track_cap(const obs_service_t *service); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 5151cb695163eb..12b80ce6e10568 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -1107,6 +1107,16 @@ static bool rtmp_common_can_try_to_connect(void *data) (key != NULL && key[0] != '\0'); } +static enum obs_service_audio_track_cap rtmp_common_audio_track_cap(void *data) +{ + struct rtmp_common *service = data; + + if (service->service && strcmp(service->service, "Twitch") == 0) + return OBS_SERVICE_AUDIO_ARCHIVE_TRACK; + + return OBS_SERVICE_AUDIO_SINGLE_TRACK; +} + struct obs_service_info rtmp_common_service = { .id = "rtmp_common", .get_name = rtmp_common_getname, @@ -1127,4 +1137,5 @@ struct obs_service_info rtmp_common_service = { .get_supported_video_codecs = rtmp_common_get_supported_video_codecs, .get_supported_audio_codecs = rtmp_common_get_supported_audio_codecs, .can_try_to_connect = rtmp_common_can_try_to_connect, + .get_audio_track_cap = rtmp_common_audio_track_cap, }; From e1fb9968274a23fb447095c0ed798f2452d56fa8 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 23 Mar 2023 15:19:36 +0100 Subject: [PATCH 06/65] UI: Move OBSBasic service functions in its own file --- UI/cmake/legacy.cmake | 1 + UI/cmake/ui-windows.cmake | 1 + UI/window-basic-main-service.cpp | 123 +++++++++++++++++++++++++++++++ UI/window-basic-main.cpp | 103 -------------------------- 4 files changed, 125 insertions(+), 103 deletions(-) create mode 100644 UI/window-basic-main-service.cpp diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 0650d3d3a98654..eb94eb0123d23f 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -247,6 +247,7 @@ target_sources( window-basic-main-profiles.cpp window-basic-main-scene-collections.cpp window-basic-main-screenshot.cpp + window-basic-main-service.cpp window-basic-main-transitions.cpp window-basic-preview.cpp window-basic-properties.cpp diff --git a/UI/cmake/ui-windows.cmake b/UI/cmake/ui-windows.cmake index 1c08ba5ee4fec2..a5e98f5c976cdb 100644 --- a/UI/cmake/ui-windows.cmake +++ b/UI/cmake/ui-windows.cmake @@ -20,6 +20,7 @@ target_sources( window-basic-main-profiles.cpp window-basic-main-scene-collections.cpp window-basic-main-screenshot.cpp + window-basic-main-service.cpp window-basic-main-transitions.cpp window-basic-main.cpp window-basic-main.hpp diff --git a/UI/window-basic-main-service.cpp b/UI/window-basic-main-service.cpp new file mode 100644 index 00000000000000..27e37469b6e8f6 --- /dev/null +++ b/UI/window-basic-main-service.cpp @@ -0,0 +1,123 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "window-basic-main.hpp" + +#include + +#define SERVICE_PATH "service.json" + +void OBSBasic::SaveService() +{ + if (!service) + return; + + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), + SERVICE_PATH); + if (ret <= 0) + return; + + OBSDataAutoRelease data = obs_data_create(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + + obs_data_set_string(data, "type", obs_service_get_type(service)); + obs_data_set_obj(data, "settings", settings); + + if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak")) + blog(LOG_WARNING, "Failed to save service"); +} + +bool OBSBasic::LoadService() +{ + const char *type; + + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), + SERVICE_PATH); + if (ret <= 0) + return false; + + OBSDataAutoRelease data = + obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); + + if (!data) + return false; + + obs_data_set_default_string(data, "type", "rtmp_common"); + type = obs_data_get_string(data, "type"); + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + + service = obs_service_create(type, "default_service", settings, + hotkey_data); + obs_service_release(service); + + if (!service) + return false; + + /* Enforce Opus on FTL if needed */ + if (strcmp(obs_service_get_protocol(service), "FTL") == 0) { + const char *option = config_get_string( + basicConfig, "SimpleOutput", "StreamAudioEncoder"); + if (strcmp(option, "opus") != 0) + config_set_string(basicConfig, "SimpleOutput", + "StreamAudioEncoder", "opus"); + + option = config_get_string(basicConfig, "AdvOut", + "AudioEncoder"); + if (strcmp(obs_get_encoder_codec(option), "opus") != 0) + config_set_string(basicConfig, "AdvOut", "AudioEncoder", + "ffmpeg_opus"); + } + + return true; +} + +bool OBSBasic::InitService() +{ + ProfileScope("OBSBasic::InitService"); + + if (LoadService()) + return true; + + service = obs_service_create("rtmp_common", "default_service", nullptr, + nullptr); + if (!service) + return false; + obs_service_release(service); + + return true; +} + +obs_service_t *OBSBasic::GetService() +{ + if (!service) { + service = + obs_service_create("rtmp_common", NULL, NULL, nullptr); + obs_service_release(service); + } + return service; +} + +void OBSBasic::SetService(obs_service_t *newService) +{ + if (newService) { + service = newService; + } +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 06f9ee5827e0db..806d971422f20e 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1302,92 +1302,6 @@ void OBSBasic::LoadData(obs_data_t *data, const char *file) } } -#define SERVICE_PATH "service.json" - -void OBSBasic::SaveService() -{ - if (!service) - return; - - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return; - - OBSDataAutoRelease data = obs_data_create(); - OBSDataAutoRelease settings = obs_service_get_settings(service); - - obs_data_set_string(data, "type", obs_service_get_type(service)); - obs_data_set_obj(data, "settings", settings); - - if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak")) - blog(LOG_WARNING, "Failed to save service"); -} - -bool OBSBasic::LoadService() -{ - const char *type; - - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return false; - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); - - if (!data) - return false; - - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); - - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); - - service = obs_service_create(type, "default_service", settings, - hotkey_data); - obs_service_release(service); - - if (!service) - return false; - - /* Enforce Opus on FTL if needed */ - if (strcmp(obs_service_get_protocol(service), "FTL") == 0) { - const char *option = config_get_string( - basicConfig, "SimpleOutput", "StreamAudioEncoder"); - if (strcmp(option, "opus") != 0) - config_set_string(basicConfig, "SimpleOutput", - "StreamAudioEncoder", "opus"); - - option = config_get_string(basicConfig, "AdvOut", - "AudioEncoder"); - if (strcmp(obs_get_encoder_codec(option), "opus") != 0) - config_set_string(basicConfig, "AdvOut", "AudioEncoder", - "ffmpeg_opus"); - } - - return true; -} - -bool OBSBasic::InitService() -{ - ProfileScope("OBSBasic::InitService"); - - if (LoadService()) - return true; - - service = obs_service_create("rtmp_common", "default_service", nullptr, - nullptr); - if (!service) - return false; - obs_service_release(service); - - return true; -} - static const double scaled_vals[] = {1.0, 1.25, (1.0 / 0.75), 1.5, (1.0 / 0.6), 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 0.0}; @@ -4525,23 +4439,6 @@ void OBSBasic::RenderMain(void *data, uint32_t, uint32_t) /* Main class functions */ -obs_service_t *OBSBasic::GetService() -{ - if (!service) { - service = - obs_service_create("rtmp_common", NULL, NULL, nullptr); - obs_service_release(service); - } - return service; -} - -void OBSBasic::SetService(obs_service_t *newService) -{ - if (newService) { - service = newService; - } -} - int OBSBasic::GetTransitionDuration() { return ui->transitionDuration->value(); From 6a12a6ae80383122b0b837c4464ee2a1c2a82cca Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Apr 2023 11:16:00 +0200 Subject: [PATCH 07/65] UI: Remove service integration UI from settings --- UI/data/locale/en-US.ini | 5 - UI/forms/OBSBasicSettings.ui | 649 +++++++++------------------- UI/window-basic-settings-stream.cpp | 295 ------------- UI/window-basic-settings.cpp | 10 - UI/window-basic-settings.hpp | 12 - 5 files changed, 208 insertions(+), 763 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 770263abb6b065..ae2b5b5d7f7e3b 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -916,11 +916,6 @@ Basic.Settings.Stream.Custom.Password="Password" Basic.Settings.Stream.Custom.Username.ToolTip="RIST: enter the srp_username.\nRTMP: enter the username.\nSRT: not used." Basic.Settings.Stream.Custom.Password.ToolTip="RIST: enter the srp_password.\nRTMP: enter the password.\nSRT: enter the encryption passphrase." Basic.Settings.Stream.BandwidthTestMode="Enable Bandwidth Test Mode" -Basic.Settings.Stream.TTVAddon="Twitch Chat Add-Ons" -Basic.Settings.Stream.TTVAddon.None="None" -Basic.Settings.Stream.TTVAddon.BTTV="BetterTTV" -Basic.Settings.Stream.TTVAddon.FFZ="FrankerFaceZ" -Basic.Settings.Stream.TTVAddon.Both="BetterTTV and FrankerFaceZ" Basic.Settings.Stream.MissingSettingAlert="Missing Stream Setup" Basic.Settings.Stream.StreamSettingsWarning="Open Settings" Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOpen settings to enter the URL and Stream Key in the 'stream' tab." diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index bcd7c365701a2e..8ecc2eb3387a42 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -989,449 +989,238 @@ - - - - 0 - 0 - + + + QFormLayout::AllNonFixedFieldsGrow - - 0 + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - Qt::Horizontal + + + + Basic.AutoConfig.StreamPage.Server + + + + + + + + 0 + 0 + + + + 1 + + + + + 0 - - QSizePolicy::Fixed + + 0 - - - 170 - 19 - + + 0 + + + 0 - - - - - - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - Qt::Horizontal - - - - 40 - 10 - - - + - - - - - Qt::Horizontal + + + + + 0 - - QSizePolicy::Fixed + + 0 - - - 170 - 19 - + + 0 + + + 0 - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKey - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + - - - - - - - QFormLayout::AllNonFixedFieldsGrow + + + + + + + Basic.AutoConfig.StreamPage.StreamKey - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - - 0 - 0 - - - - 1 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 + + key + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + - - 0 + + - - 0 + + QLineEdit::Password - - 0 + + + + + + Show - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - -4 - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Qt::Horizontal - - - - 170 - 8 - - - - - - - - 8 - - - 7 - - - 7 - - - - - font-weight: bold - - - Auth.LoadingChannel.Title - - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Basic.Settings.Stream.BandwidthTestMode - - - - - - - Basic.Settings.Stream.Custom.UseAuthentication - - - - - - - Basic.Settings.Stream.Custom.Username - - - authUsername - - - - - - - - - - Basic.Settings.Stream.Custom.Password - - - authPw - - - - - - - - 0 + + + + + + - - 0 + + -4 - - 0 + + Basic.AutoConfig.StreamPage.GetStreamKey - - 0 + + + + + + + + + Qt::Horizontal + + + + 170 + 8 + + + + + + + + Basic.Settings.Stream.BandwidthTestMode + + + + + + + Basic.Settings.Stream.Custom.UseAuthentication + + + + + + + Basic.Settings.Stream.Custom.Username + + + authUsername + + + + + + + + + + Basic.Settings.Stream.Custom.Password + + + authPw + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QLineEdit::Password - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - - - - Basic.Settings.Stream.TTVAddon - - - twitchAddonDropdown - - - - - - - Basic.Settings.Stream.IgnoreRecommended - - - - - - - - - - Qt::RichText - - - true - - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - - - PointingHandCursor - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - + + + + + + Show + + + + + + + + + + Basic.Settings.Stream.IgnoreRecommended + + + + + + + + + + Qt::RichText + + + true + + + + @@ -7807,18 +7596,13 @@ multiviewLayout service moreInfoButton - connectAccount - useStreamKey server customServer key show getStreamKeyButton - connectAccount2 - disconnectAccount bandwidthTestEnable useAuth - twitchAddonDropdown authUsername authPw authPwShow @@ -7940,7 +7724,6 @@ browserHWAccel hotkeyFocusType ignoreRecommended - useStreamKeyAdv hotkeyFilterSearch hotkeyFilterInput hotkeyFilterReset @@ -8254,22 +8037,6 @@ - - connectAccount2 - clicked() - connectAccount - click() - - - 484 - 142 - - - 454 - 87 - - - advOutSplitFile toggled(bool) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 9d0d0bd8472b63..a4766c802c616f 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -8,34 +8,11 @@ #include "qt-wrappers.hpp" #include "url-push-button.hpp" -#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; - enum class ListOpt : int { ShowAll = 1, Custom, }; -enum class Section : int { - Connect, - StreamKey, -}; - inline bool OBSBasicSettings::IsCustomService() const { return ui->service->currentData().toInt() == (int)ListOpt::Custom; @@ -43,41 +20,20 @@ inline bool OBSBasicSettings::IsCustomService() const void OBSBasicSettings::InitStreamPage() { - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - int vertSpacing = ui->topStreamLayout->verticalSpacing(); QMargins m = ui->topStreamLayout->contentsMargins(); m.setBottom(vertSpacing / 2); ui->topStreamLayout->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); LoadServices(false); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.None")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.BTTV")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.FFZ")); - ui->twitchAddonDropdown->addItem( - QTStr("Basic.Settings.Stream.TTVAddon.Both")); - connect(ui->ignoreRecommended, SIGNAL(clicked(bool)), this, SLOT(DisplayEnforceWarning(bool))); connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this, @@ -150,9 +106,6 @@ void OBSBasicSettings::LoadStream1Settings() bool bw_test = obs_data_get_bool(settings, "bwtest"); ui->bandwidthTestEnable->setChecked(bw_test); - - idx = config_get_int(main->Config(), "Twitch", "AddonChoice"); - ui->twitchAddonDropdown->setCurrentIndex(idx); } UpdateServerList(); @@ -220,25 +173,6 @@ void OBSBasicSettings::SaveStream1Settings() } } - if (!!auth && strcmp(auth->service(), "Twitch") == 0) { - bool choiceExists = config_has_user_value( - main->Config(), "Twitch", "AddonChoice"); - int currentChoice = - config_get_int(main->Config(), "Twitch", "AddonChoice"); - int newChoice = ui->twitchAddonDropdown->currentIndex(); - - config_set_int(main->Config(), "Twitch", "AddonChoice", - newChoice); - - if (choiceExists && currentChoice != newChoice) - forceAuthReload = true; - - obs_data_set_bool(settings, "bwtest", - ui->bandwidthTestEnable->isChecked()); - } else { - obs_data_set_bool(settings, "bwtest", false); - } - obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); OBSServiceAutoRelease newService = obs_service_create( @@ -249,13 +183,6 @@ void OBSBasicSettings::SaveStream1Settings() main->SetService(newService); main->SaveService(); - main->auth = auth; - if (!!main->auth) { - main->auth->LoadUI(); - main->SetBroadcastFlowEnabled(main->auth->broadcastFlow()); - } else { - main->SetBroadcastFlowEnabled(false); - } SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); } @@ -377,71 +304,6 @@ void OBSBasicSettings::LoadServices(bool showAll) ui->service->blockSignals(false); } -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); -} - -static void reset_service_ui_fields(Ui::OBSBasicSettings *ui, - std::string &service, bool loading) -{ - 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->streamStackWidget->setCurrentIndex((int)Section::StreamKey); - } else if (cef) { - QString key = ui->key->text(); - bool can_auth = is_auth_service(service); - int page = can_auth && (!loading || key.isEmpty()) - ? (int)Section::Connect - : (int)Section::StreamKey; - - ui->streamStackWidget->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->streamStackWidget->setCurrentIndex((int)Section::StreamKey); - } - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); - ui->disconnectAccount->setVisible(false); -} - -#ifdef YOUTUBE_ENABLED -static void get_yt_ch_title(Ui::OBSBasicSettings *ui) -{ - const char *name = config_get_string(OBSBasic::Get()->Config(), - "YouTube", "ChannelName"); - if (name) { - ui->connectedAccountText->setText(name); - } else { - // if we still not changed the service page - if (IsYouTubeService(QT_TO_UTF8(ui->service->currentText()))) { - ui->connectedAccountText->setText( - QTStr("Auth.LoadingChannel.Error")); - } - } -} -#endif - -void OBSBasicSettings::UseStreamKeyAdvClicked() -{ - ui->streamKeyWidget->setVisible(true); -} - void OBSBasicSettings::on_service_currentIndexChanged(int idx) { if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) { @@ -485,14 +347,7 @@ void OBSBasicSettings::ServiceChanged() std::string service = QT_TO_UTF8(ui->service->currentText()); bool custom = IsCustomService(); - ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - - if (lastService != service.c_str()) { - reset_service_ui_fields(ui.get(), service, loading); - } ui->useAuth->setVisible(custom); ui->authUsernameLabel->setVisible(custom); @@ -511,25 +366,6 @@ void OBSBasicSettings::ServiceChanged() } else { ui->serverStackedWidget->setCurrentIndex(0); } - - auth.reset(); - - if (!main->auth) { - return; - } - - 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 = main->auth; - OnAuthConnected(); - } } QString OBSBasicSettings::FindProtocol() @@ -650,124 +486,6 @@ OBSService OBSBasicSettings::SpawnTempService() return newService.Get(); } -void OBSBasicSettings::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); - - if (strcmp(a->service(), "Twitch") == 0) { - ui->bandwidthTestEnable->setVisible(true); - ui->twitchAddonLabel->setVisible(true); - ui->twitchAddonDropdown->setVisible(true); - } else { - ui->bandwidthTestEnable->setChecked(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")); - - get_yt_ch_title(ui.get()); - } -#endif - } - - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); -} - -void OBSBasicSettings::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(); - } - - if (!loading) { - stream1Changed = true; - EnableApplyButton(true); - } -} - -void OBSBasicSettings::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 OBSBasicSettings::on_disconnectAccount_clicked() -{ - QMessageBox::StandardButton button; - - button = OBSMessageBox::question(this, QTStr(DISCONNECT_COMFIRM_TITLE), - QTStr(DISCONNECT_COMFIRM_TEXT)); - - if (button == QMessageBox::No) { - return; - } - - main->auth.reset(); - auth.reset(); - main->SetBroadcastFlowEnabled(false); - - std::string service = QT_TO_UTF8(ui->service->currentText()); - -#ifdef BROWSER_AVAILABLE - OAuth::DeleteCookies(service); -#endif - - ui->bandwidthTestEnable->setChecked(false); - - reset_service_ui_fields(ui.get(), service, loading); - - ui->bandwidthTestEnable->setVisible(false); - ui->twitchAddonDropdown->setVisible(false); - ui->twitchAddonLabel->setVisible(false); - ui->key->setText(""); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); -} - -void OBSBasicSettings::on_useStreamKey_clicked() -{ - ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); -} - void OBSBasicSettings::on_useAuth_toggled() { if (!IsCustomService()) @@ -933,19 +651,6 @@ void OBSBasicSettings::UpdateServiceRecommendations() } #undef ENFORCE_TEXT -#ifdef YOUTUBE_ENABLED - if (IsYouTubeService(QT_TO_UTF8(ui->service->currentText()))) { - if (!text.isEmpty()) - text += "

"; - - text += "" - "YouTube Terms of Service
" - "" - "Google Privacy Policy
" - "" - "Google Third-Party Permissions"; - } -#endif ui->enforceSettingsLabel->setText(text); } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index f29e92e5060b31..6298ef629e0287 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -477,7 +477,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->twitchAddonDropdown, COMBO_CHANGED, STREAM1_CHANGED); HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); @@ -1009,9 +1008,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->advOutRecRescale->lineEdit()->setValidator(validator); ui->advOutFFRescale->lineEdit()->setValidator(validator); - connect(ui->useStreamKeyAdv, SIGNAL(clicked()), this, - SLOT(UseStreamKeyAdvClicked())); - connect(ui->simpleOutStrAEncoder, SIGNAL(currentIndexChanged(int)), this, SLOT(SimpleStreamAudioEncoderChanged())); connect(ui->advOutAEncoder, SIGNAL(currentIndexChanged(int)), this, @@ -4539,12 +4535,6 @@ bool OBSBasicSettings::AskIfCanCloseSettings() if (!Changed() || QueryChanges()) canCloseSettings = true; - if (forceAuthReload) { - main->auth->Save(); - main->auth->Load(); - forceAuthReload = false; - } - if (forceUpdateCheck) { main->CheckForUpdates(false); forceUpdateCheck = false; diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 33b5b107cc576a..d05dc0716c2249 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -28,8 +28,6 @@ #include -#include "auth-base.hpp" - class OBSBasic; class QAbstractButton; class QRadioButton; @@ -107,8 +105,6 @@ class OBSBasicSettings : public QDialog { std::unique_ptr ui; - std::shared_ptr auth; - bool generalChanged = false; bool stream1Changed = false; bool outputsChanged = false; @@ -119,7 +115,6 @@ class OBSBasicSettings : public QDialog { bool advancedChanged = false; int pageIndex = 0; bool loading = true; - bool forceAuthReload = false; bool forceUpdateCheck = false; std::string savedTheme; int sampleRateIndex = 0; @@ -259,8 +254,6 @@ class OBSBasicSettings : public QDialog { void InitStreamPage(); inline bool IsCustomService() const; void LoadServices(bool showAll); - void OnOAuthStreamKeyConnected(); - void OnAuthConnected(); QString lastService; QString protocol; QString lastCustomServer; @@ -282,9 +275,6 @@ private slots: void DisplayEnforceWarning(bool checked); void on_show_clicked(); void on_authPwShow_clicked(); - void on_connectAccount_clicked(); - void on_disconnectAccount_clicked(); - void on_useStreamKey_clicked(); void on_useAuth_toggled(); void on_hotkeyFilterReset_clicked(); @@ -471,8 +461,6 @@ private slots: void SetAccessibilityIcon(const QIcon &icon); void SetAdvancedIcon(const QIcon &icon); - void UseStreamKeyAdvClicked(); - void SimpleStreamAudioEncoderChanged(); void AdvAudioEncodersChanged(); From ef283c33abada071692dac741d8d51a0e798ec3a Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Apr 2023 11:16:16 +0200 Subject: [PATCH 08/65] UI: Remove service integration UI from auto-wizard --- UI/data/locale/en-US.ini | 7 - UI/forms/AutoConfigStreamPage.ui | 583 ++++++++++----------------- UI/window-basic-auto-config-test.cpp | 8 +- UI/window-basic-auto-config.cpp | 269 +----------- UI/window-basic-auto-config.hpp | 23 -- 5 files changed, 213 insertions(+), 677 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index ae2b5b5d7f7e3b..6a4dbdb3004303 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -193,14 +193,8 @@ Basic.AutoConfig.VideoPage.FPS.PreferHighRes="Either 60 or 30, but prefer high r Basic.AutoConfig.VideoPage.CanvasExplanation="Note: The canvas (base) resolution is not necessarily the same as the resolution you will stream or record with. Your actual stream/recording resolution may be scaled down from the canvas resolution to reduce resource usage or bitrate requirements." Basic.AutoConfig.StreamPage="Stream Information" Basic.AutoConfig.StreamPage.SubTitle="Please enter your stream information" -Basic.AutoConfig.StreamPage.ConnectAccount="Connect Account (recommended)" -Basic.AutoConfig.StreamPage.DisconnectAccount="Disconnect Account" -Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title="Disconnect Account?" -Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text="This change will apply immediately. Are you sure you want to disconnect your account?" Basic.AutoConfig.StreamPage.GetStreamKey="Get Stream Key" Basic.AutoConfig.StreamPage.MoreInfo="More Info" -Basic.AutoConfig.StreamPage.UseStreamKey="Use Stream Key" -Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced="Use Stream Key (advanced)" Basic.AutoConfig.StreamPage.Service="Service" Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..." Basic.AutoConfig.StreamPage.Service.Custom="Custom..." @@ -208,7 +202,6 @@ Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Stream Key" Basic.AutoConfig.StreamPage.StreamKey.ToolTip="RIST: enter the encryption passphrase.\nRTMP: enter the key provided by the service.\nSRT: enter the streamid if the service uses one." Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" -Basic.AutoConfig.StreamPage.ConnectedAccount="Connected account" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip="Hardware Encoding eliminates most CPU usage, but may require more bitrate to obtain the same level of quality." diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index a6b858082c2aaa..5103a5cb0cbc6f 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -118,386 +118,236 @@ - - - 1 + + + QFormLayout::ExpandingFieldsGrow - - - - QFormLayout::ExpandingFieldsGrow + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Basic.AutoConfig.StreamPage.Server - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + 0 - - - - Qt::Horizontal + + + + 0 - - QSizePolicy::Fixed + + 0 - - - 87 - 17 - + + 0 + + + 0 - - - - - - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + - - - - - Qt::Horizontal + + + + + 0 - - QSizePolicy::Fixed + + 0 - - - 87 - 17 - + + 0 + + + 0 - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKey - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + - - - - - - - QFormLayout::ExpandingFieldsGrow + + + + + + + Basic.AutoConfig.StreamPage.StreamKey - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - + + key + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + QLineEdit::Password + - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - + + + + + Show + - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 + + + + + Basic.AutoConfig.StreamPage.GetStreamKey - - 0 + + + + + + + + + Basic.Settings.Output.VideoBitrate + + + bitrate + + + + + + + + + + 500 + + + 51000 + + + 2500 + + + + + + + Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip + + + Basic.AutoConfig.StreamPage.PreferHardwareEncoding + + + true + + + + + + + Basic.AutoConfig.StreamPage.PerformBandwidthTest + + + true + + + + + + + BandwidthTest.Region + + + + + + BandwidthTest.Region.Asia - - 0 + + + + + + BandwidthTest.Region.US - - 0 + + + + + + BandwidthTest.Region.EU - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Basic.Settings.Output.VideoBitrate - - - bitrate - - - - - - - - - - 500 - - - 51000 - - - 2500 - - - - - - - Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip - - - Basic.AutoConfig.StreamPage.PreferHardwareEncoding - - - true - - - - - - - Basic.AutoConfig.StreamPage.PerformBandwidthTest - - - true - - - - - - - BandwidthTest.Region - - - - - - BandwidthTest.Region.Asia - - - - - - - BandwidthTest.Region.US - - - - - - - BandwidthTest.Region.EU - - - - - - - BandwidthTest.Region.Other - - - - - - - - - - PointingHandCursor - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Vertical - - - - 6 - 6 - - - - - - - - 7 - - - 7 - - - - - - true - - - - Auth.LoadingChannel.Title - - - - - - - Basic.AutoConfig.StreamPage.DisconnectAccount - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectedAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKeyAdvanced - - - - - - + + + + + + BandwidthTest.Region.Other + + + + + + + + + + Qt::Vertical + + + + 6 + 6 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + @@ -509,22 +359,5 @@ - - - connectAccount2 - clicked() - connectAccount - click() - - - 382 - 279 - - - 114 - 82 - - - - + diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 44daae4566973e..0e556675915483 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -221,7 +221,7 @@ void AutoConfigTestPage::TestBandwidthThread() OBSDataAutoRelease output_settings = obs_data_create(); std::string key = wiz->key; - if (wiz->service == AutoConfig::Service::Twitch) { + if (wiz->serviceName == "Twitch") { string_depad_key(key); key += "?bandwidthtest"; } else if (wiz->serviceName == "Restream.io" || @@ -265,15 +265,11 @@ void AutoConfigTestPage::TestBandwidthThread() wiz->serviceName == "Nimo TV") { servers.resize(1); - } else if (wiz->service == AutoConfig::Service::Twitch && - wiz->twitchAuto) { + } else if (wiz->serviceName == "Twitch" && wiz->twitchAuto) { /* 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); } /* -----------------------------------*/ diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 5a183f04956a6f..8a9ecd35a53d34 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -13,21 +13,7 @@ #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()) @@ -258,11 +244,6 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) ui->setupUi(this); ui->bitrateLabel->setVisible(false); ui->bitrate->setVisible(false); - ui->connectAccount2->setVisible(false); - ui->disconnectAccount->setVisible(false); - - ui->connectedAccountLabel->setVisible(false); - ui->connectedAccountText->setVisible(false); int vertSpacing = ui->topLayout->verticalSpacing(); @@ -270,10 +251,6 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) 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); @@ -302,9 +279,6 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) connect(ui->service, SIGNAL(currentIndexChanged(int)), this, SLOT(UpdateMoreInfoLink())); - connect(ui->useStreamKeyAdv, &QPushButton::clicked, this, - [&]() { ui->streamKeyWidget->setVisible(true); }); - connect(ui->key, SIGNAL(textChanged(const QString &)), this, SLOT(UpdateCompleted())); connect(ui->regionUS, SIGNAL(toggled(bool)), this, @@ -358,16 +332,6 @@ bool AutoConfigStreamPage::validatePage() } 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(); @@ -394,22 +358,7 @@ bool AutoConfigStreamPage::validatePage() 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 - wiz->service = AutoConfig::Service::Other; - } else { - wiz->service = AutoConfig::Service::Other; - } - - if (wiz->service != AutoConfig::Service::Twitch && - wiz->service != AutoConfig::Service::YouTube && - wiz->bandwidthTest) { + if (wiz->serviceName != "Twitch" && wiz->bandwidthTest) { QMessageBox::StandardButton button; #define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) button = OBSMessageBox::question(this, WARNING_TEXT("Title"), @@ -434,184 +383,6 @@ void AutoConfigStreamPage::on_show_clicked() } } -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(); -} - -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() == @@ -624,8 +395,6 @@ void AutoConfigStreamPage::ServiceChanged() bool testBandwidth = ui->doBandwidthTest->isChecked(); bool custom = IsCustomService(); - reset_service_ui_fields(service); - /* Test three closest servers if "Auto" is available for Twitch */ if (service == "Twitch" && wiz->twitchAuto) regionBased = false; @@ -657,25 +426,6 @@ void AutoConfigStreamPage::ServiceChanged() 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(); } @@ -834,8 +584,7 @@ void AutoConfigStreamPage::UpdateServerList() void AutoConfigStreamPage::UpdateCompleted() { - if (ui->stackedWidget->currentIndex() == (int)Section::Connect || - (ui->key->text().isEmpty() && !auth)) { + if (ui->key->text().isEmpty()) { ready = false; } else { bool custom = IsCustomService(); @@ -1023,7 +772,7 @@ bool AutoConfig::CanTestServer(const char *server) if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) return true; - if (service == Service::Twitch) { + if (serviceName == "Twitch") { if (astrcmp_n(server, "US West:", 8) == 0 || astrcmp_n(server, "US East:", 8) == 0 || astrcmp_n(server, "US Central:", 11) == 0) { @@ -1086,12 +835,7 @@ void AutoConfig::SaveStreamSettings() if (!customServer) obs_data_set_string(settings, "service", serviceName.c_str()); obs_data_set_string(settings, "server", server.c_str()); -#ifdef YOUTUBE_ENABLED - if (!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); @@ -1101,13 +845,6 @@ void AutoConfig::SaveStreamSettings() 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 */ diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index 5d966c7956e272..955be07faff49e 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -19,7 +19,6 @@ class Ui_AutoConfigStreamPage; class Ui_AutoConfigTestPage; class AutoConfigStreamPage; -class Auth; class AutoConfig : public QWizard { Q_OBJECT @@ -36,12 +35,6 @@ class AutoConfig : public QWizard { VirtualCam, }; - enum class Service { - Twitch, - YouTube, - Other, - }; - enum class Encoder { x264, NVENC, @@ -68,7 +61,6 @@ class AutoConfig : public QWizard { AutoConfigStreamPage *streamPage = nullptr; - Service service = Service::Other; Quality recordingQuality = Quality::Stream; Encoder recordingEncoder = Encoder::Stream; Encoder streamingEncoder = Encoder::x264; @@ -165,13 +157,6 @@ class AutoConfigStreamPage : public QWizardPage { friend class AutoConfig; - enum class Section : int { - Connect, - StreamKey, - }; - - std::shared_ptr auth; - std::unique_ptr ui; QString lastService; bool ready = false; @@ -187,21 +172,13 @@ class AutoConfigStreamPage : public QWizardPage { 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 ServiceChanged(); void UpdateKeyLink(); void UpdateMoreInfoLink(); void UpdateServerList(); void UpdateCompleted(); - - void reset_service_ui_fields(std::string &service); }; class AutoConfigTestPage : public QWizardPage { From 74cc3f3d468c3cd2b7adf8f4f3de36575c99d7af Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 3 May 2023 11:47:45 +0200 Subject: [PATCH 09/65] libobs,docs: Add feature flags in Services API --- docs/sphinx/reference-services.rst | 20 ++++++++++++++++++++ libobs/obs-service.c | 13 +++++++++++++ libobs/obs-service.h | 8 ++++++++ libobs/obs.h | 3 +++ 4 files changed, 44 insertions(+) diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index c780e5f2af2ee3..8b6dba0fcac846 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -213,6 +213,19 @@ Service Definition Structure - **OBS_SERVICE_AUDIO_MULTI_TRACK** - Supports multiple audio tracks +.. member:: uint32_t obs_service_info.flags + + Service feature flags (Optional). + + A bitwise OR combination of one or more of the following values: + + - **OBS_SERVICE_DEPRECATED** - Service is deprecrated. + + - **OBS_SERVICE_INTERNAL** - Service is not user-facing in a UI context. + + - **OBS_SERVICE_UNCOMMON** - Service can be hidden behind an option in a UI + context. + General Service Functions ------------------------- @@ -433,6 +446,13 @@ General Service Functions :return: The audio track capability of the service +--------------------- + +.. function:: uint32_t obs_get_service_flags(const char *id) + uint32_t obs_service_get_flags(const obs_service_t *service) + + :return: The service feature flags + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-service.c b/libobs/obs-service.c index 70bcf0200a1794..2ab6a501345308 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -533,3 +533,16 @@ obs_service_get_audio_track_cap(const obs_service_t *service) return OBS_SERVICE_AUDIO_SINGLE_TRACK; return service->info.get_audio_track_cap(service->context.data); } + +uint32_t obs_get_service_flags(const char *id) +{ + const struct obs_service_info *info = find_service(id); + return info ? info->flags : 0; +} + +uint32_t obs_service_get_flags(const obs_service_t *service) +{ + return obs_service_valid(service, "obs_service_get_flags") + ? service->info.flags + : 0; +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 4dc76a8d2e7b0e..4f25c951f08242 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -28,6 +28,12 @@ extern "C" { #endif +enum obs_service_info_flag { + OBS_SERVICE_DEPRECATED = (1 << 0), + OBS_SERVICE_INTERNAL = (1 << 1), + OBS_SERVICE_UNCOMMON = (1 << 2), +}; + struct obs_service_resolution { int cx; int cy; @@ -117,6 +123,8 @@ struct obs_service_info { bool (*can_try_to_connect)(void *data); enum obs_service_audio_track_cap (*get_audio_track_cap)(void *data); + + uint32_t flags; }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index 58240b5029893d..f8e759c2078976 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2673,6 +2673,9 @@ EXPORT bool obs_service_can_try_to_connect(const obs_service_t *service); EXPORT enum obs_service_audio_track_cap obs_service_get_audio_track_cap(const obs_service_t *service); +EXPORT uint32_t obs_get_service_flags(const char *id); +EXPORT uint32_t obs_service_get_flags(const obs_service_t *service); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, From 1370f9acbc6dc1a5404af51b11cb98924d822d31 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 3 May 2023 13:09:13 +0200 Subject: [PATCH 10/65] libobs,docs,rtmp-services: Add supported protocols in Services API --- docs/sphinx/reference-services.rst | 11 +++++++++++ libobs/obs-module.c | 1 + libobs/obs-service.c | 6 ++++++ libobs/obs-service.h | 2 ++ libobs/obs.h | 2 ++ plugins/rtmp-services/rtmp-common.c | 1 + plugins/rtmp-services/rtmp-custom.c | 1 + 7 files changed, 24 insertions(+) diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index 8b6dba0fcac846..72f1dcddda02d6 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -226,6 +226,11 @@ Service Definition Structure - **OBS_SERVICE_UNCOMMON** - Service can be hidden behind an option in a UI context. +.. member:: const char *obs_service_info.supported_protocols + + This variable specifies which protocol are supported by the service, + separated by semicolon. + General Service Functions ------------------------- @@ -453,6 +458,12 @@ General Service Functions :return: The service feature flags +--------------------- + +.. function:: const char *obs_get_service_supported_protocols(const char *id) + + :return: Supported protocol of the service, separated by semicolon + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-module.c b/libobs/obs-module.c index ba1ffa40c1371f..e26ee1b0f6bc27 100644 --- a/libobs/obs-module.c +++ b/libobs/obs-module.c @@ -929,6 +929,7 @@ void obs_register_service_s(const struct obs_service_info *info, size_t size) CHECK_REQUIRED_VAL_(info, create, obs_register_service); CHECK_REQUIRED_VAL_(info, destroy, obs_register_service); CHECK_REQUIRED_VAL_(info, get_protocol, obs_register_service); + CHECK_REQUIRED_VAL_(info, supported_protocols, obs_register_service); #undef CHECK_REQUIRED_VAL_ REGISTER_OBS_DEF(size, obs_service_info, obs->service_types, info); diff --git a/libobs/obs-service.c b/libobs/obs-service.c index 2ab6a501345308..d59d0ef3204b06 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -546,3 +546,9 @@ uint32_t obs_service_get_flags(const obs_service_t *service) ? service->info.flags : 0; } + +const char *obs_get_service_supported_protocols(const char *id) +{ + const struct obs_service_info *info = find_service(id); + return info ? info->supported_protocols : NULL; +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 4f25c951f08242..c33d7508d7536c 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -125,6 +125,8 @@ struct obs_service_info { enum obs_service_audio_track_cap (*get_audio_track_cap)(void *data); uint32_t flags; + + const char *supported_protocols; }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index f8e759c2078976..aa9a54a5599602 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2676,6 +2676,8 @@ obs_service_get_audio_track_cap(const obs_service_t *service); EXPORT uint32_t obs_get_service_flags(const char *id); EXPORT uint32_t obs_service_get_flags(const obs_service_t *service); +EXPORT const char *obs_get_service_supported_protocols(const char *id); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 12b80ce6e10568..01adf3866f524d 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -1138,4 +1138,5 @@ struct obs_service_info rtmp_common_service = { .get_supported_audio_codecs = rtmp_common_get_supported_audio_codecs, .can_try_to_connect = rtmp_common_can_try_to_connect, .get_audio_track_cap = rtmp_common_audio_track_cap, + .supported_protocols = "RTMP;RTMPS;HLS;FTL;SRT;RIST", }; diff --git a/plugins/rtmp-services/rtmp-custom.c b/plugins/rtmp-services/rtmp-custom.c index c44d881a265201..90bcf6550870d9 100644 --- a/plugins/rtmp-services/rtmp-custom.c +++ b/plugins/rtmp-services/rtmp-custom.c @@ -201,4 +201,5 @@ struct obs_service_info rtmp_custom_service = { .get_password = rtmp_custom_password, .apply_encoder_settings = rtmp_custom_apply_settings, .can_try_to_connect = rtmp_custom_can_try_to_connect, + .supported_protocols = "RTMP;RTMPS;FTL;SRT;RIST", }; From b50b325f1879573f163136cc0e161d9695459df1 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 3 May 2023 13:17:26 +0200 Subject: [PATCH 11/65] libobs: Add properties2 and default2 to Services API Those were documented but not implemented. --- libobs/obs-service.c | 24 +++++++++++++++++++----- libobs/obs-service.h | 3 +++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/libobs/obs-service.c b/libobs/obs-service.c index d59d0ef3204b06..c98dd9b8114735 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -126,8 +126,11 @@ const char *obs_service_get_name(const obs_service_t *service) static inline obs_data_t *get_defaults(const struct obs_service_info *info) { obs_data_t *settings = obs_data_create(); - if (info->get_defaults) + if (info->get_defaults2) { + info->get_defaults2(info->type_data, settings); + } else if (info->get_defaults) { info->get_defaults(settings); + } return settings; } @@ -140,11 +143,17 @@ obs_data_t *obs_service_defaults(const char *id) obs_properties_t *obs_get_service_properties(const char *id) { const struct obs_service_info *info = find_service(id); - if (info && info->get_properties) { + if (info && (info->get_properties || info->get_properties2)) { obs_data_t *defaults = get_defaults(info); obs_properties_t *properties; - properties = info->get_properties(NULL); + if (info->get_properties2) { + properties = + info->get_properties2(NULL, info->type_data); + } else { + properties = info->get_properties(NULL); + } + obs_properties_apply_settings(properties, defaults); obs_data_release(defaults); return properties; @@ -157,8 +166,13 @@ obs_properties_t *obs_service_properties(const obs_service_t *service) if (!obs_service_valid(service, "obs_service_properties")) return NULL; - if (service->info.get_properties) { - obs_properties_t *props; + obs_properties_t *props; + if (service->info.get_properties2) { + props = service->info.get_properties2(service->context.settings, + service->info.type_data); + obs_properties_apply_settings(props, service->context.settings); + return props; + } else if (service->info.get_properties) { props = service->info.get_properties(service->context.data); obs_properties_apply_settings(props, service->context.settings); return props; diff --git a/libobs/obs-service.h b/libobs/obs-service.h index c33d7508d7536c..383565bebcfaba 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -127,6 +127,9 @@ struct obs_service_info { uint32_t flags; const char *supported_protocols; + + void (*get_defaults2)(void *type_data, obs_data_t *settings); + obs_properties_t *(*get_properties2)(void *data, void *type_data); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, From ea9e432b51e137b736f9718d85b546924c6d0135 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 11:05:59 +0200 Subject: [PATCH 12/65] UI: Use PropertiesView for service settings --- UI/data/locale/en-US.ini | 8 +- UI/forms/OBSBasicSettings.ui | 357 +++----------- UI/window-basic-settings-stream.cpp | 709 +++++++++------------------- UI/window-basic-settings.cpp | 15 +- UI/window-basic-settings.hpp | 31 +- 5 files changed, 330 insertions(+), 790 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 6a4dbdb3004303..b29153b639870c 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -903,6 +903,10 @@ Basic.Settings.General.ChannelDescription.beta="Potentially unstable pre-release # basic mode 'stream' settings Basic.Settings.Stream="Stream" +Basic.Settings.Stream.DeprecatedType="%1 (Deprecated)" +Basic.Settings.Stream.ServiceSettings="Service Settings" +Basic.Settings.Stream.NoDefaultProtocol.Title="No Default Protocol" +Basic.Settings.Stream.NoDefaultProtocol.Text="The service \"%1\" does not have a default protocol.\n\nThe service change will be reverted." Basic.Settings.Stream.Custom.UseAuthentication="Use authentication" Basic.Settings.Stream.Custom.Username="Username" Basic.Settings.Stream.Custom.Password="Password" @@ -914,8 +918,8 @@ Basic.Settings.Stream.StreamSettingsWarning="Open Settings" Basic.Settings.Stream.MissingUrlAndApiKey="URL and Stream Key are missing.\n\nOpen settings to enter the URL and Stream Key in the 'stream' tab." Basic.Settings.Stream.MissingUrl="Stream URL is missing.\n\nOpen settings to enter the URL in the 'Stream' tab." Basic.Settings.Stream.MissingStreamKey="Stream key is missing.\n\nOpen settings to enter the stream key in the 'Stream' tab." -Basic.Settings.Stream.IgnoreRecommended="Ignore streaming service setting recommendations" -Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Recommended Settings" +Basic.Settings.Stream.IgnoreRecommended="Ignore service setting recommendations and maximums" +Basic.Settings.Stream.IgnoreRecommended.Warn.Title="Override Service Settings" Basic.Settings.Stream.IgnoreRecommended.Warn.Text="Warning: Ignoring the service's limitations may result in degraded stream quality or prevent you from streaming.\n\nContinue?" Basic.Settings.Stream.Recommended.MaxVideoBitrate="Maximum Video Bitrate: %1 kbps" Basic.Settings.Stream.Recommended.MaxAudioBitrate="Maximum Audio Bitrate: %1 kbps" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 8ecc2eb3387a42..b50d2d013a4c4a 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -907,7 +907,7 @@ - + 0 0 @@ -928,7 +928,7 @@ 0 - + @@ -942,43 +942,80 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - service + + + + + + 20 - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 20 + + + Basic.Settings.Stream.IgnoreRecommended + + + + + + + + + + + 0 + 0 + + + + Basic.Settings.Stream.ServiceSettings + + + + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + + + Qt::Horizontal - - - - - - - 0 - 0 - + + + 170 + 8 + + + + + - Basic.AutoConfig.StreamPage.MoreInfo + + + + Qt::RichText + + + true @@ -989,238 +1026,17 @@ - - - QFormLayout::AllNonFixedFieldsGrow + + + Qt::Vertical - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 20 + 10 + - - - - Basic.AutoConfig.StreamPage.Server - - - - - - - - 0 - 0 - - - - 1 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - -4 - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - - - - Qt::Horizontal - - - - 170 - 8 - - - - - - - - Basic.Settings.Stream.BandwidthTestMode - - - - - - - Basic.Settings.Stream.Custom.UseAuthentication - - - - - - - Basic.Settings.Stream.Custom.Username - - - authUsername - - - - - - - - - - Basic.Settings.Stream.Custom.Password - - - authPw - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - Basic.Settings.Stream.IgnoreRecommended - - - - - - - - - - Qt::RichText - - - true - - - - + @@ -7542,11 +7358,6 @@ - - UrlPushButton - QPushButton -
url-push-button.hpp
-
OBSHotkeyEdit QLineEdit @@ -7595,17 +7406,6 @@ multiviewDrawAreas multiviewLayout service - moreInfoButton - server - customServer - key - show - getStreamKeyButton - bandwidthTestEnable - useAuth - authUsername - authPw - authPwShow outputMode simpleOutputVBitrate simpleOutputABitrate @@ -7723,7 +7523,6 @@ enableLowLatencyMode browserHWAccel hotkeyFocusType - ignoreRecommended hotkeyFilterSearch hotkeyFilterInput hotkeyFilterReset diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index a4766c802c616f..cd2e6990178e51 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -6,32 +6,24 @@ #include "obs-app.hpp" #include "window-basic-main.hpp" #include "qt-wrappers.hpp" -#include "url-push-button.hpp" -enum class ListOpt : int { - ShowAll = 1, - Custom, -}; +constexpr int SHOW_ALL = 1; +constexpr const char *TEMP_SERVICE_NAME = "temp_service"; -inline bool OBSBasicSettings::IsCustomService() const +inline bool OBSBasicSettings::IsCustomOrInternalService() const { - return ui->service->currentData().toInt() == (int)ListOpt::Custom; + return ui->service->currentIndex() == -1 || + ui->service->currentData().toString() == "custom_service"; } void OBSBasicSettings::InitStreamPage() { - ui->bandwidthTestEnable->setVisible(false); - int vertSpacing = ui->topStreamLayout->verticalSpacing(); QMargins m = ui->topStreamLayout->contentsMargins(); m.setBottom(vertSpacing / 2); ui->topStreamLayout->setContentsMargins(m); - m = ui->streamkeyPageLayout->contentsMargins(); - m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); - LoadServices(false); connect(ui->ignoreRecommended, SIGNAL(clicked(bool)), this, @@ -42,141 +34,67 @@ void OBSBasicSettings::InitStreamPage() void OBSBasicSettings::LoadStream1Settings() { + loading = true; + bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); + obs_service_t *service = main->GetService(); + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *id = obs_service_get_id(service); + uint32_t flags = obs_service_get_flags(service); - obs_service_t *service_obj = main->GetService(); - const char *type = obs_service_get_type(service_obj); + tempService = + obs_service_create_private(id, TEMP_SERVICE_NAME, nullptr); - loading = true; + /* Avoid sharing the same obs_data_t pointer, + * between the service ,the temp service and the properties view */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); + obs_service_update(tempService, settings); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const char *service = obs_data_get_string(settings, "service"); - const char *server = obs_data_get_string(settings, "server"); - const char *key = obs_data_get_string(settings, "key"); - protocol = QT_UTF8(obs_service_get_protocol(service_obj)); - - if (strcmp(type, "rtmp_custom") == 0) { - ui->service->setCurrentIndex(0); - ui->customServer->setText(server); - lastServiceIdx = 0; - lastCustomServer = ui->customServer->text(); - - bool use_auth = obs_data_get_bool(settings, "use_auth"); - const char *username = - obs_data_get_string(settings, "username"); - const char *password = - obs_data_get_string(settings, "password"); - ui->authUsername->setText(QT_UTF8(username)); - ui->authPw->setText(QT_UTF8(password)); - ui->useAuth->setChecked(use_auth); - - /* add tooltips for stream key, user, password fields */ - QString file = !App()->IsThemeDark() - ? ":/res/images/help.svg" - : ":/res/images/help_light.svg"; - QString lStr = "%1 "; - - ui->streamKeyLabel->setText( - lStr.arg(ui->streamKeyLabel->text(), file)); - ui->streamKeyLabel->setToolTip( - QTStr("Basic.AutoConfig.StreamPage.StreamKey.ToolTip")); - - ui->authUsernameLabel->setText( - lStr.arg(ui->authUsernameLabel->text(), file)); - ui->authUsernameLabel->setToolTip( - QTStr("Basic.Settings.Stream.Custom.Username.ToolTip")); - - ui->authPwLabel->setText( - lStr.arg(ui->authPwLabel->text(), file)); - ui->authPwLabel->setToolTip( - QTStr("Basic.Settings.Stream.Custom.Password.ToolTip")); - } else { - int idx = ui->service->findText(service); - if (idx == -1) { - if (service && *service) - ui->service->insertItem(1, service); - idx = 1; - } - ui->service->setCurrentIndex(idx); - lastServiceIdx = idx; - - bool bw_test = obs_data_get_bool(settings, "bwtest"); - ui->bandwidthTestEnable->setChecked(bw_test); - } + if ((flags & OBS_SERVICE_UNCOMMON) != 0) + LoadServices(true); - UpdateServerList(); + int idx = ui->service->findData(QT_UTF8(id)); + if (idx == -1) { + QString name(obs_service_get_display_name(id)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); - if (strcmp(type, "rtmp_common") == 0) { - int idx = ui->server->findData(server); - if (idx == -1) { - if (server && *server) - ui->server->insertItem(0, server, server); - idx = 0; - } - ui->server->setCurrentIndex(idx); + ui->service->setPlaceholderText(name); } - ui->key->setText(key); + QSignalBlocker s(ui->service); + QSignalBlocker i(ui->ignoreRecommended); - lastService.clear(); - ServiceChanged(); + ui->service->setCurrentIndex(idx); - UpdateKeyLink(); - UpdateMoreInfoLink(); - UpdateVodTrackSetting(); - UpdateServiceRecommendations(); + ui->ignoreRecommended->setEnabled(!IsCustomOrInternalService()); + ui->ignoreRecommended->setChecked(ignoreRecommended); - bool streamActive = obs_frontend_streaming_active(); - ui->streamPage->setEnabled(!streamActive); + delete streamServiceProps; + streamServiceProps = CreateServicePropertyView(id, settings); + ui->serviceLayout->addWidget(streamServiceProps); - ui->ignoreRecommended->setChecked(ignoreRecommended); + UpdateServiceRecommendations(); + DisplayEnforceWarning(ignoreRecommended); loading = false; - QMetaObject::invokeMethod(this, "UpdateResFPSLimits", + QMetaObject::invokeMethod(this, &OBSBasicSettings::UpdateResFPSLimits, Qt::QueuedConnection); } void OBSBasicSettings::SaveStream1Settings() { - bool customServer = IsCustomService(); - 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_service_get_settings(tempService); + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); - OBSDataAutoRelease settings = obs_data_create(); - - if (!customServer) { - obs_data_set_string(settings, "service", - QT_TO_UTF8(ui->service->currentText())); - obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); - } else { - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->customServer->text().trimmed())); - obs_data_set_bool(settings, "use_auth", - ui->useAuth->isChecked()); - if (ui->useAuth->isChecked()) { - obs_data_set_string( - settings, "username", - QT_TO_UTF8(ui->authUsername->text())); - obs_data_set_string(settings, "password", - QT_TO_UTF8(ui->authPw->text())); - } - } - - obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); - - OBSServiceAutoRelease newService = obs_service_create( - service_id, "default_service", settings, hotkeyData); + OBSServiceAutoRelease newService = + obs_service_create(obs_service_get_id(tempService), + "default_service", settings, nullptr); if (!newService) return; @@ -187,328 +105,145 @@ void OBSBasicSettings::SaveStream1Settings() SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); } -void OBSBasicSettings::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 OBSBasicSettings::UpdateKeyLink() +void OBSBasicSettings::LoadServices(bool showAll) { - 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(); + const char *id; + size_t idx = 0; + bool needShowAllOption = false; - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); + QSignalBlocker sb(ui->service); - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + ui->service->clear(); - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = - "https://www.facebook.com/live/producer?ref=OBS"; - } + while (obs_enum_service_types(idx++, &id)) { + uint32_t flags = obs_get_service_flags(id); - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - } else if (!IsCustomService()) { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - } + if ((flags & OBS_SERVICE_INTERNAL) != 0) + continue; - if (QString(streamKeyLink).isNull() || - QString(streamKeyLink).isEmpty()) { - ui->getStreamKeyButton->hide(); - } else { - ui->getStreamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->getStreamKeyButton->show(); - } - obs_properties_destroy(props); -} + QStringList protocols = + QT_UTF8(obs_get_service_supported_protocols(id)) + .split(";"); -void OBSBasicSettings::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); + if (protocols.empty()) { + blog(LOG_WARNING, "No protocol found for service '%s'", + id); + continue; + } - OBSDataAutoRelease settings = obs_data_create(); + bool protocolRegistered = false; + for (uint32_t i = 0; i < protocols.size(); i++) { + protocolRegistered |= obs_is_output_protocol_registered( + QT_TO_UTF8(protocols[i])); + } - obs_data_set_bool(settings, "show_all", showAll); + if (!protocolRegistered) { + blog(LOG_WARNING, + "No registered protocol compatible with service '%s'", + id); + continue; + } - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); + bool isUncommon = (flags & OBS_SERVICE_UNCOMMON) != 0; + bool isDeprecated = (flags & OBS_SERVICE_DEPRECATED) != 0; - ui->service->blockSignals(true); - ui->service->clear(); + QString name(obs_service_get_display_name(id)); + if (isDeprecated) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); - QStringList names; + if (showAll || !(isUncommon || isDeprecated)) + ui->service->addItem(name, QT_UTF8(id)); - 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 ((isUncommon || isDeprecated) && !showAll) + needShowAllOption = true; } - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { + if (needShowAllOption) { 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); + QVariant(SHOW_ALL)); } - - obs_properties_destroy(props); - - ui->service->blockSignals(false); } -void OBSBasicSettings::on_service_currentIndexChanged(int idx) +void OBSBasicSettings::on_service_currentIndexChanged(int) { - if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) { + ui->service->setPlaceholderText(""); + + if (ui->service->currentData().toInt() == SHOW_ALL) { LoadServices(true); ui->service->showPopup(); return; } - ServiceChanged(); - - UpdateMoreInfoLink(); - UpdateServerList(); - UpdateKeyLink(); - UpdateServiceRecommendations(); - - UpdateVodTrackSetting(); - - protocol = FindProtocol(); - UpdateAdvNetworkGroup(); - - if (ServiceSupportsCodecCheck() && UpdateResFPSLimits()) { - lastServiceIdx = idx; - if (idx == 0) - lastCustomServer = ui->customServer->text(); + const char *oldId = obs_service_get_id(tempService); + OBSDataAutoRelease oldSettings = obs_service_get_settings(tempService); + + QString service = ui->service->currentData().toString(); + OBSDataAutoRelease newSettings = + obs_service_defaults(QT_TO_UTF8(service)); + tempService = obs_service_create_private( + QT_TO_UTF8(service), TEMP_SERVICE_NAME, newSettings); + + bool cancelChange = false; + if (!obs_service_get_protocol(tempService)) { + /* + * Cancel the change if the service happen to be without default protocol. + * + * This is better than generating dozens of obs_service_t to check + * if there is a default protocol while filling the combo box. + */ + OBSMessageBox::warning( + this, + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Title"), + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Text") + .arg(ui->service->currentText())); + cancelChange = true; } -} - -void OBSBasicSettings::on_customServer_textChanged(const QString &) -{ - UpdateKeyLink(); - protocol = FindProtocol(); - UpdateAdvNetworkGroup(); - - if (ServiceSupportsCodecCheck()) - lastCustomServer = ui->customServer->text(); -} - -void OBSBasicSettings::ServiceChanged() -{ - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool custom = IsCustomService(); - - ui->bandwidthTestEnable->setVisible(false); - - ui->useAuth->setVisible(custom); - ui->authUsernameLabel->setVisible(custom); - ui->authUsername->setVisible(custom); - ui->authPwLabel->setVisible(custom); - ui->authPwWidget->setVisible(custom); - - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, - ui->serverStackedWidget); - - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - on_useAuth_toggled(); - } else { - ui->serverStackedWidget->setCurrentIndex(0); - } -} - -QString OBSBasicSettings::FindProtocol() -{ - if (IsCustomService()) { - if (ui->customServer->text().isEmpty()) - return QString("RTMP"); - - QString server = ui->customServer->text(); - - if (obs_is_output_protocol_registered("RTMPS") && - server.startsWith("rtmps://")) - return QString("RTMPS"); - - if (server.startsWith("ftl://")) - return QString("FTL"); - - if (server.startsWith("srt://")) - return QString("SRT"); - - if (server.startsWith("rist://")) - return QString("RIST"); - - } else { - 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(ui->service->currentText())); - obs_property_modified(services, settings); - - obs_properties_destroy(props); - - const char *protocol = - obs_data_get_string(settings, "protocol"); - if (protocol && *protocol) - return QT_UTF8(protocol); - } - - return QString("RTMP"); -} - -void OBSBasicSettings::UpdateServerList() -{ - QString serviceName = ui->service->currentText(); - - 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 OBSBasicSettings::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 OBSBasicSettings::on_authPwShow_clicked() -{ - if (ui->authPw->echoMode() == QLineEdit::Password) { - ui->authPw->setEchoMode(QLineEdit::Normal); - ui->authPwShow->setText(QTStr("Hide")); - } else { - ui->authPw->setEchoMode(QLineEdit::Password); - ui->authPwShow->setText(QTStr("Show")); - } -} - -OBSService OBSBasicSettings::SpawnTempService() -{ - bool custom = IsCustomService(); - const char *service_id = custom ? "rtmp_custom" : "rtmp_common"; + if (cancelChange || + !(ServiceSupportsCodecCheck() && UpdateResFPSLimits())) { + tempService = obs_service_create_private( + oldId, TEMP_SERVICE_NAME, oldSettings); + uint32_t flags = obs_get_service_flags(oldId); + if ((flags & OBS_SERVICE_INTERNAL) != 0) { + QString name(obs_service_get_display_name(oldId)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); + + ui->service->setPlaceholderText(name); + } - OBSDataAutoRelease settings = obs_data_create(); + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex( + ui->service->findData(QT_UTF8(oldId))); - if (!custom) { - obs_data_set_string(settings, "service", - QT_TO_UTF8(ui->service->currentText())); - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->server->currentData().toString())); - } else { - obs_data_set_string( - settings, "server", - QT_TO_UTF8(ui->customServer->text().trimmed())); + return; } - obs_data_set_string(settings, "key", QT_TO_UTF8(ui->key->text())); - OBSServiceAutoRelease newService = obs_service_create( - service_id, "temp_service", settings, nullptr); - return newService.Get(); -} + ui->ignoreRecommended->setEnabled(!IsCustomOrInternalService()); -void OBSBasicSettings::on_useAuth_toggled() -{ - if (!IsCustomService()) - return; + delete streamServiceProps; + streamServiceProps = + CreateServicePropertyView(QT_TO_UTF8(service), nullptr, true); + ui->serviceLayout->addWidget(streamServiceProps); - bool use_auth = ui->useAuth->isChecked(); + UpdateServiceRecommendations(); - ui->authUsernameLabel->setVisible(use_auth); - ui->authUsername->setVisible(use_auth); - ui->authPwLabel->setVisible(use_auth); - ui->authPwWidget->setVisible(use_auth); + UpdateVodTrackSetting(); + UpdateAdvNetworkGroup(); } void OBSBasicSettings::UpdateVodTrackSetting() { - OBSService service = GetStream1Service(); bool enableForCustomServer = config_get_bool( GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); - bool enableVodTrack = obs_service_get_audio_track_cap(service) == + bool enableVodTrack = obs_service_get_audio_track_cap(tempService) == OBS_SERVICE_AUDIO_ARCHIVE_TRACK; bool wasEnabled = !!vodTrackCheckbox; - if (enableForCustomServer && IsCustomService()) + if (enableForCustomServer && IsCustomOrInternalService()) enableVodTrack = true; if (enableVodTrack == wasEnabled) @@ -587,28 +322,17 @@ void OBSBasicSettings::UpdateVodTrackSetting() } } -OBSService OBSBasicSettings::GetStream1Service() -{ - return stream1Changed ? SpawnTempService() - : OBSService(main->GetService()); -} - void OBSBasicSettings::UpdateServiceRecommendations() { - bool customServer = IsCustomService(); - ui->ignoreRecommended->setVisible(!customServer); - ui->enforceSettingsLabel->setVisible(!customServer); - - OBSService service = GetStream1Service(); - int vbitrate, abitrate; BPtr res_list; size_t res_count; int fps; - obs_service_get_max_bitrate(service, &vbitrate, &abitrate); - obs_service_get_supported_resolutions(service, &res_list, &res_count); - obs_service_get_max_fps(service, &fps); + obs_service_get_max_bitrate(tempService, &vbitrate, &abitrate); + obs_service_get_supported_resolutions(tempService, &res_list, + &res_count); + obs_service_get_max_fps(tempService, &fps); QString text; @@ -651,12 +375,13 @@ void OBSBasicSettings::UpdateServiceRecommendations() } #undef ENFORCE_TEXT + ui->enforceSettings->setVisible(!text.isEmpty()); ui->enforceSettingsLabel->setText(text); } void OBSBasicSettings::DisplayEnforceWarning(bool checked) { - if (IsCustomService()) + if (IsCustomOrInternalService()) return; if (!checked) { @@ -758,11 +483,10 @@ bool OBSBasicSettings::UpdateResFPSLimits() size_t res_count = 0; int max_fps = 0; - if (!IsCustomService() && !ignoreRecommended) { - OBSService service = GetStream1Service(); - obs_service_get_supported_resolutions(service, &res_list, + if (!IsCustomOrInternalService() && !ignoreRecommended) { + obs_service_get_supported_resolutions(tempService, &res_list, &res_count); - obs_service_get_max_fps(service, &max_fps); + obs_service_get_max_fps(tempService, &max_fps); } /* ------------------------------------ */ @@ -822,16 +546,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() bool valid = ResFPSValid(res_list, res_count, max_fps); if (!valid) { - /* if the user was already on facebook with an incompatible - * resolution, assume it's an upgrade */ - if (lastServiceIdx == -1 && lastIgnoreRecommended == -1) { - ui->ignoreRecommended->setChecked(true); - ui->ignoreRecommended->setProperty("changed", true); - stream1Changed = true; - EnableApplyButton(true); - return UpdateResFPSLimits(); - } - QMessageBox::StandardButton button; #define WARNING_VAL(x) \ @@ -851,16 +565,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() #undef WARNING_VAL if (button == QMessageBox::No) { - if (idx != lastServiceIdx) - QMetaObject::invokeMethod( - ui->service, "setCurrentIndex", - Qt::QueuedConnection, - Q_ARG(int, lastServiceIdx)); - else - QMetaObject::invokeMethod(ui->ignoreRecommended, - "setChecked", - Qt::QueuedConnection, - Q_ARG(bool, true)); return false; } } @@ -941,8 +645,6 @@ bool OBSBasicSettings::UpdateResFPSLimits() /* ------------------------------------ */ - lastIgnoreRecommended = (int)ignoreRecommended; - return true; } @@ -998,15 +700,16 @@ bool OBSBasicSettings::ServiceAndVCodecCompatible() codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); } - OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_video_codecs(service); + const char **codecs = + obs_service_get_supported_video_codecs(tempService); - if (!codecs || IsCustomService()) { + if (!codecs) { const char *output; char **output_codecs; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol( + obs_service_get_protocol(tempService), &output, + return_first_id); output_codecs = strlist_split( obs_get_output_supported_video_codecs(output), ';', @@ -1037,15 +740,16 @@ bool OBSBasicSettings::ServiceAndACodecCompatible() codec = obs_get_encoder_codec(QT_TO_UTF8(encoder)); } - OBSService service = SpawnTempService(); - const char **codecs = obs_service_get_supported_audio_codecs(service); + const char **codecs = + obs_service_get_supported_audio_codecs(tempService); - if (!codecs || IsCustomService()) { + if (!codecs) { const char *output; char **output_codecs; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol( + obs_service_get_protocol(tempService), &output, + return_first_id); output_codecs = strlist_split( obs_get_output_supported_audio_codecs(output), ';', false); @@ -1113,9 +817,7 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() bool acodec_compat = ServiceAndACodecCompatible(); if (vcodec_compat && acodec_compat) { - if (lastServiceIdx != ui->service->currentIndex() || - IsCustomService()) - ResetEncoders(true); + ResetEncoders(true); return true; } @@ -1182,17 +884,6 @@ bool OBSBasicSettings::ServiceSupportsCodecCheck() #undef WARNING_VAL if (button == QMessageBox::No) { - if (lastServiceIdx == 0 && - lastServiceIdx == ui->service->currentIndex()) - QMetaObject::invokeMethod(ui->customServer, "setText", - Qt::QueuedConnection, - Q_ARG(QString, - lastCustomServer)); - else - QMetaObject::invokeMethod(ui->service, - "setCurrentIndex", - Qt::QueuedConnection, - Q_ARG(int, lastServiceIdx)); return false; } @@ -1211,30 +902,32 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) QString lastAdvAudioEnc = ui->advOutAEncoder->currentData().toString(); QString lastAudioEnc = ui->simpleOutStrAEncoder->currentData().toString(); - OBSService service = SpawnTempService(); - const char **vcodecs = obs_service_get_supported_video_codecs(service); - const char **acodecs = obs_service_get_supported_audio_codecs(service); + const char *protocol = obs_service_get_protocol(tempService); + const char **vcodecs = + obs_service_get_supported_video_codecs(tempService); + const char **acodecs = + obs_service_get_supported_audio_codecs(tempService); const char *type; BPtr output_vcodecs; BPtr output_acodecs; size_t idx = 0; - if (!vcodecs || IsCustomService()) { + if (!vcodecs) { const char *output; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol(protocol, &output, + return_first_id); output_vcodecs = strlist_split( obs_get_output_supported_video_codecs(output), ';', false); vcodecs = (const char **)output_vcodecs.Get(); } - if (!acodecs || IsCustomService()) { + if (!acodecs) { const char *output; - obs_enum_output_types_with_protocol(QT_TO_UTF8(protocol), - &output, return_first_id); + obs_enum_output_types_with_protocol(protocol, &output, + return_first_id); output_acodecs = strlist_split( obs_get_output_supported_audio_codecs(output), ';', false); @@ -1445,3 +1138,61 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) ui->simpleOutStrAEncoder->setCurrentIndex(idx); } } + +OBSPropertiesView * +OBSBasicSettings::CreateServicePropertyView(const char *service, + obs_data_t *settings, bool changed) +{ + OBSDataAutoRelease defaultSettings = obs_service_defaults(service); + OBSPropertiesView *view; + + view = new OBSPropertiesView( + settings ? settings : defaultSettings.Get(), service, + (PropertiesReloadCallback)obs_get_service_properties, 170); + view->setFrameShape(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + view->setProperty("changed", QVariant(changed)); + /* NOTE: Stream1Changed is implemented inside ServicePropertyViewChanged, + * in case the settings are reverted. */ + QObject::connect(view, &OBSPropertiesView::Changed, this, + &OBSBasicSettings::ServicePropertyViewChanged); + + return view; +} + +void OBSBasicSettings::ServicePropertyViewChanged() +{ + OBSDataAutoRelease settings = obs_service_get_settings(tempService); + QString oldSettingsJson = QT_UTF8(obs_data_get_json(settings)); + obs_service_update(tempService, streamServiceProps->GetSettings()); + + if (!(ServiceSupportsCodecCheck() && UpdateResFPSLimits())) { + QMetaObject::invokeMethod(this, "RestoreServiceSettings", + Qt::QueuedConnection, + Q_ARG(QString, oldSettingsJson)); + return; + } + + UpdateVodTrackSetting(); + UpdateAdvNetworkGroup(); + + if (!loading) { + stream1Changed = true; + streamServiceProps->setProperty("changed", QVariant(true)); + EnableApplyButton(true); + } +} + +void OBSBasicSettings::RestoreServiceSettings(QString settingsJson) +{ + OBSDataAutoRelease settings = + obs_data_create_from_json(QT_TO_UTF8(settingsJson)); + obs_service_update(tempService, settings); + + bool changed = streamServiceProps->property("changed").toBool(); + + delete streamServiceProps; + streamServiceProps = CreateServicePropertyView( + obs_service_get_id(tempService), settings, changed); + ui->serviceLayout->addWidget(streamServiceProps); +} diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 6298ef629e0287..20a3ea6609ad30 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -473,13 +473,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->multiviewDrawAreas, CHECK_CHANGED, GENERAL_CHANGED); HookWidget(ui->multiviewLayout, COMBO_CHANGED, GENERAL_CHANGED); HookWidget(ui->service, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->server, COMBO_CHANGED, STREAM1_CHANGED); - HookWidget(ui->customServer, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->key, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->bandwidthTestEnable, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->useAuth, CHECK_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authUsername, EDIT_CHANGED, STREAM1_CHANGED); - HookWidget(ui->authPw, EDIT_CHANGED, STREAM1_CHANGED); HookWidget(ui->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED); HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); @@ -5794,11 +5787,10 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() QString qual = ui->simpleOutRecQuality->currentData().toString(); QString warning; bool enforceBitrate = !ui->ignoreRecommended->isChecked(); - OBSService service = GetStream1Service(); delete simpleOutRecWarning; - if (enforceBitrate && service) { + if (enforceBitrate && tempService) { OBSDataAutoRelease videoSettings = obs_data_create(); OBSDataAutoRelease audioSettings = obs_data_create(); int oldVBitrate = ui->simpleOutputVBitrate->value(); @@ -5807,7 +5799,7 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() obs_data_set_int(videoSettings, "bitrate", oldVBitrate); obs_data_set_int(audioSettings, "bitrate", oldABitrate); - obs_service_apply_encoder_settings(service, videoSettings, + obs_service_apply_encoder_settings(tempService, videoSettings, audioSettings); int newVBitrate = obs_data_get_int(videoSettings, "bitrate"); @@ -6218,7 +6210,8 @@ void OBSBasicSettings::RecreateOutputResolutionWidget() void OBSBasicSettings::UpdateAdvNetworkGroup() { - bool enabled = protocol.contains("RTMP"); + bool enabled = + QT_UTF8(obs_service_get_protocol(tempService)).contains("RTMP"); ui->advNetworkDisabled->setVisible(!enabled); diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index d05dc0716c2249..1191716ccb62d8 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -123,8 +123,6 @@ class OBSBasicSettings : public QDialog { bool hotkeysLoaded = false; int lastSimpleRecQualityIdx = 0; - int lastServiceIdx = -1; - int lastIgnoreRecommended = -1; int lastChannelSetupIdx = 0; static constexpr uint32_t ENCODER_HIDE_FLAGS = @@ -132,7 +130,8 @@ class OBSBasicSettings : public QDialog { OBSFFFormatDesc formats; - OBSPropertiesView *streamProperties = nullptr; + OBSPropertiesView *streamServiceProps = nullptr; + OBSPropertiesView *streamEncoderProps = nullptr; OBSPropertiesView *recordEncoderProps = nullptr; @@ -251,31 +250,30 @@ class OBSBasicSettings : public QDialog { void LoadBranchesList(); /* stream */ + OBSServiceAutoRelease tempService; + void InitStreamPage(); - inline bool IsCustomService() const; + inline bool IsCustomOrInternalService() const; void LoadServices(bool showAll); - QString lastService; - QString protocol; - QString lastCustomServer; int prevLangIndex; bool prevBrowserAccel; - void ServiceChanged(); - QString FindProtocol(); - void UpdateServerList(); void UpdateKeyLink(); void UpdateVodTrackSetting(); void UpdateServiceRecommendations(); - void UpdateMoreInfoLink(); void UpdateAdvNetworkGroup(); + OBSPropertiesView *CreateServicePropertyView(const char *service, + obs_data_t *settings, + bool changed = false); + private slots: void RecreateOutputResolutionWidget(); bool UpdateResFPSLimits(); void DisplayEnforceWarning(bool checked); - void on_show_clicked(); - void on_authPwShow_clicked(); - void on_useAuth_toggled(); + + void ServicePropertyViewChanged(); + void RestoreServiceSettings(QString settingsJson); void on_hotkeyFilterReset_clicked(); void on_hotkeyFilterSearch_textChanged(const QString text); @@ -368,8 +366,6 @@ private slots: int SimpleOutGetSelectedAudioTracks(); int AdvOutGetSelectedAudioTracks(); - OBSService GetStream1Service(); - bool ServiceAndVCodecCompatible(); bool ServiceAndACodecCompatible(); bool ServiceSupportsCodecCheck(); @@ -381,7 +377,6 @@ private slots: void on_buttonBox_clicked(QAbstractButton *button); void on_service_currentIndexChanged(int idx); - void on_customServer_textChanged(const QString &text); void on_simpleOutputBrowse_clicked(); void on_advOutRecPathBrowse_clicked(); void on_advOutFFPathBrowse_clicked(); @@ -450,8 +445,6 @@ private slots: void SimpleStreamingEncoderChanged(); - OBSService SpawnTempService(); - void SetGeneralIcon(const QIcon &icon); void SetStreamIcon(const QIcon &icon); void SetOutputIcon(const QIcon &icon); From 647a743ba3591cca7beb0b942382b2fc97bba53c Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sun, 4 Jun 2023 14:10:52 +0200 Subject: [PATCH 13/65] UI: Use service object to create PropertiesView --- UI/window-basic-settings-stream.cpp | 34 ++++++++++++++--------------- UI/window-basic-settings.hpp | 5 ++--- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index cd2e6990178e51..6ac5e692f340f7 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -74,7 +74,7 @@ void OBSBasicSettings::LoadStream1Settings() ui->ignoreRecommended->setChecked(ignoreRecommended); delete streamServiceProps; - streamServiceProps = CreateServicePropertyView(id, settings); + streamServiceProps = CreateTempServicePropertyView(settings); ui->serviceLayout->addWidget(streamServiceProps); UpdateServiceRecommendations(); @@ -176,8 +176,8 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) return; } - const char *oldId = obs_service_get_id(tempService); - OBSDataAutoRelease oldSettings = obs_service_get_settings(tempService); + OBSServiceAutoRelease oldService = + obs_service_get_ref(tempService.Get()); QString service = ui->service->currentData().toString(); OBSDataAutoRelease newSettings = @@ -203,11 +203,11 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) if (cancelChange || !(ServiceSupportsCodecCheck() && UpdateResFPSLimits())) { - tempService = obs_service_create_private( - oldId, TEMP_SERVICE_NAME, oldSettings); - uint32_t flags = obs_get_service_flags(oldId); + tempService = obs_service_get_ref(oldService); + const char *id = obs_service_get_id(tempService); + uint32_t flags = obs_get_service_flags(id); if ((flags & OBS_SERVICE_INTERNAL) != 0) { - QString name(obs_service_get_display_name(oldId)); + QString name(obs_service_get_display_name(id)); if ((flags & OBS_SERVICE_DEPRECATED) != 0) name = QTStr("Basic.Settings.Stream.DeprecatedType") .arg(name); @@ -217,7 +217,7 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) QSignalBlocker s(ui->service); ui->service->setCurrentIndex( - ui->service->findData(QT_UTF8(oldId))); + ui->service->findData(QT_UTF8(id))); return; } @@ -225,8 +225,7 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) ui->ignoreRecommended->setEnabled(!IsCustomOrInternalService()); delete streamServiceProps; - streamServiceProps = - CreateServicePropertyView(QT_TO_UTF8(service), nullptr, true); + streamServiceProps = CreateTempServicePropertyView(nullptr, true); ui->serviceLayout->addWidget(streamServiceProps); UpdateServiceRecommendations(); @@ -1140,15 +1139,17 @@ void OBSBasicSettings::ResetEncoders(bool streamOnly) } OBSPropertiesView * -OBSBasicSettings::CreateServicePropertyView(const char *service, - obs_data_t *settings, bool changed) +OBSBasicSettings::CreateTempServicePropertyView(obs_data_t *settings, + bool changed) { - OBSDataAutoRelease defaultSettings = obs_service_defaults(service); + OBSDataAutoRelease defaultSettings = + obs_service_defaults(obs_service_get_id(tempService)); OBSPropertiesView *view; view = new OBSPropertiesView( - settings ? settings : defaultSettings.Get(), service, - (PropertiesReloadCallback)obs_get_service_properties, 170); + settings ? settings : defaultSettings.Get(), tempService, + (PropertiesReloadCallback)obs_service_properties, nullptr, + nullptr, 170); view->setFrameShape(QFrame::NoFrame); view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); view->setProperty("changed", QVariant(changed)); @@ -1192,7 +1193,6 @@ void OBSBasicSettings::RestoreServiceSettings(QString settingsJson) bool changed = streamServiceProps->property("changed").toBool(); delete streamServiceProps; - streamServiceProps = CreateServicePropertyView( - obs_service_get_id(tempService), settings, changed); + streamServiceProps = CreateTempServicePropertyView(settings, changed); ui->serviceLayout->addWidget(streamServiceProps); } diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 1191716ccb62d8..466c471575fe6a 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -263,9 +263,8 @@ class OBSBasicSettings : public QDialog { void UpdateServiceRecommendations(); void UpdateAdvNetworkGroup(); - OBSPropertiesView *CreateServicePropertyView(const char *service, - obs_data_t *settings, - bool changed = false); + OBSPropertiesView *CreateTempServicePropertyView(obs_data_t *settings, + bool changed = false); private slots: void RecreateOutputResolutionWidget(); From d030ade29ca48bd9d0b49233f20e2c9641692cea Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 18 May 2023 15:05:18 +0200 Subject: [PATCH 14/65] UI: Sort services list --- UI/cmake/legacy.cmake | 2 ++ UI/cmake/ui-elements.cmake | 2 ++ UI/service-sort-filter.cpp | 39 +++++++++++++++++++++++++++++ UI/service-sort-filter.hpp | 17 +++++++++++++ UI/window-basic-settings-stream.cpp | 14 +++++++++++ 5 files changed, 74 insertions(+) create mode 100644 UI/service-sort-filter.cpp create mode 100644 UI/service-sort-filter.hpp diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index eb94eb0123d23f..236717164acfca 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -201,6 +201,8 @@ target_sources( scene-tree.cpp scene-tree.hpp screenshot-obj.hpp + service-sort-filter.cpp + service-sort-filter.hpp slider-absoluteset-style.cpp slider-absoluteset-style.hpp slider-ignorewheel.cpp diff --git a/UI/cmake/ui-elements.cmake b/UI/cmake/ui-elements.cmake index a24cd9b592d77d..c19d6e97d5a4b1 100644 --- a/UI/cmake/ui-elements.cmake +++ b/UI/cmake/ui-elements.cmake @@ -45,6 +45,8 @@ target_sources( scene-tree.cpp scene-tree.hpp screenshot-obj.hpp + service-sort-filter.cpp + service-sort-filter.hpp slider-absoluteset-style.cpp slider-absoluteset-style.hpp slider-ignorewheel.cpp diff --git a/UI/service-sort-filter.cpp b/UI/service-sort-filter.cpp new file mode 100644 index 00000000000000..b1c77fc739b1bf --- /dev/null +++ b/UI/service-sort-filter.cpp @@ -0,0 +1,39 @@ +#include "service-sort-filter.hpp" + +#include + +#include "obs-app.hpp" + +#define SERVICE_SHOW_ALL QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll") +constexpr const char *CUSTOM_SERVICE_ID = "custom_service"; + +ServiceSortFilterProxyModel::ServiceSortFilterProxyModel(QComboBox *parent) + : QSortFilterProxyModel(parent) +{ + setSortCaseSensitivity(Qt::CaseInsensitive); + + // Store text/data combo to be able to find the id while sorting + for (int i = 0; i < parent->count(); i++) + items[parent->itemText(i)] = parent->itemData(i); +} + +bool ServiceSortFilterProxyModel::lessThan(const QModelIndex &left, + const QModelIndex &right) const +{ + QVariant leftData = sourceModel()->data(left); + QVariant rightData = sourceModel()->data(right); + + // Make Show All last + if (leftData.toString() == SERVICE_SHOW_ALL) + return false; + if (rightData.toString() == SERVICE_SHOW_ALL) + return true; + + // Make Custom… first + if (items.value(leftData.toString()) == CUSTOM_SERVICE_ID) + return true; + if (items.value(rightData.toString()) == CUSTOM_SERVICE_ID) + return false; + + return QSortFilterProxyModel::lessThan(left, right); +} diff --git a/UI/service-sort-filter.hpp b/UI/service-sort-filter.hpp new file mode 100644 index 00000000000000..07d523188007c1 --- /dev/null +++ b/UI/service-sort-filter.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +class ServiceSortFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT + QHash items; + +public: + ServiceSortFilterProxyModel(QComboBox *parent); + +protected: + bool lessThan(const QModelIndex &left, + const QModelIndex &right) const override; +}; diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 6ac5e692f340f7..66e6a7a8ebd637 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -1,11 +1,13 @@ #include #include +#include #include "window-basic-settings.hpp" #include "obs-frontend-api.h" #include "obs-app.hpp" #include "window-basic-main.hpp" #include "qt-wrappers.hpp" +#include "service-sort-filter.hpp" constexpr int SHOW_ALL = 1; constexpr const char *TEMP_SERVICE_NAME = "temp_service"; @@ -114,6 +116,7 @@ void OBSBasicSettings::LoadServices(bool showAll) QSignalBlocker sb(ui->service); ui->service->clear(); + ui->service->setModel(new QStandardItemModel(0, 1, ui->service)); while (obs_enum_service_types(idx++, &id)) { uint32_t flags = obs_get_service_flags(id); @@ -164,6 +167,17 @@ void OBSBasicSettings::LoadServices(bool showAll) QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), QVariant(SHOW_ALL)); } + + QSortFilterProxyModel *model = + new ServiceSortFilterProxyModel(ui->service); + model->setSourceModel(ui->service->model()); + // Combo's current model must be reparented, + // Otherwise QComboBox::setModel() will delete it + ui->service->model()->setParent(model); + + ui->service->setModel(model); + + ui->service->model()->sort(0); } void OBSBasicSettings::on_service_currentIndexChanged(int) From fa910f97f26f7914a5baaeddd208625fba08d669 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 17 May 2023 14:39:19 +0200 Subject: [PATCH 15/65] UI: Remove Twitch region test feature in auto-config wizard --- UI/data/locale/en-US.ini | 7 ---- UI/forms/AutoConfigStreamPage.ui | 63 ++++++---------------------- UI/window-basic-auto-config-test.cpp | 6 +-- UI/window-basic-auto-config.cpp | 51 +--------------------- UI/window-basic-auto-config.hpp | 6 --- 5 files changed, 16 insertions(+), 117 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index b29153b639870c..18f0b09e3b74d8 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -168,13 +168,6 @@ Paste.Filters="Paste Filters" BrowserPanelInit.Title="Initializing Browser..." BrowserPanelInit.Text="Initializing browser, please wait..." -# bandwidth test -BandwidthTest.Region="Region" -BandwidthTest.Region.US="United States" -BandwidthTest.Region.EU="Europe" -BandwidthTest.Region.Asia="Asia" -BandwidthTest.Region.Other="Other" - # auto config wizard Basic.AutoConfig="Auto-Configuration Wizard" Basic.AutoConfig.ApplySettings="Apply Settings" diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index 5103a5cb0cbc6f..377c4fc1a64fdf 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -261,6 +261,19 @@
+ + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -284,43 +297,6 @@ - - - - BandwidthTest.Region - - - - - - BandwidthTest.Region.Asia - - - - - - - BandwidthTest.Region.US - - - - - - - BandwidthTest.Region.EU - - - - - - - BandwidthTest.Region.Other - - - - - - @@ -334,19 +310,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 0e556675915483..0680c578cbb0e0 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -134,10 +134,8 @@ void AutoConfigTestPage::GetServers(std::vector &servers) 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); - } + ServerInfo info(name, server); + servers.push_back(info); } obs_properties_destroy(ppts); diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 8a9ecd35a53d34..18f9e600cd7b83 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -281,14 +281,6 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) connect(ui->key, SIGNAL(textChanged(const QString &)), this, SLOT(UpdateCompleted())); - connect(ui->regionUS, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionEU, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionAsia, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); - connect(ui->regionOther, SIGNAL(toggled(bool)), this, - SLOT(UpdateCompleted())); } AutoConfigStreamPage::~AutoConfigStreamPage() {} @@ -349,10 +341,6 @@ bool AutoConfigStreamPage::validatePage() 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(); @@ -391,14 +379,9 @@ void AutoConfigStreamPage::ServiceChanged() return; std::string service = QT_TO_UTF8(ui->service->currentText()); - bool regionBased = service == "Twitch"; bool testBandwidth = ui->doBandwidthTest->isChecked(); bool custom = IsCustomService(); - /* Test three closest servers if "Auto" is available for Twitch */ - if (service == "Twitch" && wiz->twitchAuto) - regionBased = false; - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); @@ -406,7 +389,6 @@ void AutoConfigStreamPage::ServiceChanged() ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); - ui->region->setVisible(false); ui->serverStackedWidget->setCurrentIndex(1); ui->serverStackedWidget->setVisible(true); ui->serverLabel->setVisible(true); @@ -415,14 +397,11 @@ void AutoConfigStreamPage::ServiceChanged() 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); @@ -591,11 +570,7 @@ void AutoConfigStreamPage::UpdateCompleted() if (custom) { ready = !ui->customServer->text().isEmpty(); } else { - ready = !wiz->testRegions || - ui->regionUS->isChecked() || - ui->regionEU->isChecked() || - ui->regionAsia->isChecked() || - ui->regionOther->isChecked(); + ready = true; } } emit completeChanged(); @@ -767,30 +742,6 @@ void AutoConfig::TestHardwareEncoding() } } -bool AutoConfig::CanTestServer(const char *server) -{ - if (!testRegions || (regionUS && regionEU && regionAsia && regionOther)) - return true; - - if (serviceName == "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); diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index 955be07faff49e..ddcd66530ff76b 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -87,19 +87,13 @@ class AutoConfig : public QWizard { int startingBitrate = 2500; bool customServer = false; bool bandwidthTest = false; - bool testRegions = true; bool twitchAuto = 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; From 7c5cfc9233021c2040f744704267d954adfac277 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 22 Jun 2023 16:34:30 +0200 Subject: [PATCH 16/65] UI,libobs,docs: Add bandwidth test to the Services API --- UI/window-basic-main.cpp | 5 +---- docs/sphinx/reference-services.rst | 35 ++++++++++++++++++++++++++++++ libobs/obs-service.c | 35 ++++++++++++++++++++++++++++++ libobs/obs-service.h | 4 ++++ libobs/obs.h | 5 +++++ 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 806d971422f20e..fc495c32d32cd3 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -7950,12 +7950,9 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); - bool bwtest = false; + bool bwtest = obs_service_bandwidth_test_enabled(service); if (this->auth) { - OBSDataAutoRelease settings = - obs_service_get_settings(service); - bwtest = obs_data_get_bool(settings, "bwtest"); // Disable confirmation if this is going to open broadcast setup if (auth && auth->broadcastFlow() && !broadcastReady && !broadcastActive) diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index 72f1dcddda02d6..501aced97de1d9 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -231,6 +231,23 @@ Service Definition Structure This variable specifies which protocol are supported by the service, separated by semicolon. +.. member:: bool (*obs_service_info.can_bandwidth_test)(void *data) + + (Optional) + + :return: If the service can do bandwith test or not + +.. member:: void (*obs_service_info.enable_bandwidth_test)(void *data, bool enabled) + + Enable/disable the bandwith test of the service. + (Optional) + +.. member:: bool (*obs_service_info.bandwidth_test_enabled)(void *data) + + (Optional) + + :return: If the bandwith test is enabled or not + General Service Functions ------------------------- @@ -464,6 +481,24 @@ General Service Functions :return: Supported protocol of the service, separated by semicolon +--------------------- + +.. function:: bool obs_service_can_bandwidth_test(const obs_service_t *service) + + :return: If the service can do bandwidth test + +--------------------- + +.. function:: void obs_service_enable_bandwidth_test(const obs_service_t *service, bool enabled) + + Enable the bandwidth test has this capability + +--------------------- + +.. function:: bool obs_service_bandwidth_test_enabled(const obs_service_t *service) + + :return: If the service has bandwidth test enabled + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-service.c b/libobs/obs-service.c index c98dd9b8114735..6646808a691236 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -566,3 +566,38 @@ const char *obs_get_service_supported_protocols(const char *id) const struct obs_service_info *info = find_service(id); return info ? info->supported_protocols : NULL; } + +bool obs_service_can_bandwidth_test(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_has_bandwidth_test")) + return false; + + if (!(service->info.can_bandwidth_test && + service->info.enable_bandwidth_test && + service->info.bandwidth_test_enabled)) + return false; + + return service->info.can_bandwidth_test(service->context.data); +} + +void obs_service_enable_bandwidth_test(const obs_service_t *service, + bool enabled) +{ + if (!obs_service_valid(service, "obs_service_enable_bandwidth_test")) + return; + + if (service->info.enable_bandwidth_test) + service->info.enable_bandwidth_test(service->context.data, + enabled); +} + +bool obs_service_bandwidth_test_enabled(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_bandwidth_test_enabled")) + return false; + + if (!service->info.bandwidth_test_enabled) + return false; + + return service->info.bandwidth_test_enabled(service->context.data); +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 383565bebcfaba..ae8f14b5834b4c 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -130,6 +130,10 @@ struct obs_service_info { void (*get_defaults2)(void *type_data, obs_data_t *settings); obs_properties_t *(*get_properties2)(void *data, void *type_data); + + bool (*can_bandwidth_test)(void *data); + void (*enable_bandwidth_test)(void *data, bool enabled); + bool (*bandwidth_test_enabled)(void *data); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index aa9a54a5599602..071330fa68f7b1 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2678,6 +2678,11 @@ EXPORT uint32_t obs_service_get_flags(const obs_service_t *service); EXPORT const char *obs_get_service_supported_protocols(const char *id); +EXPORT bool obs_service_can_bandwidth_test(const obs_service_t *service); +EXPORT void obs_service_enable_bandwidth_test(const obs_service_t *service, + bool enabled); +EXPORT bool obs_service_bandwidth_test_enabled(const obs_service_t *service); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, From ea33cac44ddada46ecbcb1dc40b74bdef53d7109 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 20:16:55 +0200 Subject: [PATCH 17/65] UI: Use PropertyView for auto-wizard service --- UI/data/locale/en-US.ini | 6 - UI/data/themes/Acri.qss | 3 +- UI/data/themes/Grey.qss | 3 +- UI/data/themes/Light.qss | 3 +- UI/data/themes/Rachni.qss | 3 +- UI/data/themes/Yami.qss | 3 +- UI/forms/AutoConfigStreamPage.ui | 161 +------- UI/window-basic-auto-config-test.cpp | 118 ++---- UI/window-basic-auto-config.cpp | 533 ++++++++++----------------- UI/window-basic-auto-config.hpp | 29 +- UI/window-basic-settings-stream.cpp | 1 + 11 files changed, 271 insertions(+), 592 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 18f0b09e3b74d8..186b97d16d5497 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -193,7 +193,6 @@ Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..." Basic.AutoConfig.StreamPage.Service.Custom="Custom..." Basic.AutoConfig.StreamPage.Server="Server" Basic.AutoConfig.StreamPage.StreamKey="Stream Key" -Basic.AutoConfig.StreamPage.StreamKey.ToolTip="RIST: enter the encryption passphrase.\nRTMP: enter the key provided by the service.\nSRT: enter the streamid if the service uses one." Basic.AutoConfig.StreamPage.EncoderKey="Encoder Key" Basic.AutoConfig.StreamPage.PerformBandwidthTest="Estimate bitrate with bandwidth test (may take a few minutes)" Basic.AutoConfig.StreamPage.PreferHardwareEncoding="Prefer hardware encoding" @@ -900,11 +899,6 @@ Basic.Settings.Stream.DeprecatedType="%1 (Deprecated)" Basic.Settings.Stream.ServiceSettings="Service Settings" Basic.Settings.Stream.NoDefaultProtocol.Title="No Default Protocol" Basic.Settings.Stream.NoDefaultProtocol.Text="The service \"%1\" does not have a default protocol.\n\nThe service change will be reverted." -Basic.Settings.Stream.Custom.UseAuthentication="Use authentication" -Basic.Settings.Stream.Custom.Username="Username" -Basic.Settings.Stream.Custom.Password="Password" -Basic.Settings.Stream.Custom.Username.ToolTip="RIST: enter the srp_username.\nRTMP: enter the username.\nSRT: not used." -Basic.Settings.Stream.Custom.Password.ToolTip="RIST: enter the srp_password.\nRTMP: enter the password.\nSRT: enter the encryption passphrase." Basic.Settings.Stream.BandwidthTestMode="Enable Bandwidth Test Mode" Basic.Settings.Stream.MissingSettingAlert="Missing Stream Setup" Basic.Settings.Stream.StreamSettingsWarning="Open Settings" diff --git a/UI/data/themes/Acri.qss b/UI/data/themes/Acri.qss index 3a47002d1827db..4fd32e1f0cab2f 100644 --- a/UI/data/themes/Acri.qss +++ b/UI/data/themes/Acri.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Grey.qss b/UI/data/themes/Grey.qss index 93733dd07c6797..7eea4b0d83bb86 100644 --- a/UI/data/themes/Grey.qss +++ b/UI/data/themes/Grey.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Light.qss b/UI/data/themes/Light.qss index 3f8f81781f6e3e..f9316a10de507e 100644 --- a/UI/data/themes/Light.qss +++ b/UI/data/themes/Light.qss @@ -269,7 +269,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/data/themes/Rachni.qss b/UI/data/themes/Rachni.qss index 351e9bef9a24be..1c9c5b48dae0eb 100644 --- a/UI/data/themes/Rachni.qss +++ b/UI/data/themes/Rachni.qss @@ -271,7 +271,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: rgb(59,65,71); } diff --git a/UI/data/themes/Yami.qss b/UI/data/themes/Yami.qss index 54b3c2e6a225cf..453fa44afcab37 100644 --- a/UI/data/themes/Yami.qss +++ b/UI/data/themes/Yami.qss @@ -273,7 +273,8 @@ OBSBasicSettings QListWidget::item { } /* Settings properties view */ -OBSBasicSettings #PropertiesContainer { +OBSBasicSettings #PropertiesContainer, +QWizardPage #PropertiesContainer { background-color: palette(dark); } diff --git a/UI/forms/AutoConfigStreamPage.ui b/UI/forms/AutoConfigStreamPage.ui index 377c4fc1a64fdf..55e545956f7ce6 100644 --- a/UI/forms/AutoConfigStreamPage.ui +++ b/UI/forms/AutoConfigStreamPage.ui @@ -74,32 +74,7 @@ - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Basic.AutoConfig.StreamPage.MoreInfo - - - - - + @@ -117,6 +92,20 @@ + + + + + 0 + 0 + + + + Basic.Settings.Stream.ServiceSettings + + + + @@ -126,116 +115,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Basic.AutoConfig.StreamPage.Server - - - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - Basic.AutoConfig.StreamPage.StreamKey - - - true - - - key - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - Basic.AutoConfig.StreamPage.GetStreamKey - - - - - - - Basic.Settings.Output.VideoBitrate @@ -245,7 +124,7 @@ - + @@ -261,7 +140,7 @@ - + Qt::Horizontal @@ -274,7 +153,7 @@ - + Basic.AutoConfig.StreamPage.PreferHardwareEncoding.ToolTip @@ -287,7 +166,7 @@ - + Basic.AutoConfig.StreamPage.PerformBandwidthTest @@ -297,7 +176,7 @@ - + Qt::Vertical diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 0680c578cbb0e0..11ce8d8cef2a58 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -117,30 +117,6 @@ void AutoConfigTestPage::StartRecordingEncoderStage() 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); - - ServerInfo info(name, server); - servers.push_back(info); - } - - obs_properties_destroy(ppts); -} - static inline void string_depad_key(string &key) { while (!key.empty()) { @@ -193,48 +169,24 @@ void AutoConfigTestPage::TestBandwidthThread() /* -----------------------------------*/ /* 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->serviceName == "Twitch") { - string_depad_key(key); - key += "?bandwidthtest"; - } else if (wiz->serviceName == "Restream.io" || - wiz->serviceName == "Restream.io - RTMP") { - string_depad_key(key); - key += "?test=true"; - } else if (wiz->serviceName == "Restream.io - FTL") { - string_depad_key(key); - key += "?test"; - } - - 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"); @@ -251,30 +203,18 @@ void AutoConfigTestPage::TestBandwidthThread() /* 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->serviceName == "Twitch" && wiz->twitchAuto) { - /* if using Twitch and "Auto" is available, test 3 closest - * server */ - servers.erase(servers.begin() + 1); - servers.resize(3); - } + servers.emplace_back(obs_service_get_connect_info( + wiz->service, OBS_SERVICE_CONNECT_INFO_SERVER_URL)); + + /* Since we no longer hack arround obs_properties_t, we can only test + * the server set in the service properties. + * + * TODO: Create API of proc handler to restore this feature */ /* -----------------------------------*/ /* apply service settings */ - obs_service_update(service, service_settings); - obs_service_apply_encoder_settings(service, vencoder_settings, + obs_service_apply_encoder_settings(wiz->service, vencoder_settings, aencoder_settings); /* -----------------------------------*/ @@ -282,11 +222,11 @@ void AutoConfigTestPage::TestBandwidthThread() /* Check if the service has a preferred output type */ const char *output_type = - obs_service_get_preferred_output_type(service); + obs_service_get_preferred_output_type(wiz->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); + const char *protocol = obs_service_get_protocol(wiz->service); if (can_use_output(protocol, "rtmp_output", "RTMP", "RTMPS")) { output_type = "rtmp_output"; @@ -336,7 +276,7 @@ void AutoConfigTestPage::TestBandwidthThread() obs_output_set_audio_encoder(output, aencoder, 0); obs_output_set_reconnect_settings(output, 0, 0); - obs_output_set_service(output, service); + obs_output_set_service(output, wiz->service); /* -----------------------------------*/ /* connect signals */ @@ -379,6 +319,10 @@ void AutoConfigTestPage::TestBandwidthThread() bool success = false; + bool canBandwidthTest = obs_service_can_bandwidth_test(wiz->service); + if (canBandwidthTest) + obs_service_enable_bandwidth_test(wiz->service, true); + for (size_t i = 0; i < servers.size(); i++) { auto &server = servers[i]; @@ -392,10 +336,6 @@ void AutoConfigTestPage::TestBandwidthThread() 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; @@ -467,6 +407,9 @@ void AutoConfigTestPage::TestBandwidthThread() success = true; } + if (canBandwidthTest) + obs_service_enable_bandwidth_test(wiz->service, false); + if (!success) { QMetaObject::invokeMethod(this, "Failure", Q_ARG(QString, @@ -1051,30 +994,21 @@ void AutoConfigTestPage::FinalizeResults() }; 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(); 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); + obs_service_apply_encoder_settings(wiz->service, + vencoder_settings, nullptr); BPtr res_list; size_t res_count; int maxFPS; - obs_service_get_supported_resolutions(service, &res_list, + obs_service_get_supported_resolutions(wiz->service, &res_list, &res_count); - obs_service_get_max_fps(service, &maxFPS); + obs_service_get_max_fps(wiz->service, &maxFPS); if (res_list) { set_closest_res(wiz->idealResolutionCX, @@ -1093,11 +1027,9 @@ void AutoConfigTestPage::FinalizeResults() 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.Service"), + new QLabel(wiz->serviceName.c_str(), + ui->finishPage)); form->addRow(newLabel("Basic.AutoConfig.StreamPage.Server"), new QLabel(wiz->serverName.c_str(), ui->finishPage)); diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 18f9e600cd7b83..273134148daae7 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -9,6 +10,8 @@ #include "obs-app.hpp" #include "url-push-button.hpp" +#include "service-sort-filter.hpp" + #include "ui_AutoConfigStartPage.h" #include "ui_AutoConfigVideoPage.h" #include "ui_AutoConfigStreamPage.h" @@ -19,39 +22,6 @@ /* ------------------------------------------------------------------------- */ -#define SERVICE_PATH "service.json" - -static OBSData OpenServiceSettings(std::string &type) -{ - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return OBSData(); - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "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) @@ -237,7 +207,10 @@ enum class ListOpt : int { Custom, }; -AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) +constexpr const char *TEMP_SERVICE_NAME = "temp_service"; + +AutoConfigStreamPage::AutoConfigStreamPage(obs_service_t *service, + QWidget *parent) : QWizardPage(parent), ui(new Ui_AutoConfigStreamPage) { @@ -251,6 +224,10 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) m.setBottom(vertSpacing / 2); ui->topLayout->setContentsMargins(m); + m = ui->serviceProps->contentsMargins(); + m.setTop(vertSpacing / 2); + ui->serviceProps->setContentsMargins(m); + m = ui->streamkeyPageLayout->contentsMargins(); m.setTop(vertSpacing / 2); ui->streamkeyPageLayout->setContentsMargins(m); @@ -260,27 +237,40 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent) LoadServices(false); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(ServiceChanged())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(ServiceChanged())); - connect(ui->customServer, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateKeyLink())); - connect(ui->customServer, SIGNAL(editingFinished()), this, - SLOT(UpdateKeyLink())); - connect(ui->doBandwidthTest, SIGNAL(toggled(bool)), this, - SLOT(ServiceChanged())); + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *id = obs_service_get_id(service); + uint32_t flags = obs_service_get_flags(service); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateServerList())); + tempService = + obs_service_create_private(id, TEMP_SERVICE_NAME, nullptr); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateKeyLink())); - connect(ui->service, SIGNAL(currentIndexChanged(int)), this, - SLOT(UpdateMoreInfoLink())); + /* Avoid sharing the same obs_data_t pointer, + * between the service ,the temp service and the properties view */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); + obs_service_update(tempService, settings); - connect(ui->key, SIGNAL(textChanged(const QString &)), this, - SLOT(UpdateCompleted())); + /* Reset to index 0 if internal or deprecated */ + if ((flags & OBS_SERVICE_INTERNAL) == 0 && + (flags & OBS_SERVICE_DEPRECATED) == 0) { + if ((flags & OBS_SERVICE_UNCOMMON) != 0) + LoadServices(true); + + int idx = ui->service->findData(QT_UTF8(id)); + + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex(idx); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(settings); + ui->serviceLayout->addWidget(streamServiceProps); + + ServiceChanged(); + } else { + ui->service->currentIndexChanged(0); + } + + connect(ui->doBandwidthTest, SIGNAL(toggled(bool)), this, + SLOT(ServiceChanged())); } AutoConfigStreamPage::~AutoConfigStreamPage() {} @@ -295,30 +285,11 @@ 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()) { + if (!(ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled())) { bitrate = ui->bitrate->value(); wiz->idealBitrate = bitrate; } else { @@ -328,25 +299,20 @@ bool AutoConfigStreamPage::validatePage() OBSDataAutoRelease settings = obs_data_create(); obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(service, settings, nullptr); + obs_service_apply_encoder_settings(tempService, 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->service = obs_service_get_ref(tempService); - wiz->bandwidthTest = ui->doBandwidthTest->isChecked(); + wiz->bandwidthTest = ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled(); wiz->startingBitrate = (int)obs_data_get_int(settings, "bitrate"); wiz->idealBitrate = wiz->startingBitrate; 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->serviceName != "Twitch" && wiz->bandwidthTest) { + if (wiz->bandwidthTest && + !obs_service_can_bandwidth_test(tempService)) { QMessageBox::StandardButton button; #define WARNING_TEXT(x) QTStr("Basic.AutoConfig.StreamPage.StreamWarning." x) button = OBSMessageBox::question(this, WARNING_TEXT("Title"), @@ -360,47 +326,28 @@ bool AutoConfigStreamPage::validatePage() 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::ServiceChanged() { - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; - if (showMore) - return; + obs_service_update(tempService, streamServiceProps->GetSettings()); - std::string service = QT_TO_UTF8(ui->service->currentText()); - bool testBandwidth = ui->doBandwidthTest->isChecked(); - bool custom = IsCustomService(); + /* Enable bandwidth test if available */ + bool canBandwidthTest = obs_service_can_bandwidth_test(tempService); + if (canBandwidthTest) + obs_service_enable_bandwidth_test(tempService, true); - ui->streamkeyPageLayout->removeWidget(ui->serverLabel); - ui->streamkeyPageLayout->removeWidget(ui->serverStackedWidget); + /* Check if the service can connect to allow to do a bandwidth test */ + bool canTryToConnect = obs_service_can_try_to_connect(tempService); - if (custom) { - ui->streamkeyPageLayout->insertRow(1, ui->serverLabel, - ui->serverStackedWidget); + /* Disable bandwidth test */ + if (canBandwidthTest) + obs_service_enable_bandwidth_test(tempService, false); - ui->serverStackedWidget->setCurrentIndex(1); - ui->serverStackedWidget->setVisible(true); - ui->serverLabel->setVisible(true); - } else { - if (!testBandwidth) - ui->streamkeyPageLayout->insertRow( - 2, ui->serverLabel, ui->serverStackedWidget); + /* Make wizard bandwidth test available if the service can connect. + * Otherwise, disable it */ + ui->doBandwidthTest->setEnabled(canTryToConnect); - ui->serverStackedWidget->setCurrentIndex(0); - ui->serverStackedWidget->setHidden(testBandwidth); - ui->serverLabel->setHidden(testBandwidth); - } + bool testBandwidth = ui->doBandwidthTest->isChecked() && + ui->doBandwidthTest->isEnabled(); ui->bitrateLabel->setHidden(testBandwidth); ui->bitrate->setHidden(testBandwidth); @@ -408,172 +355,168 @@ void AutoConfigStreamPage::ServiceChanged() 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); -} +constexpr int SHOW_ALL = 1; -void AutoConfigStreamPage::UpdateKeyLink() +/* Note: Identical to OBSBasicSettings function except it does not show deprecated services */ +void AutoConfigStreamPage::LoadServices(bool showAll) { - 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(); + const char *id; + size_t idx = 0; + bool needShowAllOption = false; - obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName)); - obs_property_modified(services, settings); + QSignalBlocker sb(ui->service); - streamKeyLink = obs_data_get_string(settings, "stream_key_link"); + ui->service->clear(); + ui->service->setModel(new QStandardItemModel(0, 1, ui->service)); - if (customServer.contains("fbcdn.net") && IsCustomService()) { - streamKeyLink = - "https://www.facebook.com/live/producer?ref=OBS"; - } + while (obs_enum_service_types(idx++, &id)) { + uint32_t flags = obs_get_service_flags(id); - if (serviceName == "Dacast") { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.EncoderKey")); - } else { - ui->streamKeyLabel->setText( - QTStr("Basic.AutoConfig.StreamPage.StreamKey")); - } + if ((flags & OBS_SERVICE_INTERNAL) != 0 || + (flags & OBS_SERVICE_DEPRECATED) != 0) + continue; - if (QString(streamKeyLink).isNull() || - QString(streamKeyLink).isEmpty()) { - ui->streamKeyButton->hide(); - } else { - ui->streamKeyButton->setTargetUrl(QUrl(streamKeyLink)); - ui->streamKeyButton->show(); - } - obs_properties_destroy(props); -} + QStringList protocols = + QT_UTF8(obs_get_service_supported_protocols(id)) + .split(";"); -void AutoConfigStreamPage::LoadServices(bool showAll) -{ - obs_properties_t *props = obs_get_service_properties("rtmp_common"); + if (protocols.empty()) { + blog(LOG_WARNING, "No protocol found for service '%s'", + id); + continue; + } - OBSDataAutoRelease settings = obs_data_create(); + bool protocolRegistered = false; + for (uint32_t i = 0; i < protocols.size(); i++) { + protocolRegistered |= obs_is_output_protocol_registered( + QT_TO_UTF8(protocols[i])); + } - obs_data_set_bool(settings, "show_all", showAll); + if (!protocolRegistered) { + blog(LOG_WARNING, + "No registered protocol compatible with service '%s'", + id); + continue; + } - obs_property_t *prop = obs_properties_get(props, "show_all"); - obs_property_modified(prop, settings); + bool isUncommon = (flags & OBS_SERVICE_UNCOMMON) != 0; - ui->service->blockSignals(true); - ui->service->clear(); + QString name(obs_service_get_display_name(id)); - QStringList names; + if (showAll || !isUncommon) + ui->service->addItem(name, QT_UTF8(id)); - 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 (isUncommon && !showAll) + needShowAllOption = true; } - if (showAll) - names.sort(Qt::CaseInsensitive); - - for (QString &name : names) - ui->service->addItem(name); - - if (!showAll) { + if (needShowAllOption) { ui->service->addItem( QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); + QVariant(SHOW_ALL)); } - 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); - } + QSortFilterProxyModel *model = + new ServiceSortFilterProxyModel(ui->service); + model->setSourceModel(ui->service->model()); + // Combo's current model must be reparented, + // Otherwise QComboBox::setModel() will delete it + ui->service->model()->setParent(model); - obs_properties_destroy(props); + ui->service->setModel(model); - ui->service->blockSignals(false); + ui->service->model()->sort(0); } -void AutoConfigStreamPage::UpdateServerList() +void AutoConfigStreamPage::UpdateCompleted() { - QString serviceName = ui->service->currentText(); - bool showMore = ui->service->currentData().toInt() == - (int)ListOpt::ShowAll; + /* Assume ready if an URL is present, we can't add a specific behavior + * for each service. */ + const char *streamUrl = obs_service_get_connect_info( + tempService, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + + ready = (streamUrl != NULL && streamUrl[0] != '\0'); - if (showMore) { + emit completeChanged(); +} + +void AutoConfigStreamPage::on_service_currentIndexChanged(int) +{ + if (ui->service->currentData().toInt() == SHOW_ALL) { 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"); + OBSServiceAutoRelease oldService = + obs_service_get_ref(tempService.Get()); + + QString service = ui->service->currentData().toString(); + OBSDataAutoRelease newSettings = + obs_service_defaults(QT_TO_UTF8(service)); + tempService = obs_service_create_private( + QT_TO_UTF8(service), TEMP_SERVICE_NAME, newSettings); + + bool cancelChange = false; + if (!obs_service_get_protocol(tempService)) { + /* + * Cancel the change if the service happen to be without default protocol. + * + * This is better than generating dozens of obs_service_t to check + * if there is a default protocol while filling the combo box. + */ + OBSMessageBox::warning( + this, + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Title"), + QTStr("Basic.Settings.Stream.NoDefaultProtocol.Text") + .arg(ui->service->currentText())); + cancelChange = true; + } - ui->server->clear(); + if (cancelChange) { + tempService = obs_service_get_ref(oldService); + const char *id = obs_service_get_id(tempService); + uint32_t flags = obs_get_service_flags(id); + if ((flags & OBS_SERVICE_INTERNAL) != 0) { + QString name(obs_service_get_display_name(id)); + if ((flags & OBS_SERVICE_DEPRECATED) != 0) + name = QTStr("Basic.Settings.Stream.DeprecatedType") + .arg(name); + + ui->service->setPlaceholderText(name); + } + QSignalBlocker s(ui->service); + ui->service->setCurrentIndex( + ui->service->findData(QT_UTF8(id))); - 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); + return; } - obs_properties_destroy(props); + delete streamServiceProps; + streamServiceProps = CreateTempServicePropertyView(nullptr); + ui->serviceLayout->addWidget(streamServiceProps); + + ServiceChanged(); } -void AutoConfigStreamPage::UpdateCompleted() +OBSPropertiesView * +AutoConfigStreamPage::CreateTempServicePropertyView(obs_data_t *settings) { - if (ui->key->text().isEmpty()) { - ready = false; - } else { - bool custom = IsCustomService(); - if (custom) { - ready = !ui->customServer->text().isEmpty(); - } else { - ready = true; - } - } - emit completeChanged(); + OBSDataAutoRelease defaultSettings = + obs_service_defaults(obs_service_get_id(tempService)); + OBSPropertiesView *view; + + view = new OBSPropertiesView( + settings ? settings : defaultSettings.Get(), tempService, + (PropertiesReloadCallback)obs_service_properties, nullptr, + nullptr, 170); + ; + view->setFrameShape(QFrame::NoFrame); + view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + QObject::connect(view, &OBSPropertiesView::Changed, this, + &AutoConfigStreamPage::ServiceChanged); + + return view; } /* ------------------------------------------------------------------------- */ @@ -585,21 +528,16 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) 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); - 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(); + obs_service_t *service = main->GetService(); + streamPage = new AutoConfigStreamPage(service); setPage(StartPage, new AutoConfigStartPage()); setPage(VideoPage, new AutoConfigVideoPage()); @@ -614,79 +552,9 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent) 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); - - /* ----------------------------------------- */ - /* 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()); - int bitrate = config_get_int(main->Config(), "SimpleOutput", "VBitrate"); streamPage->ui->bitrate->setValue(bitrate); - streamPage->ServiceChanged(); TestHardwareEncoding(); if (!hardwareEncodingAvailable) { @@ -776,20 +644,13 @@ void AutoConfig::SaveStreamSettings() /* ---------------------------------- */ /* 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()); - obs_data_set_string(settings, "key", key.c_str()); + OBSDataAutoRelease settings = obs_service_get_settings(service); + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); - OBSServiceAutoRelease newService = obs_service_create( - service_id, "default_service", settings, hotkeyData); + OBSServiceAutoRelease newService = + obs_service_create(obs_service_get_id(service), + "default_service", settings, nullptr); if (!newService) return; diff --git a/UI/window-basic-auto-config.hpp b/UI/window-basic-auto-config.hpp index ddcd66530ff76b..bdcaee4bb680b7 100644 --- a/UI/window-basic-auto-config.hpp +++ b/UI/window-basic-auto-config.hpp @@ -12,6 +12,7 @@ #include #include #include +#include class Ui_AutoConfigStartPage; class Ui_AutoConfigVideoPage; @@ -19,6 +20,7 @@ class Ui_AutoConfigStreamPage; class Ui_AutoConfigTestPage; class AutoConfigStreamPage; +class OBSPropertiesView; class AutoConfig : public QWizard { Q_OBJECT @@ -76,7 +78,7 @@ class AutoConfig : public QWizard { std::string serviceName; std::string serverName; std::string server; - std::string key; + OBSServiceAutoRelease service = nullptr; bool hardwareEncodingAvailable = false; bool nvencAvailable = false; @@ -85,9 +87,7 @@ class AutoConfig : public QWizard { bool appleAvailable = false; int startingBitrate = 2500; - bool customServer = false; bool bandwidthTest = false; - bool twitchAuto = false; bool preferHighFPS = false; bool preferHardware = false; int specificFPSNum = 0; @@ -155,11 +155,18 @@ class AutoConfigStreamPage : public QWizardPage { QString lastService; bool ready = false; + OBSServiceAutoRelease tempService; + OBSPropertiesView *streamServiceProps = nullptr; + void LoadServices(bool showAll); - inline bool IsCustomService() const; + + OBSPropertiesView *CreateTempServicePropertyView(obs_data_t *settings); + +private slots: + void on_service_currentIndexChanged(int idx); public: - AutoConfigStreamPage(QWidget *parent = nullptr); + AutoConfigStreamPage(obs_service_t *service, QWidget *parent = nullptr); ~AutoConfigStreamPage(); virtual bool isComplete() const override; @@ -167,11 +174,7 @@ class AutoConfigStreamPage : public QWizardPage { virtual bool validatePage() override; public slots: - void on_show_clicked(); void ServiceChanged(); - void UpdateKeyLink(); - void UpdateMoreInfoLink(); - void UpdateServerList(); void UpdateCompleted(); }; @@ -226,9 +229,13 @@ class AutoConfigTestPage : public QWizardPage { address(address_) { } - }; - void GetServers(std::vector &servers); + inline ServerInfo(const char *address_) + : name(address_), + address(address_) + { + } + }; public: AutoConfigTestPage(QWidget *parent = nullptr); diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 66e6a7a8ebd637..51b33d47426b76 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -107,6 +107,7 @@ void OBSBasicSettings::SaveStream1Settings() SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); } +/* NOTE: Identical to AutoConfigStreamPage function except it shows deprecated services */ void OBSBasicSettings::LoadServices(bool showAll) { const char *id; From 7269ad171cf84f1450ec490a863a317edf406e7c Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 29 May 2023 16:09:10 +0200 Subject: [PATCH 18/65] build-aux,plugins: Add obs-services with its JSON Schemas --- .editorconfig | 8 + build-aux/README.md | 1 + build-aux/json-schema/codecDefs.json | 17 + build-aux/json-schema/obs-services.json | 60 + build-aux/json-schema/protocolDefs.json | 59 + build-aux/json-schema/service.json | 139 + plugins/CMakeLists.txt | 4 + plugins/obs-services/CMakeLists.txt | 42 + .../obs-services/cmake/macos/Info.plist.in | 28 + .../cmake/windows/obs-module.rc.in | 24 + plugins/obs-services/data/locale/en-US.ini | 10 + plugins/obs-services/generated/.clang-format | 3 + .../obs-services/generated/services-json.hpp | 350 +++ plugins/obs-services/json/schema | 1 + plugins/obs-services/json/services.json | 2425 +++++++++++++++++ plugins/obs-services/obs-services.cpp | 20 + plugins/obs-services/service-config.cpp | 114 + plugins/obs-services/service-config.hpp | 48 + .../obs-services/service-instance-info.cpp | 88 + plugins/obs-services/service-instance.cpp | 337 +++ plugins/obs-services/service-instance.hpp | 62 + plugins/obs-services/services-json-util.hpp | 40 + plugins/obs-services/services-manager.cpp | 84 + plugins/obs-services/services-manager.hpp | 23 + 24 files changed, 3987 insertions(+) create mode 100644 build-aux/json-schema/codecDefs.json create mode 100644 build-aux/json-schema/obs-services.json create mode 100644 build-aux/json-schema/protocolDefs.json create mode 100644 build-aux/json-schema/service.json create mode 100644 plugins/obs-services/CMakeLists.txt create mode 100644 plugins/obs-services/cmake/macos/Info.plist.in create mode 100644 plugins/obs-services/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-services/data/locale/en-US.ini create mode 100644 plugins/obs-services/generated/.clang-format create mode 100644 plugins/obs-services/generated/services-json.hpp create mode 120000 plugins/obs-services/json/schema create mode 100644 plugins/obs-services/json/services.json create mode 100644 plugins/obs-services/obs-services.cpp create mode 100644 plugins/obs-services/service-config.cpp create mode 100644 plugins/obs-services/service-config.hpp create mode 100644 plugins/obs-services/service-instance-info.cpp create mode 100644 plugins/obs-services/service-instance.cpp create mode 100644 plugins/obs-services/service-instance.hpp create mode 100644 plugins/obs-services/services-json-util.hpp create mode 100644 plugins/obs-services/services-manager.cpp create mode 100644 plugins/obs-services/services-manager.hpp diff --git a/.editorconfig b/.editorconfig index 2995eb3fa6a8fb..315f2be3c4d455 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,3 +45,11 @@ indent_size = 4 [*.py] indent_style = space indent_size = 4 + +[json-schema/*.json] +indent_style = space +indent_size = 4 + +[plugins/obs-services/json/**/*.json] +indent_style = space +indent_size = 4 diff --git a/build-aux/README.md b/build-aux/README.md index 9343a02198a9a3..66e71d702fdaed 100644 --- a/build-aux/README.md +++ b/build-aux/README.md @@ -3,6 +3,7 @@ This folder contains: - The Flatpak manifest used to build OBS Studio - The script `format-manifest.py` which format manifest JSON files +- JSON Schemas related to plugins ## OBS Studio Flatpak Manifest diff --git a/build-aux/json-schema/codecDefs.json b/build-aux/json-schema/codecDefs.json new file mode 100644 index 00000000000000..2ae63aa6aef618 --- /dev/null +++ b/build-aux/json-schema/codecDefs.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/codecDefs.json", + "title": "Codec Enums, Patterns and Properties", + "description": "Codec-related enums, patterns and properties used in OBS JSON Schemas", + "$comment": "Made to allow an easy way to add codec to schemas", + "$defs": { + "videoCodecEnum": { + "$comment": "Enumeration of video codecs", + "enum": ["h264","hevc","av1"] + }, + "audioCodecEnum": { + "$comment": "Enumeration of audio codecs", + "enum": ["aac","opus"] + } + } +} diff --git a/build-aux/json-schema/obs-services.json b/build-aux/json-schema/obs-services.json new file mode 100644 index 00000000000000..e24bbe33e2d1c5 --- /dev/null +++ b/build-aux/json-schema/obs-services.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/obs-service.json", + "title": "OBS Services Plugin JSON Schema", + "type": "object", + "properties": { + "services": { + "type": "array", + "items": { + "allOf": [ + { + "title": "Service", + "properties": { + "id": { + "type": "string", + "title": "Service ID", + "description": "Human readable identifier used to register the service in OBS\nMaking it human readable is meant to allow users to use it through scripts and plugins", + "pattern": "^[a-z0-9_\\-]+$", + "minLength": 1 + }, + "name": { + "type": "string", + "title": "Service Name", + "description": "Name of the streaming service. Will be displayed in the Service dropdown.", + "minLength": 1 + }, + "common": { + "type": "boolean", + "title": "Property reserved to OBS Project developers", + "description": "Whether or not the service is shown in the list before it is expanded to all services by the user.", + "default": false + } + }, + "$commit": "Forbid 'common' being set if false", + "if": { "properties": { "common": { "const": false } } }, + "then": { "properties": { "common": { "description": "This property is unneeded if set to false" , "const": true } } }, + "required": ["id","name"] + }, + { + "$comment": "Add base service JSON schema", + "$ref": "service.json" + }, + { + "$comment": "Add protocol properties", + "$ref": "protocolDefs.json#/$defs/protocolProperties" + } + ], + "unevaluatedProperties": false + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "$comment": "Support '$schema' without making it a proper property", + "if": { "not": { "maxProperties": 1 } }, + "then": { "properties": { "$schema": { "type": "string" } } }, + "required": ["services"], + "unevaluatedProperties": false +} diff --git a/build-aux/json-schema/protocolDefs.json b/build-aux/json-schema/protocolDefs.json new file mode 100644 index 00000000000000..bc4df9bb5d7bcc --- /dev/null +++ b/build-aux/json-schema/protocolDefs.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/protocolDefs.json", + "title": "Protocol Enums, Patterns and Properties", + "description": "Protocol-related enums, patterns and properties used in OBS JSON Schemas", + "$comment": "Made to allow an easy way to add protocol to schemas", + "$defs": { + "protocolEnum": { + "$comment": "Enumeration of protocols", + "enum": ["RTMP","RTMPS","HLS","SRT","RIST"] + }, + "protocolMapEnum": { + "$comment": "Enumeration of protocols (with '*' as any) that have various compatible codecs", + "enum": ["*","RTMP","RTMPS","HLS","SRT","RIST"] + }, + "serverPattern": { + "$comment": "Pattern to enforce a supported server URL", + "pattern": "^(rtmps?|https?|srt|rist)://" + }, + "protocolProperties": { + "$comment": "Per-protocol properties meant for obs-services schema", + "type": "object", + "properties": { + "SRT": { + "type": "object", + "title": "SRT Properties", + "description": "Properties related to the SRT protocol", + "properties": { + "stream_id": { + "type": "boolean", + "default": true + }, + "encrypt_passphrase": { + "type": "boolean", + "default": false + } + }, + "required": ["stream_id", "encrypt_passphrase"] + }, + "RIST": { + "type": "object", + "title": "RIST Properties", + "description": "Properties related to the RIST protocol", + "properties": { + "encrypt_passphrase": { + "type": "boolean", + "default": true + }, + "srp_username_password": { + "type": "boolean", + "default": false + } + }, + "required": ["encrypt_passphrase", "srp_username_password"] + } + } + } + } +} diff --git a/build-aux/json-schema/service.json b/build-aux/json-schema/service.json new file mode 100644 index 00000000000000..e76011369c8b47 --- /dev/null +++ b/build-aux/json-schema/service.json @@ -0,0 +1,139 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://obsproject.com/schemas/service.json", + "title": "OBS Service Object Schema", + "type": "object", + "properties": { + "more_info_link": { + "type": "string", + "description": "Link that provides additional info about the service, presented in the UI as a button next to the services dropdown.", + "format": "uri", + "$ref": "#/$defs/httpPattern", + "minLength": 1 + }, + "stream_key_link": { + "type": "string", + "description": "Link where a logged-in user can find the 'stream key', presented as a button alongside the stream key field.", + "format": "uri", + "$ref": "#/$defs/httpPattern", + "minLength": 1 + }, + "servers": { + "type": "array", + "description": "Array of server objects", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the server (e.g. location, primary/backup), displayed in the Server dropdown.", + "minLength": 1 + }, + "protocol": { + "type": "string", + "title": "Server Protocol", + "description": "Protocol used by the server. Required if the URI scheme can't help identify the streaming protocol (e.g. HLS).", + "$ref": "protocolDefs.json#/$defs/protocolEnum" + }, + "url": { + "type": "string", + "title": "Server URL", + "description": "URL of the ingest server.", + "format": "uri", + "$ref": "protocolDefs.json#/$defs/serverPattern", + "minLength": 1 + } + }, + "required": ["name","url","protocol"], + "unevaluatedProperties": false + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "supported_codecs": { + "type": "object", + "title": "Supported Codecs", + "description": "Codecs that are supported by the service.", + "properties": { + "video": { + "type": "object", + "title": "Supported Video Codecs", + "description": "Video codecs that are supported by the service.", + "propertyNames": { "$ref": "protocolDefs.json#/$defs/protocolMapEnum" }, + "properties": { + "*": { + "type": "array", + "title": "Any Protocol Supported Video Codec", + "description": "Video codecs that are supported by the service and any supported protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/videoCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "additionalProperties": { + "type": "array", + "title": "Protocol Supported Video Codec", + "description": "Video codecs that are supported by the service on this protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/videoCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "minProperties": 1, + "unevaluatedProperties": false + }, + "audio": { + "type": "object", + "title": "Supported Audio Codecs", + "description": "Audio codecs that are supported by the service.", + "propertyNames": { "$ref": "protocolDefs.json#/$defs/protocolMapEnum" }, + "properties": { + "*": { + "type": "array", + "title": "Any Protocol Supported Audio Codec", + "description": "Audio codecs that are supported by the service and any supported protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/audioCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + } + }, + "additionalProperties": { + "type": "array", + "title": "Protocol Supported Audio Codec", + "description": "Audio codecs that are supported by the service on this protocol.", + "items": { + "type": "string", + "$ref": "codecDefs.json#/$defs/audioCodecEnum" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "minProperties": 1, + "unevaluatedProperties": false + } + }, + "minProperties": 1, + "unevaluatedProperties": false + } + }, + "required": ["servers"], + "$defs": { + "httpPattern": { + "$comment": "Pattern to enforce HTTP(S) links", + "pattern": "^https?://" + } + } +} diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 38d3896cdd9c67..19c995a0ccabae 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -68,6 +68,9 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) add_subdirectory(obs-outputs) if(OS_WINDOWS) add_subdirectory(obs-qsv11) + endif() + add_subdirectory(obs-services) + if(OS_WINDOWS) add_subdirectory(obs-text) endif() add_subdirectory(obs-transitions) @@ -194,3 +197,4 @@ add_subdirectory(rtmp-services) add_subdirectory(text-freetype2) add_subdirectory(aja) add_subdirectory(obs-webrtc) +add_subdirectory(obs-services) diff --git a/plugins/obs-services/CMakeLists.txt b/plugins/obs-services/CMakeLists.txt new file mode 100644 index 00000000000000..03b9ff0cfeac13 --- /dev/null +++ b/plugins/obs-services/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +option(ENABLE_SERVICES_UPDATE "Checks for services update" ON) +if(ENABLE_SERVICES_UPDATE AND NOT TARGET OBS::file-updater) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/file-updater" "${CMAKE_BINARY_DIR}/deps/file-updater") +endif() + +find_package(nlohmann_json) + +add_library(obs-services MODULE) +add_library(OBS::obs-services ALIAS obs-services) + +target_sources( + obs-services + PRIVATE # cmake-format: sortable + generated/services-json.hpp + obs-services.cpp + service-config.cpp + service-config.hpp + service-instance-info.cpp + service-instance.cpp + service-instance.hpp + services-json-util.hpp + services-manager.cpp + services-manager.hpp) + +target_link_libraries(obs-services PRIVATE OBS::libobs nlohmann_json::nlohmann_json) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-services.rc) + target_sources(obs-services PRIVATE obs-services.rc) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_add_resource(obs-services "${CMAKE_CURRENT_SOURCE_DIR}/json/services.json" + "${OBS_DATA_DESTINATION}/obs-plugins/obs-services") + set_target_properties_obs(obs-services PROPERTIES FOLDER plugins PREFIX "") +else() + add_target_resource(obs-services json/services.json "obs-plugins/obs-services/") + set_target_properties(obs-services PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(obs-services) +endif() diff --git a/plugins/obs-services/cmake/macos/Info.plist.in b/plugins/obs-services/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..f0485d3a37b215 --- /dev/null +++ b/plugins/obs-services/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-services + CFBundleIdentifier + com.obsproject.obs-services + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + obs-services + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/obs-services/cmake/windows/obs-module.rc.in b/plugins/obs-services/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..391670bdb47b7b --- /dev/null +++ b/plugins/obs-services/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS Services" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-services" + VALUE "OriginalFilename", "obs-services" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-services/data/locale/en-US.ini b/plugins/obs-services/data/locale/en-US.ini new file mode 100644 index 00000000000000..96c672ff988f6b --- /dev/null +++ b/plugins/obs-services/data/locale/en-US.ini @@ -0,0 +1,10 @@ +Services.MoreInfo="More Info" +Services.Protocol="Protocol" +Services.Protocol.OnlyOne="The service uses %s as protocol" +Services.Server="Server" +Services.StreamID="Stream ID" +Services.StreamID.Key="Stream Key" +Services.GetStreamKey="Get Stream Key" +Services.Username="Username" +Services.Password="Password" +Services.EncryptPassphrase="Encryption Passphrase" diff --git a/plugins/obs-services/generated/.clang-format b/plugins/obs-services/generated/.clang-format new file mode 100644 index 00000000000000..6420a46881e054 --- /dev/null +++ b/plugins/obs-services/generated/.clang-format @@ -0,0 +1,3 @@ +Language: Cpp +SortIncludes: false +DisableFormat: true diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp new file mode 100644 index 00000000000000..9833141490aedb --- /dev/null +++ b/plugins/obs-services/generated/services-json.hpp @@ -0,0 +1,350 @@ +// To parse this JSON data, first install +// +// json.hpp https://github.com/nlohmann/json +// +// Then include this file, and then do +// +// ServicesJson data = nlohmann::json::parse(jsonString); + +#pragma once + +#include +#include + +#ifndef NLOHMANN_OPT_HELPER +#define NLOHMANN_OPT_HELPER +namespace nlohmann { + template + struct adl_serializer> { + static void to_json(json & j, const std::shared_ptr & opt) { + if (!opt) j = nullptr; else j = *opt; + } + + static std::shared_ptr from_json(const json & j) { + if (j.is_null()) return std::make_shared(); else return std::make_shared(j.get()); + } + }; + template + struct adl_serializer> { + static void to_json(json & j, const std::optional & opt) { + if (!opt) j = nullptr; else j = *opt; + } + + static std::optional from_json(const json & j) { + if (j.is_null()) return std::make_optional(); else return std::make_optional(j.get()); + } + }; +} +#endif + +namespace OBSServices { + using nlohmann::json; + + #ifndef NLOHMANN_UNTYPED_OBSServices_HELPER + #define NLOHMANN_UNTYPED_OBSServices_HELPER + inline json get_untyped(const json & j, const char * property) { + if (j.find(property) != j.end()) { + return j.at(property).get(); + } + return json(); + } + + inline json get_untyped(const json & j, std::string property) { + return get_untyped(j, property.data()); + } + #endif + + #ifndef NLOHMANN_OPTIONAL_OBSServices_HELPER + #define NLOHMANN_OPTIONAL_OBSServices_HELPER + template + inline std::shared_ptr get_heap_optional(const json & j, const char * property) { + auto it = j.find(property); + if (it != j.end() && !it->is_null()) { + return j.at(property).get>(); + } + return std::shared_ptr(); + } + + template + inline std::shared_ptr get_heap_optional(const json & j, std::string property) { + return get_heap_optional(j, property.data()); + } + template + inline std::optional get_stack_optional(const json & j, const char * property) { + auto it = j.find(property); + if (it != j.end() && !it->is_null()) { + return j.at(property).get>(); + } + return std::optional(); + } + + template + inline std::optional get_stack_optional(const json & j, std::string property) { + return get_stack_optional(j, property.data()); + } + #endif + + /** + * Properties related to the RIST protocol + */ + struct RistProperties { + bool encryptPassphrase; + bool srpUsernamePassword; + }; + + enum class ServerProtocol : int { HLS, RIST, RTMP, RTMPS, SRT }; + + struct Server { + /** + * Name of the server (e.g. location, primary/backup), displayed in the Server dropdown. + */ + std::string name; + /** + * Protocol used by the server. Required if the URI scheme can't help identify the streaming + * protocol (e.g. HLS). + */ + ServerProtocol protocol; + /** + * URL of the ingest server. + */ + std::string url; + }; + + /** + * Properties related to the SRT protocol + */ + struct SrtProperties { + bool encryptPassphrase; + bool streamId; + }; + + /** + * Audio codecs that are supported by the service on this protocol. + */ + enum class ProtocolSupportedAudioCodec : int { AAC, OPUS }; + + /** + * Video codecs that are supported by the service on this protocol. + */ + enum class ProtocolSupportedVideoCodec : int { AV1, H264, HEVC }; + + /** + * Codecs that are supported by the service. + */ + struct SupportedCodecs { + /** + * Audio codecs that are supported by the service. + */ + std::optional>> audio; + /** + * Video codecs that are supported by the service. + */ + std::optional>> video; + }; + + struct Service { + /** + * Whether or not the service is shown in the list before it is expanded to all services by + * the user. + */ + std::optional common; + /** + * Human readable identifier used to register the service in OBS + * Making it human readable is meant to allow users to use it through scripts and plugins + */ + std::string id; + /** + * Name of the streaming service. Will be displayed in the Service dropdown. + */ + std::string name; + /** + * Link that provides additional info about the service, presented in the UI as a button + * next to the services dropdown. + */ + std::optional moreInfoLink; + /** + * Array of server objects + */ + std::vector servers; + /** + * Link where a logged-in user can find the 'stream key', presented as a button alongside + * the stream key field. + */ + std::optional streamKeyLink; + /** + * Codecs that are supported by the service. + */ + std::optional supportedCodecs; + /** + * Properties related to the RIST protocol + */ + std::optional rist; + /** + * Properties related to the SRT protocol + */ + std::optional srt; + }; + + struct ServicesJson { + std::vector services; + }; +} + +namespace OBSServices { + void from_json(const json & j, RistProperties & x); + void to_json(json & j, const RistProperties & x); + + void from_json(const json & j, Server & x); + void to_json(json & j, const Server & x); + + void from_json(const json & j, SrtProperties & x); + void to_json(json & j, const SrtProperties & x); + + void from_json(const json & j, SupportedCodecs & x); + void to_json(json & j, const SupportedCodecs & x); + + void from_json(const json & j, Service & x); + void to_json(json & j, const Service & x); + + void from_json(const json & j, ServicesJson & x); + void to_json(json & j, const ServicesJson & x); + + void from_json(const json & j, ServerProtocol & x); + void to_json(json & j, const ServerProtocol & x); + + void from_json(const json & j, ProtocolSupportedAudioCodec & x); + void to_json(json & j, const ProtocolSupportedAudioCodec & x); + + void from_json(const json & j, ProtocolSupportedVideoCodec & x); + void to_json(json & j, const ProtocolSupportedVideoCodec & x); + + inline void from_json(const json & j, RistProperties& x) { + x.encryptPassphrase = j.at("encrypt_passphrase").get(); + x.srpUsernamePassword = j.at("srp_username_password").get(); + } + + inline void to_json(json & j, const RistProperties & x) { + j = json::object(); + j["encrypt_passphrase"] = x.encryptPassphrase; + j["srp_username_password"] = x.srpUsernamePassword; + } + + inline void from_json(const json & j, Server& x) { + x.name = j.at("name").get(); + x.protocol = j.at("protocol").get(); + x.url = j.at("url").get(); + } + + inline void to_json(json & j, const Server & x) { + j = json::object(); + j["name"] = x.name; + j["protocol"] = x.protocol; + j["url"] = x.url; + } + + inline void from_json(const json & j, SrtProperties& x) { + x.encryptPassphrase = j.at("encrypt_passphrase").get(); + x.streamId = j.at("stream_id").get(); + } + + inline void to_json(json & j, const SrtProperties & x) { + j = json::object(); + j["encrypt_passphrase"] = x.encryptPassphrase; + j["stream_id"] = x.streamId; + } + + inline void from_json(const json & j, SupportedCodecs& x) { + x.audio = get_stack_optional>>(j, "audio"); + x.video = get_stack_optional>>(j, "video"); + } + + inline void to_json(json & j, const SupportedCodecs & x) { + j = json::object(); + j["audio"] = x.audio; + j["video"] = x.video; + } + + inline void from_json(const json & j, Service& x) { + x.common = get_stack_optional(j, "common"); + x.id = j.at("id").get(); + x.name = j.at("name").get(); + x.moreInfoLink = get_stack_optional(j, "more_info_link"); + x.servers = j.at("servers").get>(); + x.streamKeyLink = get_stack_optional(j, "stream_key_link"); + x.supportedCodecs = get_stack_optional(j, "supported_codecs"); + x.rist = get_stack_optional(j, "RIST"); + x.srt = get_stack_optional(j, "SRT"); + } + + inline void to_json(json & j, const Service & x) { + j = json::object(); + j["common"] = x.common; + j["id"] = x.id; + j["name"] = x.name; + j["more_info_link"] = x.moreInfoLink; + j["servers"] = x.servers; + j["stream_key_link"] = x.streamKeyLink; + j["supported_codecs"] = x.supportedCodecs; + j["RIST"] = x.rist; + j["SRT"] = x.srt; + } + + inline void from_json(const json & j, ServicesJson& x) { + x.services = j.at("services").get>(); + } + + inline void to_json(json & j, const ServicesJson & x) { + j = json::object(); + j["services"] = x.services; + } + + inline void from_json(const json & j, ServerProtocol & x) { + if (j == "HLS") x = ServerProtocol::HLS; + else if (j == "RIST") x = ServerProtocol::RIST; + else if (j == "RTMP") x = ServerProtocol::RTMP; + else if (j == "RTMPS") x = ServerProtocol::RTMPS; + else if (j == "SRT") x = ServerProtocol::SRT; + else { throw std::runtime_error("Input JSON does not conform to schema!"); } + } + + inline void to_json(json & j, const ServerProtocol & x) { + switch (x) { + case ServerProtocol::HLS: j = "HLS"; break; + case ServerProtocol::RIST: j = "RIST"; break; + case ServerProtocol::RTMP: j = "RTMP"; break; + case ServerProtocol::RTMPS: j = "RTMPS"; break; + case ServerProtocol::SRT: j = "SRT"; break; + default: throw std::runtime_error("This should not happen"); + } + } + + inline void from_json(const json & j, ProtocolSupportedAudioCodec & x) { + if (j == "aac") x = ProtocolSupportedAudioCodec::AAC; + else if (j == "opus") x = ProtocolSupportedAudioCodec::OPUS; + else { throw std::runtime_error("Input JSON does not conform to schema!"); } + } + + inline void to_json(json & j, const ProtocolSupportedAudioCodec & x) { + switch (x) { + case ProtocolSupportedAudioCodec::AAC: j = "aac"; break; + case ProtocolSupportedAudioCodec::OPUS: j = "opus"; break; + default: throw std::runtime_error("This should not happen"); + } + } + + inline void from_json(const json & j, ProtocolSupportedVideoCodec & x) { + if (j == "av1") x = ProtocolSupportedVideoCodec::AV1; + else if (j == "h264") x = ProtocolSupportedVideoCodec::H264; + else if (j == "hevc") x = ProtocolSupportedVideoCodec::HEVC; + else { throw std::runtime_error("Input JSON does not conform to schema!"); } + } + + inline void to_json(json & j, const ProtocolSupportedVideoCodec & x) { + switch (x) { + case ProtocolSupportedVideoCodec::AV1: j = "av1"; break; + case ProtocolSupportedVideoCodec::H264: j = "h264"; break; + case ProtocolSupportedVideoCodec::HEVC: j = "hevc"; break; + default: throw std::runtime_error("This should not happen"); + } + } +} diff --git a/plugins/obs-services/json/schema b/plugins/obs-services/json/schema new file mode 120000 index 00000000000000..6a77f66c51098f --- /dev/null +++ b/plugins/obs-services/json/schema @@ -0,0 +1 @@ +../../../build-aux/json-schema \ No newline at end of file diff --git a/plugins/obs-services/json/services.json b/plugins/obs-services/json/services.json new file mode 100644 index 00000000000000..a6e29f15439ba3 --- /dev/null +++ b/plugins/obs-services/json/services.json @@ -0,0 +1,2425 @@ +{ + "$schema": "schema/obs-services.json", + "services": [ + { + "id": "loola", + "name": "Loola.tv", + "servers": [ + { + "name": "US East: Virginia", + "protocol": "RTMP", + "url": "rtmp://rtmp.loola.tv/push" + }, + { + "name": "EU Central: Germany", + "protocol": "RTMP", + "url": "rtmp://rtmp-eu.loola.tv/push" + }, + { + "name": "South America: Brazil", + "protocol": "RTMP", + "url": "rtmp://rtmp-sa.loola.tv/push" + }, + { + "name": "Asia/Pacific: Singapore", + "protocol": "RTMP", + "url": "rtmp://rtmp-sg.loola.tv/push" + }, + { + "name": "Middle East: Bahrain", + "protocol": "RTMP", + "url": "rtmp://rtmp-me.loola.tv/push" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "lovecast", + "name": "Lovecast", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live-a.lovecastapp.com:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "luzento", + "name": "Luzento.com - RTMP", + "stream_key_link": "https://cms.luzento.com/dashboard/stream-key?from=OBS", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://ingest.luzento.com/live" + }, + { + "name": "Primary (Test)", + "protocol": "RTMP", + "url": "rtmp://ingest.luzento.com/test" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "vimm", + "name": "VIMM", + "servers": [ + { + "name": "Europe: Frankfurt", + "protocol": "RTMP", + "url": "rtmp://eu.vimm.tv/live" + }, + { + "name": "North America: Montreal", + "protocol": "RTMP", + "url": "rtmp://us.vimm.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "web_tv", + "name": "Web.TV", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://live3.origins.web.tv/liveext" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "goodgame", + "name": "GoodGame.ru", + "servers": [ + { + "name": "Моscow", + "protocol": "RTMP", + "url": "rtmp://msk.goodgame.ru:1940/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "youstreamer", + "name": "YouStreamer", + "stream_key_link": "https://www.app.youstreamer.com/stream/", + "servers": [ + { + "name": "Moscow", + "protocol": "RTMP", + "url": "rtmp://push.youstreamer.com/in/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "vaughn_live", + "name": "Vaughn Live / iNSTAGIB", + "servers": [ + { + "name": "US: Chicago, IL", + "protocol": "RTMP", + "url": "rtmp://live-ord.vaughnsoft.net/live" + }, + { + "name": "US: Vint Hill, VA", + "protocol": "RTMP", + "url": "rtmp://live-iad.vaughnsoft.net/live" + }, + { + "name": "US: Denver, CO", + "protocol": "RTMP", + "url": "rtmp://live-den.vaughnsoft.net/live" + }, + { + "name": "US: New York, NY", + "protocol": "RTMP", + "url": "rtmp://live-nyc.vaughnsoft.net/live" + }, + { + "name": "US: Miami, FL", + "protocol": "RTMP", + "url": "rtmp://live-mia.vaughnsoft.net/live" + }, + { + "name": "US: Seattle, WA", + "protocol": "RTMP", + "url": "rtmp://live-sea.vaughnsoft.net/live" + }, + { + "name": "EU: Amsterdam, NL", + "protocol": "RTMP", + "url": "rtmp://live-ams.vaughnsoft.net/live" + }, + { + "name": "EU: London, UK", + "protocol": "RTMP", + "url": "rtmp://live-lhr.vaughnsoft.net/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "breakers_tv", + "name": "Breakers.TV", + "servers": [ + { + "name": "US: Chicago, IL", + "protocol": "RTMP", + "url": "rtmp://live-ord.vaughnsoft.net/live" + }, + { + "name": "US: Vint Hill, VA", + "protocol": "RTMP", + "url": "rtmp://live-iad.vaughnsoft.net/live" + }, + { + "name": "US: Denver, CO", + "protocol": "RTMP", + "url": "rtmp://live-den.vaughnsoft.net/live" + }, + { + "name": "US: New York, NY", + "protocol": "RTMP", + "url": "rtmp://live-nyc.vaughnsoft.net/live" + }, + { + "name": "US: Miami, FL", + "protocol": "RTMP", + "url": "rtmp://live-mia.vaughnsoft.net/live" + }, + { + "name": "US: Seattle, WA", + "protocol": "RTMP", + "url": "rtmp://live-sea.vaughnsoft.net/live" + }, + { + "name": "EU: Amsterdam, NL", + "protocol": "RTMP", + "url": "rtmp://live-ams.vaughnsoft.net/live" + }, + { + "name": "EU: London, UK", + "protocol": "RTMP", + "url": "rtmp://live-lhr.vaughnsoft.net/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "facebook", + "name": "Facebook Live", + "common": true, + "stream_key_link": "https://www.facebook.com/live/producer?ref=OBS", + "servers": [ + { + "name": "Default", + "protocol": "RTMPS", + "url": "rtmps://rtmp-api.facebook.com:443/rtmp/" + } + ], + "supported_codecs": { + "video": { + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "castr", + "name": "Castr.io", + "servers": [ + { + "name": "US-East (Chicago, IL)", + "protocol": "RTMP", + "url": "rtmp://cg.castr.io/static" + }, + { + "name": "US-East (New York, NY)", + "protocol": "RTMP", + "url": "rtmp://ny.castr.io/static" + }, + { + "name": "US-East (Miami, FL)", + "protocol": "RTMP", + "url": "rtmp://mi.castr.io/static" + }, + { + "name": "US-West (Seattle, WA)", + "protocol": "RTMP", + "url": "rtmp://se.castr.io/static" + }, + { + "name": "US-West (Los Angeles, CA)", + "protocol": "RTMP", + "url": "rtmp://la.castr.io/static" + }, + { + "name": "US-Central (Dallas, TX)", + "protocol": "RTMP", + "url": "rtmp://da.castr.io/static" + }, + { + "name": "NA-East (Toronto, CA)", + "protocol": "RTMP", + "url": "rtmp://qc.castr.io/static" + }, + { + "name": "SA (Sao Paulo, BR)", + "protocol": "RTMP", + "url": "rtmp://br.castr.io/static" + }, + { + "name": "EU-West (London, UK)", + "protocol": "RTMP", + "url": "rtmp://uk.castr.io/static" + }, + { + "name": "EU-Central (Frankfurt, DE)", + "protocol": "RTMP", + "url": "rtmp://fr.castr.io/static" + }, + { + "name": "Russia (Moscow)", + "protocol": "RTMP", + "url": "rtmp://ru.castr.io/static" + }, + { + "name": "Asia (Singapore)", + "protocol": "RTMP", + "url": "rtmp://sg.castr.io/static" + }, + { + "name": "Asia (India)", + "protocol": "RTMP", + "url": "rtmp://in.castr.io/static" + }, + { + "name": "Australia (Sydney)", + "protocol": "RTMP", + "url": "rtmp://au.castr.io/static" + }, + { + "name": "US Central", + "protocol": "RTMP", + "url": "rtmp://us-central.castr.io/static" + }, + { + "name": "US West", + "protocol": "RTMP", + "url": "rtmp://us-west.castr.io/static" + }, + { + "name": "US East", + "protocol": "RTMP", + "url": "rtmp://us-east.castr.io/static" + }, + { + "name": "US South", + "protocol": "RTMP", + "url": "rtmp://us-south.castr.io/static" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://south-am.castr.io/static" + }, + { + "name": "EU Central", + "protocol": "RTMP", + "url": "rtmp://eu-central.castr.io/static" + }, + { + "name": "Singapore", + "protocol": "RTMP", + "url": "rtmp://sg-central.castr.io/static" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "boomstream", + "name": "Boomstream", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live.boomstream.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "meridix", + "name": "Meridix Live Sports Platform", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://publish.meridix.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "afreecatv", + "name": "AfreecaTV", + "servers": [ + { + "name": "Asia : Korea", + "protocol": "RTMP", + "url": "rtmp://rtmpmanager-freecat.afreeca.tv/app" + }, + { + "name": "North America : US East", + "protocol": "RTMP", + "url": "rtmp://rtmp-esu.afreecatv.com/app" + }, + { + "name": "North America : US West", + "protocol": "RTMP", + "url": "rtmp://rtmp-wsu.afreecatv.com/app" + }, + { + "name": "South America : Brazil", + "protocol": "RTMP", + "url": "rtmp://rtmp-brz.afreecatv.com/app" + }, + { + "name": "Europe : UK", + "protocol": "RTMP", + "url": "rtmp://rtmp-uk.afreecatv.com/app" + }, + { + "name": "Asia : Singapore", + "protocol": "RTMP", + "url": "rtmp://rtmp-sgp.afreecatv.com/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "cam4", + "name": "CAM4", + "servers": [ + { + "name": "CAM4", + "protocol": "RTMP", + "url": "rtmp://origin.cam4.com/cam4-origin-live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "eplay", + "name": "ePlay", + "servers": [ + { + "name": "ePlay Primary", + "protocol": "RTMP", + "url": "rtmp://live.eplay.link/origin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "picarto", + "name": "Picarto", + "servers": [ + { + "name": "Autoselect closest server", + "protocol": "RTMP", + "url": "rtmp://live.us.picarto.tv/golive" + }, + { + "name": "Los Angeles, USA", + "protocol": "RTMP", + "url": "rtmp://live.us-losangeles.picarto.tv/golive" + }, + { + "name": "Dallas, USA", + "protocol": "RTMP", + "url": "rtmp://live.us-dallas.picarto.tv/golive" + }, + { + "name": "Miami, USA", + "protocol": "RTMP", + "url": "rtmp://live.us-miami.picarto.tv/golive" + }, + { + "name": "New York, USA", + "protocol": "RTMP", + "url": "rtmp://live.us-newyork.picarto.tv/golive" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://live.eu-west1.picarto.tv/golive" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "livestream", + "name": "Livestream", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://rtmpin.livestreamingest.com/rtmpin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "uscreen", + "name": "Uscreen", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://global-live.uscreen.app:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "stripchat", + "name": "Stripchat", + "servers": [ + { + "name": "Auto", + "protocol": "RTMP", + "url": "rtmp://live.doppiocdn.com/ext" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "camsoda", + "name": "CamSoda", + "servers": [ + { + "name": "North America", + "protocol": "RTMP", + "url": "rtmp://obs-ingest-na.livemediahost.com/cam_obs" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://obs-ingest-sa.livemediahost.com/cam_obs" + }, + { + "name": "Asia", + "protocol": "RTMP", + "url": "rtmp://obs-ingest-as.livemediahost.com/cam_obs" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://obs-ingest-eu.livemediahost.com/cam_obs" + }, + { + "name": "Oceania", + "protocol": "RTMP", + "url": "rtmp://obs-ingest-oc.livemediahost.com/cam_obs" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "chartubate", + "name": "Chaturbate", + "servers": [ + { + "name": "Global Main Fastest - Recommended", + "protocol": "RTMP", + "url": "rtmp://live.stream.highwebmedia.com/live-origin" + }, + { + "name": "Global Backup", + "protocol": "RTMP", + "url": "rtmp://live-backup.stream.highwebmedia.com/live-origin" + }, + { + "name": "US West: Seattle, WA", + "protocol": "RTMP", + "url": "rtmp://live-sea.stream.highwebmedia.com/live-origin" + }, + { + "name": "US West: Phoenix, AZ", + "protocol": "RTMP", + "url": "rtmp://live-phx.stream.highwebmedia.com/live-origin" + }, + { + "name": "US Central: Salt Lake City, UT", + "protocol": "RTMP", + "url": "rtmp://live-slc.stream.highwebmedia.com/live-origin" + }, + { + "name": "US Central: Chicago, IL", + "protocol": "RTMP", + "url": "rtmp://live-chi.stream.highwebmedia.com/live-origin" + }, + { + "name": "US East: Atlanta, GA", + "protocol": "RTMP", + "url": "rtmp://live-atl.stream.highwebmedia.com/live-origin" + }, + { + "name": "US East: Ashburn, VA", + "protocol": "RTMP", + "url": "rtmp://live-ash.stream.highwebmedia.com/live-origin" + }, + { + "name": "South America: Sao Paulo, Brazil", + "protocol": "RTMP", + "url": "rtmp://live-gru.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Amsterdam, NL", + "protocol": "RTMP", + "url": "rtmp://live-nld.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Alblasserdam, NL", + "protocol": "RTMP", + "url": "rtmp://live-alb.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Frankfurt, DE", + "protocol": "RTMP", + "url": "rtmp://live-fra.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Belgrade, Serbia", + "protocol": "RTMP", + "url": "rtmp://live-srb.stream.highwebmedia.com/live-origin" + }, + { + "name": "Asia: Singapore", + "protocol": "RTMP", + "url": "rtmp://live-sin.stream.highwebmedia.com/live-origin" + }, + { + "name": "Asia: Tokyo, Japan", + "protocol": "RTMP", + "url": "rtmp://live-nrt.stream.highwebmedia.com/live-origin" + }, + { + "name": "Australia: Sydney", + "protocol": "RTMP", + "url": "rtmp://live-syd.stream.highwebmedia.com/live-origin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "wpstream", + "name": "WpStream", + "more_info_link": "https://wpstream.net/obs-more-info", + "stream_key_link": "https://wpstream.net/obs-get-stream-key", + "servers": [ + { + "name": "Closest server - Automatic", + "protocol": "RTMP", + "url": "rtmp://ingest.wpstream.net/golive" + }, + { + "name": "North America", + "protocol": "RTMP", + "url": "rtmp://ingest-na.wpstream.net/golive" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://ingest-eu.wpstream.net/golive" + }, + { + "name": "Asia", + "protocol": "RTMP", + "url": "rtmp://ingest-as.wpstream.net/golive" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://ingest-sa.wpstream.net/golive" + }, + { + "name": "Australia & Oceania", + "protocol": "RTMP", + "url": "rtmp://ingest-au.wpstream.net/golive" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "twitter", + "name": "Twitter", + "common": true, + "stream_key_link": "https://studio.twitter.com/producer/sources", + "servers": [ + { + "name": "US West: California", + "protocol": "RTMP", + "url": "rtmp://ca.pscp.tv:80/x" + }, + { + "name": "US West: Oregon", + "protocol": "RTMP", + "url": "rtmp://or.pscp.tv:80/x" + }, + { + "name": "US East: Virginia", + "protocol": "RTMP", + "url": "rtmp://va.pscp.tv:80/x" + }, + { + "name": "South America: Brazil", + "protocol": "RTMP", + "url": "rtmp://br.pscp.tv:80/x" + }, + { + "name": "EU West: France", + "protocol": "RTMP", + "url": "rtmp://fr.pscp.tv:80/x" + }, + { + "name": "EU West: Ireland", + "protocol": "RTMP", + "url": "rtmp://ie.pscp.tv:80/x" + }, + { + "name": "EU Central: Germany", + "protocol": "RTMP", + "url": "rtmp://de.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Australia", + "protocol": "RTMP", + "url": "rtmp://au.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: India", + "protocol": "RTMP", + "url": "rtmp://in.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Japan", + "protocol": "RTMP", + "url": "rtmp://jp.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Korea", + "protocol": "RTMP", + "url": "rtmp://kr.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Singapore", + "protocol": "RTMP", + "url": "rtmp://sg.pscp.tv:80/x" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "switchboard_live", + "name": "Switchboard Live", + "servers": [ + { + "name": "Global - Recommended", + "protocol": "RTMPS", + "url": "rtmps://live.sb.zone:443/live" + }, + { + "name": "Global - Legacy", + "protocol": "RTMP", + "url": "rtmp://ingest-global.switchboard.zone/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ], + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "looch", + "name": "Looch", + "servers": [ + { + "name": "Primary Looch ingest server", + "protocol": "RTMP", + "url": "rtmp://ingest.looch.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "eventials", + "name": "Eventials", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://transmission.eventials.com/eventialsLiveOrigin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "eventlive", + "name": "EventLive.pro", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://go.eventlive.pro/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "lahzenegar", + "name": "Lahzenegar - StreamG | لحظه‌نگار - استریمجی", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://rtmp.lahzecdn.com/pro" + }, + { + "name": "Iran", + "protocol": "RTMP", + "url": "rtmp://rtmp-iran.lahzecdn.com/pro" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "mylive", + "name": "MyLive", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://stream.mylive.in.th/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "trovo", + "name": "Trovo", + "stream_key_link": "https://studio.trovo.live/mychannel/stream", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://livepush.trovo.live/live/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "mixcloud", + "name": "Mixcloud", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.mixcloud.com/broadcast" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "sermonaudio", + "name": "SermonAudio Cloud", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://webcast.sermonaudio.com/sa" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "vimeo", + "name": "Vimeo", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.cloud.vimeo.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "aparat", + "name": "Aparat", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.cdn.asset.aparat.com:443/event" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "kakaotv", + "name": "KakaoTV", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.play.kakao.com/kakaotv" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "piczel_tv", + "name": "Piczel.tv", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://piczel.tv:1935/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "stage_ten", + "name": "STAGE TEN", + "servers": [ + { + "name": "STAGE TEN", + "protocol": "RTMPS", + "url": "rtmps://app-rtmp.stageten.tv:443/stageten" + } + ], + "supported_codecs": { + "video": { + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "dlive", + "name": "DLive", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://stream.dlive.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "lightcast_com", + "name": "Lightcast.com", + "servers": [ + { + "name": "North America / East", + "protocol": "RTMP", + "url": "rtmp://us-east.live.lightcast.com/202E1F/default" + }, + { + "name": "North America / West", + "protocol": "RTMP", + "url": "rtmp://us-west.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Amsterdam", + "protocol": "RTMP", + "url": "rtmp://europe.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Frankfurt", + "protocol": "RTMP", + "url": "rtmp://europe-fra.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Stockholm", + "protocol": "RTMP", + "url": "rtmp://europe-sto.live.lightcast.com/202E1F/default" + }, + { + "name": "Asia / Hong Kong", + "protocol": "RTMP", + "url": "rtmp://asia.live.lightcast.com/202E1F/default" + }, + { + "name": "Australia / Sydney", + "protocol": "RTMP", + "url": "rtmp://australia.live.lightcast.com/202E1F/default" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "bongacams", + "name": "Bongacams", + "servers": [ + { + "name": "Automatic / Default", + "protocol": "RTMP", + "url": "rtmp://auto.origin.gnsbc.com:1934/live" + }, + { + "name": "Automatic / Backup", + "protocol": "RTMP", + "url": "rtmp://origin.bcvidorigin.com:1934/live" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://z-eu.origin.gnsbc.com:1934/live" + }, + { + "name": "North America", + "protocol": "RTMP", + "url": "rtmp://z-us.origin.gnsbc.com:1934/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "chathostess", + "name": "Chathostess", + "servers": [ + { + "name": "Chathostess - Backup", + "protocol": "RTMP", + "url": "rtmp://wowza05.foobarweb.com/cmschatsys_video" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "onlyfans", + "name": "OnlyFans.com", + "servers": [ + { + "name": "USA", + "protocol": "RTMP", + "url": "rtmp://route0.onlyfans.com/live" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://route0-dc2.onlyfans.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "steam", + "name": "Steam", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://ingest-rtmp.broadcast.steamcontent.com/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "konduit", + "name": "Konduit.live", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.konduit.live/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "loco", + "name": "LOCO", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://ivory-ingest.getloconow.com:1935/stream" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "niconico-premium", + "name": "niconico, premium member (ニコニコ生放送 プレミアム会員)", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://aliveorigin.dmc.nico/named_input" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "niconico-free", + "name": "niconico, free member (ニコニコ生放送 一般会員)", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://aliveorigin.dmc.nico/named_input" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "wasd_tv", + "name": "WASD.TV", + "servers": [ + { + "name": "Automatic", + "protocol": "RTMP", + "url": "rtmp://push.rtmp.wasd.tv/live" + }, + { + "name": "Russia, Moscow", + "protocol": "RTMP", + "url": "rtmp://ru-moscow.rtmp.wasd.tv/live" + }, + { + "name": "Germany, Frankfurt", + "protocol": "RTMP", + "url": "rtmp://de-frankfurt.rtmp.wasd.tv/live" + }, + { + "name": "Finland, Helsinki", + "protocol": "RTMP", + "url": "rtmp://fi-helsinki.rtmp.wasd.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "xlovecam", + "name": "XLoveCam.com", + "servers": [ + { + "name": "Europe(main)", + "protocol": "RTMP", + "url": "rtmp://nl.eu.stream.xlove.com/performer-origin" + }, + { + "name": "Europe(Romania)", + "protocol": "RTMP", + "url": "rtmp://ro.eu.stream.xlove.com/performer-origin" + }, + { + "name": "Europe(Russia)", + "protocol": "RTMP", + "url": "rtmp://ru.eu.stream.xlove.com/performer-origin" + }, + { + "name": "North America(US East)", + "protocol": "RTMP", + "url": "rtmp://usec.na.stream.xlove.com/performer-origin" + }, + { + "name": "North America(US West)", + "protocol": "RTMP", + "url": "rtmp://uswc.na.stream.xlove.com/performer-origin" + }, + { + "name": "North America(Canada)", + "protocol": "RTMP", + "url": "rtmp://ca.na.stream.xlove.com/performer-origin" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://co.sa.stream.xlove.com/performer-origin" + }, + { + "name": "Asia", + "protocol": "RTMP", + "url": "rtmp://sg.as.stream.xlove.com/performer-origin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "angelthump", + "name": "AngelThump", + "servers": [ + { + "name": "Auto", + "protocol": "RTMP", + "url": "rtmp://ingest.angelthump.com/live" + }, + { + "name": "New York 3", + "protocol": "RTMP", + "url": "rtmp://nyc-ingest.angelthump.com:1935/live" + }, + { + "name": "San Francisco 2", + "protocol": "RTMP", + "url": "rtmp://sfo-ingest.angelthump.com:1935/live" + }, + { + "name": "Singapore 1", + "protocol": "RTMP", + "url": "rtmp://sgp-ingest.angelthump.com:1935/live" + }, + { + "name": "London 1", + "protocol": "RTMP", + "url": "rtmp://lon-ingest.angelthump.com:1935/live" + }, + { + "name": "Frankfurt 1", + "protocol": "RTMP", + "url": "rtmp://fra-ingest.angelthump.com:1935/live" + }, + { + "name": "Toronto 1", + "protocol": "RTMP", + "url": "rtmp://tor-ingest.angelthump.com:1935/live" + }, + { + "name": "Amsterdam 3", + "protocol": "RTMP", + "url": "rtmp://ams-ingest.angelthump.com:1935/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "api_video", + "name": "api.video", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://broadcast.api.video/s" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "mux", + "name": "Mux", + "servers": [ + { + "name": "Global (RTMPS)", + "protocol": "RTMPS", + "url": "rtmps://global-live.mux.com:443/app" + }, + { + "name": "Global (RTMP)", + "protocol": "RTMP", + "url": "rtmp://global-live.mux.com:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ], + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "viloud", + "name": "Viloud", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live.viloud.tv:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "myfreecams", + "name": "MyFreeCams", + "servers": [ + { + "name": "Automatic", + "protocol": "RTMP", + "url": "rtmp://publish.myfreecams.com/NxServer" + }, + { + "name": "Australia", + "protocol": "RTMP", + "url": "rtmp://publish-syd.myfreecams.com/NxServer" + }, + { + "name": "East Asia", + "protocol": "RTMP", + "url": "rtmp://publish-tyo.myfreecams.com/NxServer" + }, + { + "name": "Europe (East)", + "protocol": "RTMP", + "url": "rtmp://publish-buh.myfreecams.com/NxServer" + }, + { + "name": "Europe (West)", + "protocol": "RTMP", + "url": "rtmp://publish-ams.myfreecams.com/NxServer" + }, + { + "name": "North America (East Coast)", + "protocol": "RTMP", + "url": "rtmp://publish-ord.myfreecams.com/NxServer" + }, + { + "name": "North America (West Coast)", + "protocol": "RTMP", + "url": "rtmp://publish-tuk.myfreecams.com/NxServer" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://publish-sao.myfreecams.com/NxServer" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "polystreamer", + "name": "PolyStreamer.com", + "servers": [ + { + "name": "Auto-select closest server", + "protocol": "RTMP", + "url": "rtmp://live.polystreamer.com/live" + }, + { + "name": "United States - West", + "protocol": "RTMP", + "url": "rtmp://us-west.live.polystreamer.com/live" + }, + { + "name": "United States - East", + "protocol": "RTMP", + "url": "rtmp://us-east.live.polystreamer.com/live" + }, + { + "name": "Australia", + "protocol": "RTMP", + "url": "rtmp://aus.live.polystreamer.com/live" + }, + { + "name": "India", + "protocol": "RTMP", + "url": "rtmp://ind.live.polystreamer.com/live" + }, + { + "name": "Germany", + "protocol": "RTMP", + "url": "rtmp://deu.live.polystreamer.com/live" + }, + { + "name": "Japan", + "protocol": "RTMP", + "url": "rtmp://jpn.live.polystreamer.com/live" + }, + { + "name": "Singapore", + "protocol": "RTMP", + "url": "rtmp://sgp.live.polystreamer.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "openrec_tv", + "name": "OPENREC.tv - Premium member (プレミアム会員)", + "stream_key_link": "https://www.openrec.tv/login?keep_login=true&url=https://www.openrec.tv/dashboard/live?from=obs", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://a.station.openrec.tv:1935/live1" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "nanostream", + "name": "nanoStream Cloud / bintu", + "more_info_link": "https://www.nanocosmos.de/obs", + "stream_key_link": "https://bintu-cloud-frontend.nanocosmos.de/organisation", + "servers": [ + { + "name": "bintu-stream global ingest (rtmp)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream.nanocosmos.de/live" + }, + { + "name": "bintu-stream global ingest (rtmps)", + "protocol": "RTMPS", + "url": "rtmps://bintu-stream.nanocosmos.de:1937/live" + }, + { + "name": "bintu-vtrans global ingest with transcoding/ABR (rtmp)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans global ingest with transcoding/ABR (rtmps)", + "protocol": "RTMPS", + "url": "rtmps://bintu-vtrans.nanocosmos.de:1937/live" + }, + { + "name": "bintu-stream Europe (EU)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream-eu.nanocosmos.de/live" + }, + { + "name": "bintu-stream USA West (USW)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream-usw.nanocosmos.de/live" + }, + { + "name": "bintu-stream US East (USE)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream-use.nanocosmos.de/live" + }, + { + "name": "bintu-stream Asia South (ASS)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream-ass.nanocosmos.de/live" + }, + { + "name": "bintu-stream Australia (AU)", + "protocol": "RTMP", + "url": "rtmp://bintu-stream-au.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Europe (EU)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans-eu.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans USA West (USW)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans-usw.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans US East (USE)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans-use.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Asia South (ASS)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans-ass.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Australia (AU)", + "protocol": "RTMP", + "url": "rtmp://bintu-vtrans-au.nanocosmos.de/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ], + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "bilibili", + "name": "Bilibili Live - RTMP | 哔哩哔哩直播 - RTMP", + "more_info_link": "https://link.bilibili.com/p/help/index?id=4#/tools-tutorial", + "stream_key_link": "https://link.bilibili.com/p/center/index#/my-room/start-live", + "servers": [ + { + "name": "Global - Primary | 全球 - 主要", + "protocol": "RTMP", + "url": "rtmp://live-push.bilivideo.com/live-bvc/" + }, + { + "name": "Non Chinese Mainland - Primary | 非中国大陆地区 - 主要", + "protocol": "RTMP", + "url": "rtmp://bdy.live-push.bilivideo.com/live-bvc/" + }, + { + "name": "Chinese Mainland - Backup | 中国大陆地区 - 备用", + "protocol": "RTMP", + "url": "rtmp://txy2.live-push.bilivideo.com/live-bvc/" + }, + { + "name": "Non Chinese Mainland - Backup | 非中国大陆地区 - 备用", + "protocol": "RTMP", + "url": "rtmp://txy.live-push.bilivideo.com/live-bvc/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "volume_com", + "name": "Volume.com", + "stream_key_link": "https://volume.com/b?show_key=1&webrtc=0", + "servers": [ + { + "name": "Default - Recommended", + "protocol": "RTMP", + "url": "rtmp://live.volume.com/live-origin" + }, + { + "name": "US - West", + "protocol": "RTMP", + "url": "rtmp://live-pdx.volume.com/live-origin" + }, + { + "name": "US - East", + "protocol": "RTMP", + "url": "rtmp://live-ash.volume.com/live-origin" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "boxcast", + "name": "BoxCast", + "stream_key_link": "https://dashboard.boxcast.com/#/sources", + "servers": [ + { + "name": "BoxCast", + "protocol": "RTMP", + "url": "rtmp://rtmp.boxcast.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "disciple", + "name": "Disciple Media", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.disciplemedia.com/b-fme" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "jio_games", + "name": "Jio Games", + "servers": [ + { + "name": "Primary", + "protocol": "RTMP", + "url": "rtmp://livepub1.api.engageapps.jio/live" + }, + { + "name": "Secondary", + "protocol": "RTMP", + "url": "rtmp://livepub2.api.engageapps.jio/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "kuaishou", + "name": "Kuaishou Live", + "stream_key_link": "https://studio.kuaishou.com/live/list", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://open-push.voip.yximgs.com/gifshow/" + }, + { + "name": "North America", + "protocol": "RTMP", + "url": "rtmp://tx.push.yximgs.com/live/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "ultreon", + "name": "Utreon", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live.utreon.com:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "autistici_org", + "name": "Autistici.org Live", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live.autistici.org/ingest" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "phonelivestreaming", + "name": "PhoneLiveStreaming", + "stream_key_link": "https://app.phonelivestreaming.com/media/rtmp", + "servers": [ + { + "name": "PhoneLiveStreaming", + "protocol": "RTMP", + "url": "rtmp://live.phonelivestreaming.com/live/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "manyvids", + "name": "ManyVids", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://rtmp.str.manyvids.com:1935/live_stream/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "fantasy_club", + "name": "Fantasy.Club", + "more_info_link": "https://help.fantasy.club/", + "stream_key_link": "https://fantasy.club/app/create-content/stream-now", + "servers": [ + { + "name": "US: East", + "protocol": "RTMP", + "url": "rtmp://live-east.fantasy.club/live" + }, + { + "name": "US: West", + "protocol": "RTMP", + "url": "rtmp://live-west.fantasy.club/live" + }, + { + "name": "Europe", + "protocol": "RTMP", + "url": "rtmp://live-eu.fantasy.club/live" + }, + { + "name": "South America", + "protocol": "RTMP", + "url": "rtmp://live-sa.fantasy.club/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "sympla", + "name": "Sympla", + "servers": [ + { + "name": "Sympla RTMP", + "protocol": "RTMP", + "url": "rtmp://rtmp.sympla.com.br:5222/app" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "mildom", + "name": "Mildom", + "more_info_link": "https://support.mildom.com/hc/ja/articles/360056569954", + "stream_key_link": "https://www.mildom.com/creator/live", + "servers": [ + { + "name": "Global", + "protocol": "RTMP", + "url": "rtmp://txlvb-push.mildom.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "nonolive", + "name": "Nonolive", + "more_info_link": "https://wia.nonolive.com/views/obs_assistant_tutorial.html", + "stream_key_link": "https://www.nonolive.com/room_setting", + "servers": [ + { + "name": "Asia: Hong Kong, China", + "protocol": "RTMP", + "url": "rtmp://live-hk-zl.nonolive.tv/live" + }, + { + "name": "Asia: Jakarta, Indonesia", + "protocol": "RTMP", + "url": "rtmp://live-jkt-zl.nonolive.tv/live" + }, + { + "name": "EU: Frankfurt, DE", + "protocol": "RTMP", + "url": "rtmp://live-fra-zl.nonolive.tv/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "streamvi", + "name": "StreamVi", + "stream_key_link": "https://streamvi.ru/settings", + "servers": [ + { + "name": "Default", + "protocol": "RTMP", + "url": "rtmp://live-default.streamvi.ru/live" + }, + { + "name": "Russia: Moscow", + "protocol": "RTMP", + "url": "rtmp://live-msk.streamvi.ru/live" + }, + { + "name": "EU Central: Germany", + "protocol": "RTMP", + "url": "rtmp://live-germany.streamvi.ru/live" + }, + { + "name": "Russia: Novosibirsk", + "protocol": "RTMP", + "url": "rtmp://live-novosib.streamvi.ru/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "livepush", + "name": "Livepush", + "more_info_link": "https://docs.livepush.io/en/articles/5065323-how-to-stream-live-from-obs-to-livepush", + "servers": [ + { + "name": "Livepush Global (Default)", + "protocol": "RTMP", + "url": "rtmp://dc-global.livepush.io/live" + }, + { + "name": "Chicago, US", + "protocol": "RTMP", + "url": "rtmp://us-central-ch.livepush.io/live" + }, + { + "name": "New York, US", + "protocol": "RTMP", + "url": "rtmp://us-east-ny.livepush.io/live" + }, + { + "name": "Los Angeles, US", + "protocol": "RTMP", + "url": "rtmp://us-west-la.livepush.io/live" + }, + { + "name": "Miami, US", + "protocol": "RTMP", + "url": "rtmp://us-south-mia.livepush.io/live" + }, + { + "name": "Dallas, US", + "protocol": "RTMP", + "url": "rtmp://us-central-dal.livepush.io/live" + }, + { + "name": "Montreal, CA", + "protocol": "RTMP", + "url": "rtmp://ca-central-mon.livepush.io/live" + }, + { + "name": "Toronto, CA", + "protocol": "RTMP", + "url": "rtmp://ca-south-tor.livepush.io/live" + }, + { + "name": "Sydney, AU", + "protocol": "RTMP", + "url": "rtmp://au-east-syd.livepush.io/live" + }, + { + "name": "London, UK", + "protocol": "RTMP", + "url": "rtmp://uk-central-ldn.livepush.io/live" + }, + { + "name": "Milan, Italy", + "protocol": "RTMP", + "url": "rtmp://it-north-mln.livepush.io/live" + }, + { + "name": "Paris, FR", + "protocol": "RTMP", + "url": "rtmp://fr-central-par.livepush.io/live" + }, + { + "name": "Singapore", + "protocol": "RTMP", + "url": "rtmp://as-southeast-sg.livepush.io/live" + }, + { + "name": "Bangalore, IN", + "protocol": "RTMP", + "url": "rtmp://in-south-blr.livepush.io/live" + }, + { + "name": "Turkiye", + "protocol": "RTMP", + "url": "rtmp://tr-north-ist.livepush.io/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "vindral", + "name": "Vindral", + "more_info_link": "https://docs.vindral.com/docs/vindral-cdn/", + "stream_key_link": "https://portal.cdn.vindral.com/channels", + "servers": [ + { + "name": "Global", + "protocol": "RTMPS", + "url": "rtmps://rtmp.global.cdn.vindral.com/publish" + } + ], + "supported_codecs": { + "video": { + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "whowhatch", + "name": "Whowatch (ふわっち)", + "more_info_link": "https://whowatch.tv/help/encoder", + "stream_key_link": "https://whowatch.tv/publish", + "servers": [ + { + "name": "default", + "protocol": "RTMP", + "url": "rtmp://live.whowatch.tv/live/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "irltoolkit", + "name": "IRLToolkit", + "stream_key_link": "https://irl.run/settings/ingest/", + "servers": [ + { + "name": "Global (Recommended)", + "protocol": "RTMP", + "url": "rtmp://stream.global.irl.run/ingest" + }, + { + "name": "Los Angeles, US", + "protocol": "RTMP", + "url": "rtmp://stream.lax.irl.run/ingest" + }, + { + "name": "New York, US", + "protocol": "RTMP", + "url": "rtmp://stream.ewr.irl.run/ingest" + }, + { + "name": "Rotterdam, NL", + "protocol": "RTMP", + "url": "rtmp://stream.rtm.irl.run/ingest" + }, + { + "name": "Singapore", + "protocol": "RTMP", + "url": "rtmp://stream.sin.irl.run/ingest" + }, + { + "name": "Tokyo, JP", + "protocol": "RTMP", + "url": "rtmp://stream.tyo.irl.run/ingest" + }, + { + "name": "Sydney, AU", + "protocol": "RTMP", + "url": "rtmp://stream.syd.irl.run/ingest" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "bitmovin", + "name": "Bitmovin", + "more_info_link": "https://developer.bitmovin.com/docs/overview", + "stream_key_link": "https://bitmovin.com/dashboard/streams?streamsTab=LIVE", + "servers": [ + { + "name": "Streams Live", + "protocol": "RTMP", + "url": "rtmp://live-input.bitmovin.com/streams" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "live_streamer_cafe", + "name": "Live Streamer Cafe", + "more_info_link": "https://livestreamercafe.com/help.php", + "stream_key_link": "https://livestreamercafe.com/profile.php", + "servers": [ + { + "name": "Live Streamer Cafe Server", + "protocol": "RTMP", + "url": "rtmp://tophicles.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "enchant_events", + "name": "Enchant.events", + "more_info_link": "https://docs.enchant.events/knowledge-base-y4pOb", + "servers": [ + { + "name": "Primary RTMPS", + "protocol": "RTMPS", + "url": "rtmps://stream.enchant.cloud:443/live" + } + ], + "supported_codecs": { + "video": { + "RTMPS": [ + "h264" + ] + } + } + }, + { + "id": "joystick_tv", + "name": "Joystick.TV", + "more_info_link": "https://support.joystick.tv/support/creator-support/setting-up-your-stream/", + "stream_key_link": "https://joystick.tv/stream-settings", + "servers": [ + { + "name": "RTMP", + "protocol": "RTMP", + "url": "rtmp://live.joystick.tv/live/" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + }, + { + "id": "livepeer_studio", + "name": "Livepeer Studio", + "more_info_link": "https://docs.livepeer.org/guides/developing/stream-via-obs", + "stream_key_link": "https://livepeer.studio/dashboard/streams", + "servers": [ + { + "name": "Global (RTMP)", + "protocol": "RTMP", + "url": "rtmp://rtmp.livepeer.com/live" + }, + { + "name": "Global (RTMP Primary)", + "protocol": "RTMP", + "url": "rtmp://rtmp-a.livepeer.com/live" + }, + { + "name": "Global (RTMP Backup)", + "protocol": "RTMP", + "url": "rtmp://rtmp-b.livepeer.com/live" + } + ], + "supported_codecs": { + "video": { + "RTMP": [ + "h264" + ] + } + } + } + ] +} diff --git a/plugins/obs-services/obs-services.cpp b/plugins/obs-services/obs-services.cpp new file mode 100644 index 00000000000000..a073c828c9660c --- /dev/null +++ b/plugins/obs-services/obs-services.cpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "services-manager.hpp" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-services", "en-US") + +bool obs_module_load(void) +{ + return ServicesManager::Initialize(); +} + +void obs_module_unload(void) +{ + ServicesManager::Finalize(); +} diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp new file mode 100644 index 00000000000000..6cf9601ae06431 --- /dev/null +++ b/plugins/obs-services/service-config.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "service-config.hpp" + +#include + +#include "services-json-util.hpp" + +ServiceConfig::ServiceConfig(obs_data_t *settings, obs_service_t *self) + : service(reinterpret_cast( + obs_service_get_type_data(self))) +{ + Update(settings); +} + +void ServiceConfig::Update(obs_data_t *settings) +{ + std::string newProtocol = obs_data_get_string(settings, "protocol"); + if (protocol.empty() || newProtocol != protocol) { + protocol = newProtocol; + + if (service->HasSupportedVideoCodecs()) + supportedVideoCodecs = + service->GetSupportedVideoCodecs(protocol); + + if (service->HasSupportedAudioCodecs()) + supportedAudioCodecs = + service->GetSupportedAudioCodecs(protocol); + } + + serverUrl = obs_data_get_string(settings, "server"); + + streamId = obs_data_get_string(settings, "stream_id"); + + username = obs_data_get_string(settings, "username"); + password = obs_data_get_string(settings, "password"); + + encryptPassphrase = obs_data_get_string(settings, "encrypt_passphrase"); +} + +const char *ServiceConfig::ConnectInfo(uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + if (!serverUrl.empty()) + return serverUrl.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + if (!streamId.empty()) + return streamId.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_USERNAME: + if (!username.empty()) + return username.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + if (!password.empty()) + return password.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: + if (!encryptPassphrase.empty()) + return encryptPassphrase.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + break; + } + + return nullptr; +} + +bool ServiceConfig::CanTryToConnect() +{ + if (serverUrl.empty()) + return false; + + switch (StdStringToServerProtocol(protocol)) { + case OBSServices::ServerProtocol::RTMP: + case OBSServices::ServerProtocol::RTMPS: + case OBSServices::ServerProtocol::HLS: + if (streamId.empty()) + return false; + break; + case OBSServices::ServerProtocol::RIST: { + OBSServices::RistProperties properties = + service->GetRISTProperties(); + + if (properties.encryptPassphrase && encryptPassphrase.empty()) + return false; + + if (properties.srpUsernamePassword && + (username.empty() || password.empty())) + return false; + + break; + } + + case OBSServices::ServerProtocol::SRT: { + OBSServices::SrtProperties properties = + service->GetSRTProperties(); + + if (properties.encryptPassphrase && encryptPassphrase.empty()) + return false; + + if (properties.streamId && streamId.empty()) + return false; + + break; + } + } + + return true; +} diff --git a/plugins/obs-services/service-config.hpp b/plugins/obs-services/service-config.hpp new file mode 100644 index 00000000000000..1afc5260d57aa3 --- /dev/null +++ b/plugins/obs-services/service-config.hpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "service-instance.hpp" + +class ServiceConfig { + const ServiceInstance *service; + + std::string protocol; + + BPtr supportedVideoCodecs; + BPtr supportedAudioCodecs; + + std::string serverUrl; + + std::string streamId; + + std::string username; + std::string password; + + std::string encryptPassphrase; + +public: + ServiceConfig(obs_data_t *settings, obs_service_t *self); + inline ~ServiceConfig(){}; + + void Update(obs_data_t *settings); + + const char *Protocol() { return protocol.c_str(); }; + + const char **SupportedVideoCodecs() + { + return (const char **)supportedVideoCodecs.Get(); + } + const char **SupportedAudioCodecs() + { + return (const char **)supportedAudioCodecs.Get(); + } + + const char *ConnectInfo(uint32_t type); + + bool CanTryToConnect(); +}; diff --git a/plugins/obs-services/service-instance-info.cpp b/plugins/obs-services/service-instance-info.cpp new file mode 100644 index 00000000000000..08b50ac0de8e02 --- /dev/null +++ b/plugins/obs-services/service-instance-info.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "service-instance.hpp" + +#include "service-config.hpp" + +const char *ServiceInstance::InfoGetName(void *typeData) +{ + if (typeData) + return reinterpret_cast(typeData)->GetName(); + return nullptr; +} + +void *ServiceInstance::InfoCreate(obs_data_t *settings, obs_service_t *service) +{ + return reinterpret_cast(new ServiceConfig(settings, service)); +} + +void ServiceInstance::InfoDestroy(void *data) +{ + if (data) + delete reinterpret_cast(data); +} + +void ServiceInstance::InfoUpdate(void *data, obs_data_t *settings) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + priv->Update(settings); +} + +const char *ServiceInstance::InfoGetConnectInfo(void *data, uint32_t type) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->ConnectInfo(type); + return nullptr; +} + +const char *ServiceInstance::InfoGetProtocol(void *data) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->Protocol(); + return nullptr; +} + +const char **ServiceInstance::InfoGetSupportedVideoCodecs(void *data) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->SupportedVideoCodecs(); + return nullptr; +} + +const char **ServiceInstance::InfoGetSupportedAudioCodecs(void *data) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->SupportedAudioCodecs(); + return nullptr; +} + +bool ServiceInstance::InfoCanTryToConnect(void *data) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->CanTryToConnect(); + return false; +} + +void ServiceInstance::InfoGetDefault2(void *typeData, obs_data_t *settings) +{ + if (typeData) + reinterpret_cast(typeData)->GetDefaults( + settings); +} + +obs_properties_t *ServiceInstance::InfoGetProperties2(void * /* data */, + void *typeData) +{ + if (typeData) + return reinterpret_cast(typeData) + ->GetProperties(); + return nullptr; +} diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp new file mode 100644 index 00000000000000..cb31639fc07b5b --- /dev/null +++ b/plugins/obs-services/service-instance.cpp @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "service-instance.hpp" + +#include + +#include "service-config.hpp" +#include "services-json-util.hpp" + +ServiceInstance::ServiceInstance(const OBSServices::Service &service_) + : service(service_) +{ + uint32_t flags = 0; + + /* Common can only be "set" if set to true in the schema */ + if (!service.common.value_or(false)) + flags |= OBS_SERVICE_UNCOMMON; + + /* Generate supported protocols string. + * A vector is used to check already added protocol because std::string's + * find() does not work well with protocols like RTMP and RTMPS*/ + std::vector addedProtocols; + std::string protocols; + for (size_t i = 0; i < service.servers.size(); i++) { + std::string protocol = + ServerProtocolToStdString(service.servers[i].protocol); + + bool alreadyAdded = false; + for (size_t i = 0; i < addedProtocols.size(); i++) + alreadyAdded |= (addedProtocols[i] == protocol); + + if (!alreadyAdded) { + if (!protocols.empty()) + protocols += ";"; + + protocols += protocol; + addedProtocols.push_back(protocol); + } + } + supportedProtocols = bstrdup(protocols.c_str()); + + /* Fill service info and register it */ + info.type_data = this; + info.id = service.id.c_str(); + + info.get_name = InfoGetName; + info.create = InfoCreate; + info.destroy = InfoDestroy; + info.update = InfoUpdate; + + info.get_connect_info = InfoGetConnectInfo; + + info.get_protocol = InfoGetProtocol; + + if (service.supportedCodecs.has_value()) { + if (service.supportedCodecs.value().video.has_value()) + info.get_supported_video_codecs = + InfoGetSupportedVideoCodecs; + + if (service.supportedCodecs.value().audio.has_value()) + info.get_supported_audio_codecs = + InfoGetSupportedAudioCodecs; + } + + info.can_try_to_connect = InfoCanTryToConnect; + + info.flags = flags; + + info.get_defaults2 = InfoGetDefault2; + info.get_properties2 = InfoGetProperties2; + + info.supported_protocols = supportedProtocols; + + obs_register_service(&info); +} + +OBSServices::RistProperties ServiceInstance::GetRISTProperties() const +{ + return service.rist.value_or(OBSServices::RistProperties{true, false}); +} + +OBSServices::SrtProperties ServiceInstance::GetSRTProperties() const +{ + return service.srt.value_or(OBSServices::SrtProperties{true, false}); +} + +static inline void AddSupportedVideoCodecs( + const std::map> + &map, + const std::string &key, std::string &codecs) +{ + if (map.count(key) == 0) + return; + + for (auto &codec : map.at(key)) { + if (!codecs.empty()) + codecs += ";"; + + codecs += SupportedVideoCodecToStdString(codec); + } +} + +char ** +ServiceInstance::GetSupportedVideoCodecs(const std::string &protocol) const +{ + std::string codecs; + auto *prtclCodecs = &service.supportedCodecs.value().video.value(); + + AddSupportedVideoCodecs(*prtclCodecs, "*", codecs); + AddSupportedVideoCodecs(*prtclCodecs, protocol, codecs); + + if (codecs.empty()) + return nullptr; + + return strlist_split(codecs.c_str(), ';', false); +} + +static inline void AddSupportedAudioCodecs( + const std::map> + &map, + const std::string &key, std::string &codecs) +{ + if (map.count(key) == 0) + return; + + for (auto &codec : map.at(key)) { + if (!codecs.empty()) + codecs += ";"; + + codecs += SupportedAudioCodecToStdString(codec); + } +} + +char ** +ServiceInstance::GetSupportedAudioCodecs(const std::string &protocol) const +{ + std::string codecs; + auto *prtclCodecs = &service.supportedCodecs.value().audio.value(); + + AddSupportedAudioCodecs(*prtclCodecs, "*", codecs); + AddSupportedAudioCodecs(*prtclCodecs, protocol, codecs); + + if (codecs.empty()) + return nullptr; + + return strlist_split(codecs.c_str(), ';', false); +} + +const char *ServiceInstance::GetName() +{ + return service.name.c_str(); +} + +void ServiceInstance::GetDefaults(obs_data_t *settings) +{ + std::string protocol = + ServerProtocolToStdString(service.servers[0].protocol); + obs_data_set_default_string(settings, "protocol", protocol.c_str()); + obs_data_set_default_string(settings, "server", + service.servers[0].url.c_str()); +} + +bool ModifiedProtocolCb(void *service_, obs_properties_t *props, + obs_property_t *, obs_data_t *settings) +{ + const OBSServices::Service *service = + reinterpret_cast(service_); + std::string protocol = obs_data_get_string(settings, "protocol"); + obs_property_t *p = obs_properties_get(props, "server"); + + if (protocol.empty()) + return false; + + OBSServices::ServerProtocol proto = StdStringToServerProtocol(protocol); + + obs_property_list_clear(p); + for (size_t i = 0; i < service->servers.size(); i++) { + const OBSServices::Server *server = &service->servers[i]; + if (server->protocol != proto) + continue; + + obs_property_list_add_string(p, server->name.c_str(), + server->url.c_str()); + } + + obs_property_t *propGetStreamKey = + obs_properties_get(props, "get_stream_key"); + + std::unordered_map properties; +#define ADD_TO_MAP(property) \ + properties.emplace(property, obs_properties_get(props, property)) + ADD_TO_MAP("stream_id"); + ADD_TO_MAP("username"); + ADD_TO_MAP("password"); + ADD_TO_MAP("encrypt_passphrase"); +#undef ADD_TO_MAP + + if (propGetStreamKey) + obs_property_set_visible(propGetStreamKey, false); + + for (auto const &[key, val] : properties) + obs_property_set_visible(val, false); + + switch (proto) { + case OBSServices::ServerProtocol::RTMP: + case OBSServices::ServerProtocol::RTMPS: + case OBSServices::ServerProtocol::HLS: + obs_property_set_description( + properties["stream_id"], + obs_module_text("Services.StreamID.Key")); + obs_property_set_visible(properties["stream_id"], true); + if (propGetStreamKey) + obs_property_set_visible(propGetStreamKey, true); + break; + case OBSServices::ServerProtocol::SRT: { + bool hasProps = service->srt.has_value(); + obs_property_set_description( + properties["stream_id"], + obs_module_text("Services.StreamID")); + obs_property_set_visible(properties["stream_id"], + hasProps ? service->srt->streamId + : true); + obs_property_set_visible( + properties["encrypt_passphrase"], + hasProps ? service->srt->encryptPassphrase : false); + break; + } + case OBSServices::ServerProtocol::RIST: { + bool hasProps = service->rist.has_value(); + obs_property_set_visible( + properties["encrypt_passphrase"], + hasProps ? service->rist->encryptPassphrase : true); + obs_property_set_visible( + properties["username"], + hasProps ? service->rist->srpUsernamePassword : false); + obs_property_set_visible( + properties["password"], + hasProps ? service->rist->srpUsernamePassword : false); + break; + } + } + + return true; +} + +obs_properties_t *ServiceInstance::GetProperties() +{ + obs_properties_t *ppts = obs_properties_create(); + obs_property_t *p; + obs_property_t *uniqueProtocol; + + if (service.moreInfoLink) { + BPtr url = bstrdup(service.moreInfoLink->c_str()); + + p = obs_properties_add_button( + ppts, "more_info", obs_module_text("Services.MoreInfo"), + nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, url); + } + + p = obs_properties_add_list(ppts, "protocol", + obs_module_text("Services.Protocol"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + uniqueProtocol = obs_properties_add_text(ppts, "unique_protocol", + "placeholder", OBS_TEXT_INFO); + obs_property_set_visible(uniqueProtocol, false); + + obs_properties_add_list(ppts, "server", + obs_module_text("Services.Server"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + for (size_t i = 0; i < service.servers.size(); i++) { + std::string protocol = + ServerProtocolToStdString(service.servers[i].protocol); + + if (protocol.empty() || + !obs_is_output_protocol_registered(protocol.c_str())) + continue; + + bool alreadyListed = false; + + for (size_t i = 0; i < obs_property_list_item_count(p); i++) + alreadyListed |= + (strcmp(obs_property_list_item_string(p, i), + protocol.c_str()) == 0); + + if (!alreadyListed) + obs_property_list_add_string(p, protocol.c_str(), + protocol.c_str()); + } + + if (obs_property_list_item_count(p) == 1) { + DStr info; + dstr_catf(info, obs_module_text("Services.Protocol.OnlyOne"), + obs_property_list_item_string(p, 0)); + + obs_property_set_visible(p, false); + obs_property_set_visible(uniqueProtocol, true); + obs_property_set_description(uniqueProtocol, info); + } + + obs_property_set_modified_callback2(p, ModifiedProtocolCb, + (void *)&service); + + obs_properties_add_text(ppts, "stream_id", + obs_module_text("Services.StreamID"), + OBS_TEXT_PASSWORD); + + if (service.streamKeyLink) { + BPtr url = bstrdup(service.streamKeyLink->c_str()); + + p = obs_properties_add_button( + ppts, "get_stream_key", + obs_module_text("Services.GetStreamKey"), nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, url); + } + + obs_properties_add_text(ppts, "username", + obs_module_text("Services.Username"), + OBS_TEXT_DEFAULT); + obs_properties_add_text(ppts, "password", + obs_module_text("Services.Password"), + OBS_TEXT_PASSWORD); + obs_properties_add_text(ppts, "encrypt_passphrase", + obs_module_text("Services.EncryptPassphrase"), + OBS_TEXT_PASSWORD); + + return ppts; +} diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp new file mode 100644 index 00000000000000..b322ccbc0b9cef --- /dev/null +++ b/plugins/obs-services/service-instance.hpp @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "generated/services-json.hpp" + +class ServiceInstance { + obs_service_info info = {0}; + + const OBSServices::Service service; + + BPtr supportedProtocols; + + static const char *InfoGetName(void *typeData); + static void *InfoCreate(obs_data_t *settings, obs_service_t *service); + static void InfoDestroy(void *data); + static void InfoUpdate(void *data, obs_data_t *settings); + + static const char *InfoGetConnectInfo(void *data, uint32_t type); + + static const char *InfoGetProtocol(void *data); + + static const char **InfoGetSupportedVideoCodecs(void *data); + static const char **InfoGetSupportedAudioCodecs(void *data); + + static bool InfoCanTryToConnect(void *data); + + static void InfoGetDefault2(void *type_data, obs_data_t *settings); + static obs_properties_t *InfoGetProperties2(void *data, void *typeData); + +public: + ServiceInstance(const OBSServices::Service &service); + inline ~ServiceInstance(){}; + + OBSServices::RistProperties GetRISTProperties() const; + OBSServices::SrtProperties GetSRTProperties() const; + + bool HasSupportedVideoCodecs() const + { + return service.supportedCodecs.has_value() && + service.supportedCodecs->video.has_value(); + } + bool HasSupportedAudioCodecs() const + { + return service.supportedCodecs.has_value() && + service.supportedCodecs->audio.has_value(); + } + + char **GetSupportedVideoCodecs(const std::string &protocol) const; + char **GetSupportedAudioCodecs(const std::string &protocol) const; + + const char *GetName(); + + void GetDefaults(obs_data_t *settings); + + obs_properties_t *GetProperties(); +}; diff --git a/plugins/obs-services/services-json-util.hpp b/plugins/obs-services/services-json-util.hpp new file mode 100644 index 00000000000000..f66cabe82063e9 --- /dev/null +++ b/plugins/obs-services/services-json-util.hpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "generated/services-json.hpp" + +static inline std::string +ServerProtocolToStdString(const OBSServices::ServerProtocol &protocol) +{ + nlohmann::json j; + OBSServices::to_json(j, protocol); + return j.get(); +} + +static inline OBSServices::ServerProtocol +StdStringToServerProtocol(const std::string &protocol) +{ + nlohmann::json j = protocol; + OBSServices::ServerProtocol prtcl; + OBSServices::from_json(j, prtcl); + return prtcl; +} + +static inline std::string SupportedVideoCodecToStdString( + const OBSServices::ProtocolSupportedVideoCodec &codec) +{ + nlohmann::json j; + OBSServices::to_json(j, codec); + return j.get(); +} + +static inline std::string SupportedAudioCodecToStdString( + const OBSServices::ProtocolSupportedAudioCodec &codec) +{ + nlohmann::json j; + OBSServices::to_json(j, codec); + return j.get(); +} diff --git a/plugins/obs-services/services-manager.cpp b/plugins/obs-services/services-manager.cpp new file mode 100644 index 00000000000000..538ba1c1b8d055 --- /dev/null +++ b/plugins/obs-services/services-manager.cpp @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "services-manager.hpp" + +#include + +std::shared_ptr manager = nullptr; + +bool ServicesManager::RegisterServices() +{ + char *file = obs_module_config_path("services.json"); + std::ifstream servicesFile(file); + OBSServices::ServicesJson json; + bool fallback = false; + + bfree(file); + + if (servicesFile.is_open()) { + try { + json = nlohmann::json::parse(servicesFile); + } catch (nlohmann::json::exception &e) { + blog(LOG_WARNING, "[obs-services] %s", e.what()); + blog(LOG_WARNING, + "[obs-services] [RegisterServices] config path services file could not be parsed"); + fallback = true; + } + } else { + blog(LOG_DEBUG, "[obs-services] [RegisterServices] " + "config path services file not found"); + fallback = true; + } + + if (fallback) { + file = obs_module_file("services.json"); + servicesFile.open(file); + bfree(file); + + if (!servicesFile.is_open()) { + blog(LOG_ERROR, "[obs-services] [RegisterServices] " + "services file not found"); + return false; + } + + try { + json = nlohmann::json::parse(servicesFile); + } catch (nlohmann::json::exception &e) { + blog(LOG_ERROR, "[obs-services] %s", e.what()); + blog(LOG_ERROR, "[obs-services] [RegisterServices] " + "services file could not be parsed"); + return false; + } + } + + for (size_t i = 0; i < json.services.size(); i++) { + const char *id = json.services[i].id.c_str(); + + services.push_back( + std::make_shared(json.services[i])); + + blog(LOG_DEBUG, + "[obs-services] [RegisterServices] " + "registered service with id : %s", + id); + } + + servicesFile.close(); + return true; +} + +bool ServicesManager::Initialize() +{ + if (!manager) { + manager = std::make_shared(); + return manager->RegisterServices(); + } + return true; +} + +void ServicesManager::Finalize() +{ + manager.reset(); +} diff --git a/plugins/obs-services/services-manager.hpp b/plugins/obs-services/services-manager.hpp new file mode 100644 index 00000000000000..dc815e53447634 --- /dev/null +++ b/plugins/obs-services/services-manager.hpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "service-instance.hpp" + +class ServicesManager { + std::vector> services; + +public: + ServicesManager(){}; + inline ~ServicesManager() { services.clear(); }; + + bool RegisterServices(); + + static bool Initialize(); + static void Finalize(); +}; From 316371f5684f7e950e8772d0067cb180ac41a628 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Tue, 30 May 2023 12:59:36 +0200 Subject: [PATCH 19/65] obs-services,json-schema: Add format version --- build-aux/json-schema/obs-services.json | 8 +++++-- plugins/obs-services/CMakeLists.txt | 5 +++++ .../obs-services/generated/services-json.hpp | 3 +++ plugins/obs-services/json/services.json | 1 + plugins/obs-services/services-manager.cpp | 21 +++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/build-aux/json-schema/obs-services.json b/build-aux/json-schema/obs-services.json index e24bbe33e2d1c5..ccff171c445360 100644 --- a/build-aux/json-schema/obs-services.json +++ b/build-aux/json-schema/obs-services.json @@ -4,6 +4,10 @@ "title": "OBS Services Plugin JSON Schema", "type": "object", "properties": { + "format_version": { + "type": "integer", + "const": 1 + }, "services": { "type": "array", "items": { @@ -53,8 +57,8 @@ } }, "$comment": "Support '$schema' without making it a proper property", - "if": { "not": { "maxProperties": 1 } }, + "if": { "not": { "maxProperties": 2 } }, "then": { "properties": { "$schema": { "type": "string" } } }, - "required": ["services"], + "required": ["format_version", "services"], "unevaluatedProperties": false } diff --git a/plugins/obs-services/CMakeLists.txt b/plugins/obs-services/CMakeLists.txt index 03b9ff0cfeac13..bdbf02197922c6 100644 --- a/plugins/obs-services/CMakeLists.txt +++ b/plugins/obs-services/CMakeLists.txt @@ -26,6 +26,11 @@ target_sources( target_link_libraries(obs-services PRIVATE OBS::libobs nlohmann_json::nlohmann_json) +file(STRINGS json/services.json _format_version REGEX "^.*\"format_version\":[ \t]+[0-9]+,$") +string(REGEX REPLACE "^.*\"format_version\":[ \t]+([0-9]+),$" "\\1" JSON_FORMAT_VER "${_format_version}") + +target_compile_definitions(obs-services PRIVATE JSON_FORMAT_VER=${JSON_FORMAT_VER}) + if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-services.rc) target_sources(obs-services PRIVATE obs-services.rc) diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp index 9833141490aedb..015b8a639ad14f 100644 --- a/plugins/obs-services/generated/services-json.hpp +++ b/plugins/obs-services/generated/services-json.hpp @@ -186,6 +186,7 @@ namespace OBSServices { }; struct ServicesJson { + int64_t formatVersion; std::vector services; }; } @@ -290,11 +291,13 @@ namespace OBSServices { } inline void from_json(const json & j, ServicesJson& x) { + x.formatVersion = j.at("format_version").get(); x.services = j.at("services").get>(); } inline void to_json(json & j, const ServicesJson & x) { j = json::object(); + j["format_version"] = x.formatVersion; j["services"] = x.services; } diff --git a/plugins/obs-services/json/services.json b/plugins/obs-services/json/services.json index a6e29f15439ba3..60fdce67be11a8 100644 --- a/plugins/obs-services/json/services.json +++ b/plugins/obs-services/json/services.json @@ -1,5 +1,6 @@ { "$schema": "schema/obs-services.json", + "format_version": 1, "services": [ { "id": "loola", diff --git a/plugins/obs-services/services-manager.cpp b/plugins/obs-services/services-manager.cpp index 538ba1c1b8d055..77be2e33948a03 100644 --- a/plugins/obs-services/services-manager.cpp +++ b/plugins/obs-services/services-manager.cpp @@ -5,6 +5,9 @@ #include "services-manager.hpp" #include +#include + +constexpr int OBS_SERVICES_FORMAT_VER = JSON_FORMAT_VER; std::shared_ptr manager = nullptr; @@ -26,6 +29,15 @@ bool ServicesManager::RegisterServices() "[obs-services] [RegisterServices] config path services file could not be parsed"); fallback = true; } + + if (json.formatVersion != OBS_SERVICES_FORMAT_VER) { + blog(LOG_WARNING, + "[obs-services] [RegisterServices] " + "config path services file format version mismatch (file: %" PRId64 + ", plugin: %d)", + json.formatVersion, OBS_SERVICES_FORMAT_VER); + fallback = true; + } } else { blog(LOG_DEBUG, "[obs-services] [RegisterServices] " "config path services file not found"); @@ -51,6 +63,15 @@ bool ServicesManager::RegisterServices() "services file could not be parsed"); return false; } + + if (json.formatVersion != OBS_SERVICES_FORMAT_VER) { + blog(LOG_ERROR, + "[obs-services] [RegisterServices] " + "services file format version mismatch (file: %" PRId64 + ", plugin: %d)", + json.formatVersion, OBS_SERVICES_FORMAT_VER); + return false; + } } for (size_t i = 0; i < json.services.size(); i++) { From 15e27393f41ea43923df20a090c5bf9c15cb9e21 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 31 May 2023 14:33:07 +0200 Subject: [PATCH 20/65] plugins: Add custom-services --- plugins/CMakeLists.txt | 2 + plugins/custom-services/CMakeLists.txt | 22 ++ .../custom-services/cmake/macos/Info.plist.in | 28 ++ .../cmake/windows/obs-module.rc.in | 24 ++ plugins/custom-services/custom-hls.c | 138 ++++++++ plugins/custom-services/custom-rist.c | 150 ++++++++ plugins/custom-services/custom-service.c | 323 ++++++++++++++++++ plugins/custom-services/custom-services.c | 33 ++ plugins/custom-services/custom-srt.c | 139 ++++++++ plugins/custom-services/data/locale/en-US.ini | 15 + plugins/custom-services/rtmp-services.c | 176 ++++++++++ 11 files changed, 1050 insertions(+) create mode 100644 plugins/custom-services/CMakeLists.txt create mode 100644 plugins/custom-services/cmake/macos/Info.plist.in create mode 100644 plugins/custom-services/cmake/windows/obs-module.rc.in create mode 100644 plugins/custom-services/custom-hls.c create mode 100644 plugins/custom-services/custom-rist.c create mode 100644 plugins/custom-services/custom-service.c create mode 100644 plugins/custom-services/custom-services.c create mode 100644 plugins/custom-services/custom-srt.c create mode 100644 plugins/custom-services/data/locale/en-US.ini create mode 100644 plugins/custom-services/rtmp-services.c diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 19c995a0ccabae..4f13f30c67230c 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -38,6 +38,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) if(OS_WINDOWS OR OS_MACOS) add_subdirectory(coreaudio-encoder) endif() + add_subdirectory(custom-services) if(OS_WINDOWS OR OS_MACOS OR OS_LINUX) @@ -198,3 +199,4 @@ add_subdirectory(text-freetype2) add_subdirectory(aja) add_subdirectory(obs-webrtc) add_subdirectory(obs-services) +add_subdirectory(custom-services) diff --git a/plugins/custom-services/CMakeLists.txt b/plugins/custom-services/CMakeLists.txt new file mode 100644 index 00000000000000..238ef1ca1688c6 --- /dev/null +++ b/plugins/custom-services/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(custom-services MODULE) +add_library(OBS::custom-services ALIAS custom-services) + +target_sources( + custom-services PRIVATE # cmake-format: sortable + custom-hls.c custom-rist.c custom-service.c custom-services.c custom-srt.c rtmp-services.c) + +target_link_libraries(custom-services PRIVATE OBS::libobs) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in custom-services.rc) + target_sources(custom-services PRIVATE custom-services.rc) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs(custom-services PROPERTIES FOLDER plugins PREFIX "") +else() + set_target_properties(custom-services PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(custom-services) +endif() diff --git a/plugins/custom-services/cmake/macos/Info.plist.in b/plugins/custom-services/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..e788b842cf688d --- /dev/null +++ b/plugins/custom-services/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + custom-services + CFBundleIdentifier + com.obsproject.custom-services + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + custom-services + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/custom-services/cmake/windows/obs-module.rc.in b/plugins/custom-services/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..be746a15be3dc0 --- /dev/null +++ b/plugins/custom-services/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS Custom Services" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "custom-services" + VALUE "OriginalFilename", "custom-services" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/custom-services/custom-hls.c b/plugins/custom-services/custom-hls.c new file mode 100644 index 00000000000000..b9c825101c582b --- /dev/null +++ b/plugins/custom-services/custom-hls.c @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct hls_service { + char *server; + char *stream_key; +}; + +static const char *hls_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomHLS"); +} + +static void hls_service_update(void *data, obs_data_t *settings) +{ + struct hls_service *service = data; + + bfree(service->server); + bfree(service->stream_key); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(stream_key); + +#undef GET_STRING_SETTINGS +} + +static void hls_service_destroy(void *data) +{ + struct hls_service *service = data; + + bfree(service->server); + bfree(service->stream_key); + bfree(service); +} + +static void *hls_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct hls_service *data = bzalloc(sizeof(struct hls_service)); + hls_service_update(data, settings); + + return data; +} + +static const char *hls_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "HLS"; +} + +static const char *hls_service_connect_info(void *data, uint32_t type) +{ + struct hls_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return !service->stream_key ? "" : service->stream_key; + default: + return NULL; + } +} + +enum obs_service_audio_track_cap hls_service_audio_track_cap(void *data) +{ + UNUSED_PARAMETER(data); + + return OBS_SERVICE_AUDIO_MULTI_TRACK; +} + +bool hls_service_can_try_to_connect(void *data) +{ + struct hls_service *service = data; + + if (!service->server) + return false; + + /* Require stream key */ + if (!service->stream_key) + return false; + + return true; +} + +static obs_properties_t *hls_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add warning about how HLS stream key works */ + obs_property_t *p = obs_properties_add_text( + ppts, "hls_warning", + obs_module_text("CustomServices.HLS.Warning"), OBS_TEXT_INFO); + obs_property_text_set_info_type(p, OBS_TEXT_INFO_WARNING); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text(ppts, "stream_key", + obs_module_text("CustomServices.StreamID.Key"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_hls = { + .id = "custom_hls", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "HLS", + .get_name = hls_service_name, + .create = hls_service_create, + .destroy = hls_service_destroy, + .update = hls_service_update, + .get_protocol = hls_service_protocol, + .get_properties = hls_service_properties, + .get_connect_info = hls_service_connect_info, + .can_try_to_connect = hls_service_can_try_to_connect, + .get_audio_track_cap = hls_service_audio_track_cap, +}; diff --git a/plugins/custom-services/custom-rist.c b/plugins/custom-services/custom-rist.c new file mode 100644 index 00000000000000..9c1826f5dfea46 --- /dev/null +++ b/plugins/custom-services/custom-rist.c @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct rist_service { + char *server; + char *username; + char *password; + char *encrypt_passphrase; +}; + +static const char *rist_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomRIST"); +} + +static void rist_service_update(void *data, obs_data_t *settings) +{ + struct rist_service *service = data; + + bfree(service->server); + bfree(service->username); + bfree(service->password); + bfree(service->encrypt_passphrase); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(username); + GET_STRING_SETTINGS(password); + + GET_STRING_SETTINGS(encrypt_passphrase); + +#undef GET_STRING_SETTINGS +} + +static void rist_service_destroy(void *data) +{ + struct rist_service *service = data; + + bfree(service->server); + bfree(service->username); + bfree(service->password); + bfree(service->encrypt_passphrase); + bfree(service); +} + +static void *rist_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct rist_service *data = bzalloc(sizeof(struct rist_service)); + rist_service_update(data, settings); + + return data; +} + +static const char *rist_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RIST"; +} + +static const char *rist_service_connect_info(void *data, uint32_t type) +{ + struct rist_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return service->username; + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return service->password; + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: + return service->encrypt_passphrase; + default: + return NULL; + } +} + +enum obs_service_audio_track_cap rist_service_audio_track_cap(void *data) +{ + UNUSED_PARAMETER(data); + + return OBS_SERVICE_AUDIO_MULTI_TRACK; +} + +bool rist_service_can_try_to_connect(void *data) +{ + struct rist_service *service = data; + + if (!service->server) + return false; + + return true; +} + +static obs_properties_t *rist_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text( + ppts, "username", + obs_module_text("CustomServices.Username.Optional"), + OBS_TEXT_DEFAULT); + obs_properties_add_text( + ppts, "password", + obs_module_text("CustomServices.Password.Optional"), + OBS_TEXT_PASSWORD); + obs_properties_add_text( + ppts, "encrypt_passphrase", + obs_module_text("CustomServices.EncryptPassphrase.Optional"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_rist = { + .id = "custom_rist", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "RIST", + .get_name = rist_service_name, + .create = rist_service_create, + .destroy = rist_service_destroy, + .update = rist_service_update, + .get_protocol = rist_service_protocol, + .get_properties = rist_service_properties, + .get_connect_info = rist_service_connect_info, + .can_try_to_connect = rist_service_can_try_to_connect, + .get_audio_track_cap = rist_service_audio_track_cap, +}; diff --git a/plugins/custom-services/custom-service.c b/plugins/custom-services/custom-service.c new file mode 100644 index 00000000000000..28ea319c71e9d1 --- /dev/null +++ b/plugins/custom-services/custom-service.c @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct custom_service { + char *protocol; + + char *server; + char *stream_id; + char *username; + char *password; + char *encrypt_passphrase; +}; + +static const char *custom_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomService.Name"); +} + +static void custom_service_defaults(obs_data_t *settings) +{ + obs_data_set_string(settings, "protocol", "RTMPS"); +} + +static void custom_service_update(void *data, obs_data_t *settings) +{ + struct custom_service *service = data; + + bfree(service->protocol); + bfree(service->server); + bfree(service->stream_id); + bfree(service->username); + bfree(service->password); + bfree(service->encrypt_passphrase); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(protocol); + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(stream_id); + + GET_STRING_SETTINGS(username); + GET_STRING_SETTINGS(password); + + GET_STRING_SETTINGS(encrypt_passphrase); + +#undef GET_STRING_SETTINGS +} + +static void custom_service_destroy(void *data) +{ + struct custom_service *service = data; + + bfree(service->protocol); + bfree(service->server); + bfree(service->stream_id); + bfree(service->username); + bfree(service->password); + bfree(service->encrypt_passphrase); + bfree(service); +} + +static void *custom_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct custom_service *data = bzalloc(sizeof(struct custom_service)); + custom_service_update(data, settings); + + return data; +} + +static const char *custom_service_protocol(void *data) +{ + struct custom_service *service = data; + + return service->protocol; +} + +static const char *custom_service_connect_info(void *data, uint32_t type) +{ + struct custom_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + return service->stream_id; + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return service->username; + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return service->password; + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: + return service->encrypt_passphrase; + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + break; + } + + return NULL; +} + +enum obs_service_audio_track_cap custom_service_audio_track_cap(void *data) +{ + struct custom_service *service = data; + + if (strcmp(service->protocol, "SRT") == 0 || + strcmp(service->protocol, "RIST") == 0 || + strcmp(service->protocol, "HLS") == 0) + return OBS_SERVICE_AUDIO_MULTI_TRACK; + + return OBS_SERVICE_AUDIO_SINGLE_TRACK; +} + +bool custom_service_can_try_to_connect(void *data) +{ + struct custom_service *service = data; + + if (!service->server) + return false; + + /* Require username password combo being set if one of those is set. */ + if ((strcmp(service->protocol, "RTMP") == 0) || + (strcmp(service->protocol, "RTMPS") == 0) || + (strcmp(service->protocol, "RIST") == 0)) { + if ((service->username && !service->password) || + (!service->username && service->password)) + return false; + } + + /* Require stream id/key */ + if ((strcmp(service->protocol, "RTMP") == 0) || + (strcmp(service->protocol, "RTMPS") == 0) || + (strcmp(service->protocol, "HLS") == 0) || + (strcmp(service->protocol, "FTL") == 0)) { + if (!service->stream_id) + return false; + } + + return true; +} + +bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, + obs_data_t *settings) +{ + UNUSED_PARAMETER(prop); + + const char *protocol = obs_data_get_string(settings, "protocol"); + +#define GET_AND_HIDE(property) \ + obs_property_t *property = obs_properties_get(props, #property); \ + obs_property_set_visible(property, false) + + /* Declare and hide all non-custom fields */ + GET_AND_HIDE(hls_warning); + GET_AND_HIDE(stream_id); + GET_AND_HIDE(username); + GET_AND_HIDE(password); + GET_AND_HIDE(encrypt_passphrase); + + GET_AND_HIDE(third_party_field_manager); + +#undef GET_AND_HIDE + + /* Restore stream_id original description */ + obs_property_set_description( + stream_id, obs_module_text("CustomServices.StreamID.Optional")); + +#define SHOW(property) obs_property_set_visible(property, true) + /* Show needed properties for first-party protocol */ + if ((strcmp(protocol, "RTMP") == 0) || + (strcmp(protocol, "RTMPS") == 0)) { + SHOW(stream_id); + obs_property_set_description( + stream_id, + obs_module_text("CustomServices.StreamID.Key")); + SHOW(username); + SHOW(password); + } else if (strcmp(protocol, "FTL") == 0) { + SHOW(stream_id); + obs_property_set_description( + stream_id, + obs_module_text("CustomServices.StreamID.Key")); + } else if (strcmp(protocol, "HLS") == 0) { + SHOW(hls_warning); + SHOW(stream_id); + obs_property_set_description( + stream_id, + obs_module_text("CustomServices.StreamID.Key")); + } else if (strcmp(protocol, "SRT") == 0) { + SHOW(stream_id); + SHOW(encrypt_passphrase); + } else if (strcmp(protocol, "RIST") == 0) { + SHOW(encrypt_passphrase); + SHOW(username); + SHOW(password); + } +#undef SHOW + + /* Clean superfluous settings */ +#define ERASE_DATA_IF_HIDDEN(property) \ + if (!obs_property_visible(property)) \ + obs_data_set_string(settings, #property, "") + + ERASE_DATA_IF_HIDDEN(stream_id); + ERASE_DATA_IF_HIDDEN(username); + ERASE_DATA_IF_HIDDEN(password); + ERASE_DATA_IF_HIDDEN(encrypt_passphrase); + +#undef ERASE_DATA_IF_HIDDEN + + return true; +} + +static obs_properties_t *custom_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add protocol list */ + obs_property_t *p = obs_properties_add_list( + ppts, "protocol", + obs_module_text("CustomServices.CustomService.Protocol"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + /* Add first-party protocol before third-party */ + if (obs_is_output_protocol_registered("RTMP")) + obs_property_list_add_string(p, "RTMP", "RTMP"); + if (obs_is_output_protocol_registered("RTMPS")) + obs_property_list_add_string(p, "RTMPS", "RTMPS"); + if (obs_is_output_protocol_registered("HLS")) + obs_property_list_add_string(p, "HLS", "HLS"); + if (obs_is_output_protocol_registered("SRT")) + obs_property_list_add_string(p, "SRT", "SRT"); + if (obs_is_output_protocol_registered("RIST")) + obs_property_list_add_string(p, "RIST", "RIST"); + + char *string = NULL; + bool add_ftl = false; + for (size_t i = 0; obs_enum_output_protocols(i, &string); i++) { + + /* Skip FTL, it will be put at the end of the list */ + if (strcmp(string, "FTL") == 0) { + add_ftl = true; + continue; + } + + bool already_listed = false; + + for (size_t i = 0; i < obs_property_list_item_count(p); i++) + already_listed |= + (strcmp(obs_property_list_item_string(p, i), + string) == 0); + + if (!already_listed) + obs_property_list_add_string(p, string, string); + } + + /* FTL is deprecated, put it at the end of the list */ + if (add_ftl) + obs_property_list_add_string( + p, obs_module_text("CustomServices.CustomService.FTL"), + "FTL"); + + /* Add callback to the list property */ + obs_property_set_modified_callback(p, update_protocol_cb); + + /* Add warning about how HLS stream key works */ + p = obs_properties_add_text( + ppts, "hls_warning", + obs_module_text("CustomServices.HLS.Warning"), OBS_TEXT_INFO); + obs_property_text_set_info_type(p, OBS_TEXT_INFO_WARNING); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text(ppts, "stream_id", + obs_module_text("CustomServices.StreamID"), + OBS_TEXT_PASSWORD); + obs_properties_add_text( + ppts, "username", + obs_module_text("CustomServices.Username.Optional"), + OBS_TEXT_DEFAULT); + obs_properties_add_text( + ppts, "password", + obs_module_text("CustomServices.Password.Optional"), + OBS_TEXT_PASSWORD); + obs_properties_add_text( + ppts, "encrypt_passphrase", + obs_module_text("CustomServices.EncryptPassphrase.Optional"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_service = { + .id = "custom_service", + .supported_protocols = "RTMP;RTMPS;SRT;RIST", + .get_name = custom_service_name, + .create = custom_service_create, + .destroy = custom_service_destroy, + .update = custom_service_update, + .get_defaults = custom_service_defaults, + .get_protocol = custom_service_protocol, + .get_properties = custom_service_properties, + .get_connect_info = custom_service_connect_info, + .can_try_to_connect = custom_service_can_try_to_connect, + .get_audio_track_cap = custom_service_audio_track_cap, +}; diff --git a/plugins/custom-services/custom-services.c b/plugins/custom-services/custom-services.c new file mode 100644 index 00000000000000..65d08528dee069 --- /dev/null +++ b/plugins/custom-services/custom-services.c @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("custom-services", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "OBS Custom Services"; +} + +extern struct obs_service_info custom_service; + +extern struct obs_service_info custom_rtmp; +extern struct obs_service_info custom_rtmps; +extern struct obs_service_info custom_hls; +extern struct obs_service_info custom_srt; +extern struct obs_service_info custom_rist; + +bool obs_module_load(void) +{ + obs_register_service(&custom_service); + + obs_register_service(&custom_rtmp); + obs_register_service(&custom_rtmps); + obs_register_service(&custom_hls); + obs_register_service(&custom_srt); + obs_register_service(&custom_rist); + + return true; +} diff --git a/plugins/custom-services/custom-srt.c b/plugins/custom-services/custom-srt.c new file mode 100644 index 00000000000000..1960d8cf7c290b --- /dev/null +++ b/plugins/custom-services/custom-srt.c @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct srt_service { + char *server; + char *stream_id; + char *encrypt_passphrase; +}; + +static const char *srt_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomSRT"); +} + +static void srt_service_update(void *data, obs_data_t *settings) +{ + struct srt_service *service = data; + + bfree(service->server); + bfree(service->stream_id); + bfree(service->encrypt_passphrase); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(stream_id); + + GET_STRING_SETTINGS(encrypt_passphrase); + +#undef GET_STRING_SETTINGS +} + +static void srt_service_destroy(void *data) +{ + struct srt_service *service = data; + + bfree(service->server); + bfree(service->stream_id); + bfree(service->encrypt_passphrase); + bfree(service); +} + +static void *srt_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct srt_service *data = bzalloc(sizeof(struct srt_service)); + srt_service_update(data, settings); + + return data; +} + +static const char *srt_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "SRT"; +} + +static const char *srt_service_connect_info(void *data, uint32_t type) +{ + struct srt_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + return service->stream_id; + case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: + return service->encrypt_passphrase; + default: + return NULL; + } +} + +enum obs_service_audio_track_cap srt_service_audio_track_cap(void *data) +{ + UNUSED_PARAMETER(data); + + return OBS_SERVICE_AUDIO_MULTI_TRACK; +} + +bool srt_service_can_try_to_connect(void *data) +{ + struct srt_service *service = data; + + if (!service->server) + return false; + + return true; +} + +static obs_properties_t *srt_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text(ppts, "stream_id", + obs_module_text("CustomServices.StreamID"), + OBS_TEXT_PASSWORD); + obs_properties_add_text( + ppts, "encrypt_passphrase", + obs_module_text("CustomServices.EncryptPassphrase.Optional"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_srt = { + .id = "custom_srt", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "SRT", + .get_name = srt_service_name, + .create = srt_service_create, + .destroy = srt_service_destroy, + .update = srt_service_update, + .get_protocol = srt_service_protocol, + .get_properties = srt_service_properties, + .get_connect_info = srt_service_connect_info, + .can_try_to_connect = srt_service_can_try_to_connect, + .get_audio_track_cap = srt_service_audio_track_cap, +}; diff --git a/plugins/custom-services/data/locale/en-US.ini b/plugins/custom-services/data/locale/en-US.ini new file mode 100644 index 00000000000000..7167044b054fff --- /dev/null +++ b/plugins/custom-services/data/locale/en-US.ini @@ -0,0 +1,15 @@ +CustomServices.Server="Server" +CustomServices.StreamID.Optional="Stream ID (Optional)" +CustomServices.StreamID.Key="Stream key" +CustomServices.Username.Optional="Username (Optional)" +CustomServices.Password.Optional="Password (Optional)" +CustomServices.EncryptPassphrase.Optional="Encryption Passphrase (Optional)" +CustomServices.CustomRTMP="Custom RTMP" +CustomServices.CustomRTMPS="Custom RTMPS" +CustomServices.CustomHLS="Custom HLS" +CustomServices.HLS.Warning="Replace the stream key in the server field with \"{stream_key}\" and put the key in the dedicated field" +CustomServices.CustomSRT="Custom SRT" +CustomServices.CustomRIST="Custom RIST" +CustomServices.CustomService.Name="Custom…" +CustomServices.CustomService.Protocol="Protocol" +CustomServices.CustomService.FTL="FTL (Deprecated)" diff --git a/plugins/custom-services/rtmp-services.c b/plugins/custom-services/rtmp-services.c new file mode 100644 index 00000000000000..7b3c2a5af1cdab --- /dev/null +++ b/plugins/custom-services/rtmp-services.c @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct rtmp_service { + char *server; + char *stream_key; + char *username; + char *password; +}; + +static const char *rtmp_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomRTMP"); +} + +static const char *rtmp_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RTMP"; +} + +static const char *rtmps_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomRTMPS"); +} + +static const char *rtmps_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RTMPS"; +} + +static void rtmp_service_update(void *data, obs_data_t *settings) +{ + struct rtmp_service *service = data; + + bfree(service->server); + bfree(service->stream_key); + bfree(service->username); + bfree(service->password); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(stream_key); + + GET_STRING_SETTINGS(username); + GET_STRING_SETTINGS(password); + +#undef GET_STRING_SETTINGS +} + +static void rtmp_service_destroy(void *data) +{ + struct rtmp_service *service = data; + + bfree(service->server); + bfree(service->stream_key); + bfree(service->username); + bfree(service->password); + bfree(service); +} + +static void *rtmp_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct rtmp_service *data = bzalloc(sizeof(struct rtmp_service)); + rtmp_service_update(data, settings); + + return data; +} + +static const char *rtmp_service_connect_info(void *data, uint32_t type) +{ + struct rtmp_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return service->stream_key; + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return service->username; + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return service->password; + default: + return NULL; + } +} + +bool rtmp_service_can_try_to_connect(void *data) +{ + struct rtmp_service *service = data; + + if (!service->server) + return false; + + /* Require username password combo being set if one of those is set. */ + if ((service->username && !service->password) || + (!service->username && service->password)) + return false; + + /* Require stream key */ + if (!service->stream_key) + return false; + + return true; +} + +static obs_properties_t *rtmp_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text(ppts, "stream_key", + obs_module_text("CustomServices.StreamID.Key"), + OBS_TEXT_PASSWORD); + obs_properties_add_text( + ppts, "username", + obs_module_text("CustomServices.Username.Optional"), + OBS_TEXT_DEFAULT); + obs_properties_add_text( + ppts, "password", + obs_module_text("CustomServices.Password.Optional"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_rtmp = { + .id = "custom_rtmp", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "RTMP", + .get_name = rtmp_service_name, + .create = rtmp_service_create, + .destroy = rtmp_service_destroy, + .update = rtmp_service_update, + .get_protocol = rtmp_service_protocol, + .get_properties = rtmp_service_properties, + .get_connect_info = rtmp_service_connect_info, + .can_try_to_connect = rtmp_service_can_try_to_connect, +}; + +const struct obs_service_info custom_rtmps = { + .id = "custom_rtmps", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "RTMPS", + .get_name = rtmps_service_name, + .create = rtmp_service_create, + .destroy = rtmp_service_destroy, + .update = rtmp_service_update, + .get_protocol = rtmps_service_protocol, + .get_properties = rtmp_service_properties, + .get_connect_info = rtmp_service_connect_info, + .can_try_to_connect = rtmp_service_can_try_to_connect, +}; From 7e45db68fe07e3053ee0ccb37905ac2b7694ee04 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 31 May 2023 15:11:41 +0200 Subject: [PATCH 21/65] rtmp-services: Deprecate and hide services --- plugins/rtmp-services/rtmp-common.c | 1 + plugins/rtmp-services/rtmp-custom.c | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/rtmp-services/rtmp-common.c b/plugins/rtmp-services/rtmp-common.c index 01adf3866f524d..fb97b22dfcc8f9 100644 --- a/plugins/rtmp-services/rtmp-common.c +++ b/plugins/rtmp-services/rtmp-common.c @@ -1119,6 +1119,7 @@ static enum obs_service_audio_track_cap rtmp_common_audio_track_cap(void *data) struct obs_service_info rtmp_common_service = { .id = "rtmp_common", + .flags = OBS_SERVICE_DEPRECATED | OBS_SERVICE_INTERNAL, .get_name = rtmp_common_getname, .create = rtmp_common_create, .destroy = rtmp_common_destroy, diff --git a/plugins/rtmp-services/rtmp-custom.c b/plugins/rtmp-services/rtmp-custom.c index 90bcf6550870d9..0698112c68b160 100644 --- a/plugins/rtmp-services/rtmp-custom.c +++ b/plugins/rtmp-services/rtmp-custom.c @@ -188,6 +188,7 @@ static bool rtmp_custom_can_try_to_connect(void *data) struct obs_service_info rtmp_custom_service = { .id = "rtmp_custom", + .flags = OBS_SERVICE_DEPRECATED | OBS_SERVICE_INTERNAL, .get_name = rtmp_custom_name, .create = rtmp_custom_create, .destroy = rtmp_custom_destroy, From 80626e1831a772ed8ece8f7aa9ac2710d01ce84f Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 1 Jun 2023 15:36:57 +0200 Subject: [PATCH 22/65] plugins: Add orphaned-services Those services were not provided through rtmp-services JSON. So those are deprecated hoping that the related streaming services will develop their own third-party plugins to replace them. --- plugins/CMakeLists.txt | 2 + plugins/orphaned-services/CMakeLists.txt | 48 +++++ .../cmake/macos/Info.plist.in | 28 +++ .../cmake/windows/obs-module.rc.in | 24 +++ .../orphaned-services/dacast/dacast-ingest.c | 179 ++++++++++++++++++ .../orphaned-services/dacast/dacast-ingest.h | 18 ++ plugins/orphaned-services/dacast/dacast.c | 135 +++++++++++++ plugins/orphaned-services/dacast/dacast.h | 8 + .../orphaned-services/data/locale/en-US.ini | 5 + .../orphaned-services/nimotv/nimotv-ingest.c | 144 ++++++++++++++ .../orphaned-services/nimotv/nimotv-ingest.h | 7 + plugins/orphaned-services/nimotv/nimotv.c | 137 ++++++++++++++ plugins/orphaned-services/nimotv/nimotv.h | 7 + plugins/orphaned-services/orphaned-services.c | 32 ++++ .../showroom/showroom-ingest.c | 163 ++++++++++++++++ .../showroom/showroom-ingest.h | 15 ++ plugins/orphaned-services/showroom/showroom.c | 128 +++++++++++++ plugins/orphaned-services/showroom/showroom.h | 8 + .../orphaned-services/younow/younow-ingest.c | 113 +++++++++++ .../orphaned-services/younow/younow-ingest.h | 7 + plugins/orphaned-services/younow/younow.c | 106 +++++++++++ plugins/orphaned-services/younow/younow.h | 7 + 22 files changed, 1321 insertions(+) create mode 100644 plugins/orphaned-services/CMakeLists.txt create mode 100644 plugins/orphaned-services/cmake/macos/Info.plist.in create mode 100644 plugins/orphaned-services/cmake/windows/obs-module.rc.in create mode 100644 plugins/orphaned-services/dacast/dacast-ingest.c create mode 100644 plugins/orphaned-services/dacast/dacast-ingest.h create mode 100644 plugins/orphaned-services/dacast/dacast.c create mode 100644 plugins/orphaned-services/dacast/dacast.h create mode 100644 plugins/orphaned-services/data/locale/en-US.ini create mode 100644 plugins/orphaned-services/nimotv/nimotv-ingest.c create mode 100644 plugins/orphaned-services/nimotv/nimotv-ingest.h create mode 100644 plugins/orphaned-services/nimotv/nimotv.c create mode 100644 plugins/orphaned-services/nimotv/nimotv.h create mode 100644 plugins/orphaned-services/orphaned-services.c create mode 100644 plugins/orphaned-services/showroom/showroom-ingest.c create mode 100644 plugins/orphaned-services/showroom/showroom-ingest.h create mode 100644 plugins/orphaned-services/showroom/showroom.c create mode 100644 plugins/orphaned-services/showroom/showroom.h create mode 100644 plugins/orphaned-services/younow/younow-ingest.c create mode 100644 plugins/orphaned-services/younow/younow-ingest.h create mode 100644 plugins/orphaned-services/younow/younow.c create mode 100644 plugins/orphaned-services/younow/younow.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4f13f30c67230c..feaa7c690a7d2e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -83,6 +83,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) add_subdirectory(obs-webrtc) check_obs_websocket() add_subdirectory(obs-x264) + add_subdirectory(orphaned-services) add_subdirectory(rtmp-services) if(OS_LINUX) add_subdirectory(sndio) @@ -200,3 +201,4 @@ add_subdirectory(aja) add_subdirectory(obs-webrtc) add_subdirectory(obs-services) add_subdirectory(custom-services) +add_subdirectory(orphaned-services) diff --git a/plugins/orphaned-services/CMakeLists.txt b/plugins/orphaned-services/CMakeLists.txt new file mode 100644 index 00000000000000..e165e7012fe153 --- /dev/null +++ b/plugins/orphaned-services/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + find_package(jansson REQUIRED) +else() + find_package(Jansson REQUIRED) +endif() + +add_library(orphaned-services MODULE) +add_library(OBS::orphaned-services ALIAS orphaned-services) + +target_sources( + orphaned-services + PRIVATE # cmake-format: sortable + dacast/dacast-ingest.c + dacast/dacast-ingest.h + dacast/dacast.c + dacast/dacast.h + nimotv/nimotv-ingest.c + nimotv/nimotv-ingest.h + nimotv/nimotv.c + nimotv/nimotv.h + orphaned-services.c + showroom/showroom-ingest.c + showroom/showroom-ingest.h + showroom/showroom.c + showroom/showroom.h + younow/younow-ingest.c + younow/younow-ingest.h + younow/younow.c + younow/younow.h) + +target_link_libraries(orphaned-services PRIVATE OBS::libobs OBS::file-updater) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in orphaned-services.rc) + target_sources(orphaned-services PRIVATE orphaned-services.rc) + target_link_options(orphaned-services PRIVATE /IGNORE:4098) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_link_libraries(orphaned-services PRIVATE jansson::jansson) + set_target_properties_obs(orphaned-services PROPERTIES FOLDER plugins PREFIX "") +else() + target_link_libraries(orphaned-services PRIVATE Jansson::Jansson) + set_target_properties(orphaned-services PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(orphaned-services) +endif() diff --git a/plugins/orphaned-services/cmake/macos/Info.plist.in b/plugins/orphaned-services/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..91b7bc25690642 --- /dev/null +++ b/plugins/orphaned-services/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + orphaned-services + CFBundleIdentifier + com.obsproject.orphaned-services + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + orphaned-services + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/orphaned-services/cmake/windows/obs-module.rc.in b/plugins/orphaned-services/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..9365db392eb963 --- /dev/null +++ b/plugins/orphaned-services/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "Services not maintained by OBS Project" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "orphaned-services" + VALUE "OriginalFilename", "orphaned-services" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/orphaned-services/dacast/dacast-ingest.c b/plugins/orphaned-services/dacast/dacast-ingest.c new file mode 100644 index 00000000000000..56183e285a1cc4 --- /dev/null +++ b/plugins/orphaned-services/dacast/dacast-ingest.c @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: 2020 Faeez Kadiri +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include + +#include "dacast-ingest.h" + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000ULL +#endif + +static update_info_t *dacast_update_info = NULL; +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +static bool ingests_loaded = false; + +struct dacast_ingest_info { + char *key; + uint64_t last_time; + struct dacast_ingest ingest; +}; + +struct dacast_ingest dacast_invalid_ingest = {"rtmp://dacast", "", "", + "fake_key"}; + +static DARRAY(struct dacast_ingest_info) cur_ingests; + +static void free_ingest(struct dacast_ingest ingest) +{ + bfree((void *)ingest.url); + bfree((void *)ingest.username); + bfree((void *)ingest.password); + bfree((void *)ingest.streamkey); +} + +static void free_ingests(void) +{ + for (size_t i = 0; i < cur_ingests.num; i++) { + struct dacast_ingest_info *info = &cur_ingests.array[i]; + bfree(info->key); + free_ingest(info->ingest); + } + da_free(cur_ingests); +} + +static struct dacast_ingest_info *find_ingest(const char *key) +{ + struct dacast_ingest_info *ret = NULL; + for (size_t i = 0; i < cur_ingests.num; i++) { + struct dacast_ingest_info *info = &cur_ingests.array[i]; + if (strcmp(info->key, key) == 0) { + ret = info; + break; + } + } + return (struct dacast_ingest_info *)ret; +} + +static bool load_ingests(const char *json, const char *key) +{ + json_t *root; + json_t *stream; + bool success = false; + struct dacast_ingest_info *info = find_ingest(key); + if (!info) { + info = da_push_back_new(cur_ingests); + info->key = bstrdup(key); + } else { + free_ingest(info->ingest); + } + + root = json_loads(json, 0, NULL); + if (!root) + goto finish; + + stream = json_object_get(root, "stream"); + if (!stream) + goto finish; + + json_t *item_server = json_object_get(stream, "server"); + json_t *item_username = json_object_get(stream, "username"); + json_t *item_password = json_object_get(stream, "password"); + json_t *item_streamkey = json_object_get(stream, "streamkey"); + + if (!item_server || !item_username || !item_password || !item_streamkey) + goto finish; + + const char *server = json_string_value(item_server); + const char *username = json_string_value(item_username); + const char *password = json_string_value(item_password); + const char *streamkey = json_string_value(item_streamkey); + + info->ingest.url = bstrdup(server); + info->ingest.username = bstrdup(username); + info->ingest.password = bstrdup(password); + info->ingest.streamkey = bstrdup(streamkey); + + info->last_time = os_gettime_ns() / SEC_TO_NSEC; + + success = true; + +finish: + if (root) + json_decref(root); + return success; +} + +static bool dacast_ingest_update(void *param, struct file_download_data *data) +{ + bool success; + + pthread_mutex_lock(&mutex); + success = load_ingests((const char *)data->buffer.array, + (const char *)param); + pthread_mutex_unlock(&mutex); + + if (success) { + os_atomic_set_bool(&ingests_loaded, true); + } + + return true; +} + +struct dacast_ingest *dacast_ingest(const char *key) +{ + pthread_mutex_lock(&mutex); + struct dacast_ingest_info *info = find_ingest(key); + pthread_mutex_unlock(&mutex); + return info == NULL ? &dacast_invalid_ingest : &info->ingest; +} + +void init_dacast_data(void) +{ + da_init(cur_ingests); + pthread_mutex_init(&mutex, NULL); +} + +#define TIMEOUT_SEC 3 + +void dacast_ingests_load_data(const char *server, const char *key) +{ + struct dstr uri = {0}; + + os_atomic_set_bool(&ingests_loaded, false); + + dstr_copy(&uri, server); + dstr_cat(&uri, key); + + if (dacast_update_info) { + update_info_destroy(dacast_update_info); + dacast_update_info = NULL; + } + + dacast_update_info = update_info_create_single( + "[dacast ingest load data] ", "orphaned Dacast plugin", + uri.array, dacast_ingest_update, (void *)key); + + if (!os_atomic_load_bool(&ingests_loaded)) { + for (int i = 0; i < TIMEOUT_SEC * 100; i++) { + if (os_atomic_load_bool(&ingests_loaded)) { + break; + } + os_sleep_ms(10); + } + } + + dstr_free(&uri); +} + +void unload_dacast_data(void) +{ + update_info_destroy(dacast_update_info); + free_ingests(); + pthread_mutex_destroy(&mutex); +} diff --git a/plugins/orphaned-services/dacast/dacast-ingest.h b/plugins/orphaned-services/dacast/dacast-ingest.h new file mode 100644 index 00000000000000..5290c2764079ba --- /dev/null +++ b/plugins/orphaned-services/dacast/dacast-ingest.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2020 Faeez Kadiri +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +struct dacast_ingest { + const char *url; + const char *username; + const char *password; + const char *streamkey; +}; + +void init_dacast_data(void); +void unload_dacast_data(void); + +void dacast_ingests_load_data(const char *server, const char *key); +struct dacast_ingest *dacast_ingest(const char *key); diff --git a/plugins/orphaned-services/dacast/dacast.c b/plugins/orphaned-services/dacast/dacast.c new file mode 100644 index 00000000000000..f261136cd9f50c --- /dev/null +++ b/plugins/orphaned-services/dacast/dacast.c @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "dacast.h" +#include "dacast-ingest.h" + +static const char *API_ENDPOINT = + "https://developer.dacast.com/v3/encoder-setup/"; +static const char *VIDEO_CODEC[] = {"h264", NULL}; + +struct dacast { + char *key; + struct dacast_ingest *ingest; +}; + +static const char *dacast_getname(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return "Dacast"; +} + +static void dacast_update(void *data, obs_data_t *settings) +{ + UNUSED_PARAMETER(settings); + + struct dacast *service = data; + bfree(service->key); + service->key = bstrdup(obs_data_get_string(settings, "key")); +} + +static void *dacast_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct dacast *data = bzalloc(sizeof(struct dacast)); + dacast_update(data, settings); + + return data; +} + +static void dacast_destroy(void *data) +{ + struct dacast *service = data; + bfree(service->key); + bfree(service); +} + +static const char *dacast_get_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RTMP"; +} + +static const char *dacast_get_connect_info(void *data, uint32_t type) +{ + struct dacast *service = data; + if (!service->ingest) { + dacast_ingests_load_data(API_ENDPOINT, service->key); + service->ingest = dacast_ingest(service->key); + } + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->ingest->url; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return service->ingest->streamkey; + case OBS_SERVICE_CONNECT_INFO_USERNAME: + return service->ingest->username; + case OBS_SERVICE_CONNECT_INFO_PASSWORD: + return service->ingest->password; + default: + return NULL; + } +} + +static bool dacast_can_try_to_connect(void *data) +{ + struct dacast *service = data; + if (!service->ingest) { + dacast_ingests_load_data(API_ENDPOINT, service->key); + service->ingest = dacast_ingest(service->key); + } + + return (service->ingest->streamkey != NULL && + service->ingest->streamkey[0] != '\0'); +} + +static const char **dacast_get_supported_video_codecs(void *data) +{ + UNUSED_PARAMETER(data); + return VIDEO_CODEC; +} + +static obs_properties_t *dacast_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + obs_properties_add_text(ppts, "key", obs_module_text("Api.Key"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +void load_dacast(void) +{ + init_dacast_data(); + + struct obs_service_info dacast_service = { + .id = "dacast", + .flags = OBS_SERVICE_UNCOMMON | OBS_SERVICE_DEPRECATED, + .get_name = dacast_getname, + .create = dacast_create, + .destroy = dacast_destroy, + .update = dacast_update, + .get_properties = dacast_properties, + .get_protocol = dacast_get_protocol, + .get_connect_info = dacast_get_connect_info, + .get_supported_video_codecs = dacast_get_supported_video_codecs, + .can_try_to_connect = dacast_can_try_to_connect, + .supported_protocols = "RTMP", + }; + + obs_register_service(&dacast_service); +} + +void unload_dacast(void) +{ + unload_dacast_data(); +} diff --git a/plugins/orphaned-services/dacast/dacast.h b/plugins/orphaned-services/dacast/dacast.h new file mode 100644 index 00000000000000..fcf6f6ae6fc459 --- /dev/null +++ b/plugins/orphaned-services/dacast/dacast.h @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +void load_dacast(void); +void unload_dacast(void); diff --git a/plugins/orphaned-services/data/locale/en-US.ini b/plugins/orphaned-services/data/locale/en-US.ini new file mode 100644 index 00000000000000..e026fc4bab99d7 --- /dev/null +++ b/plugins/orphaned-services/data/locale/en-US.ini @@ -0,0 +1,5 @@ +Access.Key="Access key" +Api.Key="API key" +Server="Server" +Server.Auto="Auto" +Stream.Key="Stream key" diff --git a/plugins/orphaned-services/nimotv/nimotv-ingest.c b/plugins/orphaned-services/nimotv/nimotv-ingest.c new file mode 100644 index 00000000000000..c8dc0bb80ad92e --- /dev/null +++ b/plugins/orphaned-services/nimotv/nimotv-ingest.c @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2020 dgeibi +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include "util/base.h" +#include "nimotv-ingest.h" +#include + +struct nimotv_mem_struct { + char *memory; + size_t size; +}; +static char *current_ingest = NULL; +static time_t last_time = -1; + +static size_t nimotv_write_cb(void *contents, size_t size, size_t nmemb, + void *userp) +{ + size_t realsize = size * nmemb; + struct nimotv_mem_struct *mem = (struct nimotv_mem_struct *)userp; + + char *ptr = realloc(mem->memory, mem->size + realsize + 1); + if (ptr == NULL) { + blog(LOG_WARNING, "nimotv_write_cb: realloc returned NULL"); + return 0; + } + + mem->memory = ptr; + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} + +static char *load_ingest(const char *json) +{ + json_t *root = json_loads(json, 0, NULL); + char *ingest = NULL; + if (!root) + return ingest; + + json_t *recommended_ingests = json_object_get(root, "ingests"); + if (recommended_ingests) { + json_t *recommended = json_array_get(recommended_ingests, 0); + if (recommended) { + json_t *item_url = json_object_get(recommended, "url"); + if (item_url) { + const char *url = json_string_value(item_url); + ingest = bstrdup(url); + } + } + } + + json_decref(root); + return ingest; +} + +const char *nimotv_get_ingest(const char *key) +{ + if (current_ingest != NULL) { + time_t now = time(NULL); + double diff = difftime(now, last_time); + if (diff < 2) { + blog(LOG_INFO, + "nimotv_get_ingest: returning ingest from cache: %s", + current_ingest); + return current_ingest; + } + } + + CURL *curl_handle; + CURLcode res; + struct nimotv_mem_struct chunk; + struct dstr uri; + long response_code; + + curl_handle = curl_easy_init(); + chunk.memory = malloc(1); /* will be grown as needed by realloc */ + chunk.size = 0; /* no data at this point */ + + char *encoded_key = curl_easy_escape(NULL, key, 0); + dstr_init(&uri); + dstr_copy(&uri, "https://globalcdnweb.nimo.tv/api/ingests/nimo?id="); + dstr_ncat(&uri, encoded_key, strlen(encoded_key)); + curl_free(encoded_key); + + curl_easy_setopt(curl_handle, CURLOPT_URL, uri.array); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, true); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 3L); + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, nimotv_write_cb); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk); + curl_obs_set_revoke_setting(curl_handle); + + res = curl_easy_perform(curl_handle); + dstr_free(&uri); + + if (res != CURLE_OK) { + blog(LOG_WARNING, + "nimotv_get_ingest: curl_easy_perform() failed: %s", + curl_easy_strerror(res)); + curl_easy_cleanup(curl_handle); + free(chunk.memory); + return NULL; + } + + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) { + blog(LOG_WARNING, + "nimotv_get_ingest: curl_easy_perform() returned code: %ld", + response_code); + curl_easy_cleanup(curl_handle); + free(chunk.memory); + return NULL; + } + + curl_easy_cleanup(curl_handle); + + if (chunk.size == 0) { + blog(LOG_WARNING, + "nimotv_get_ingest: curl_easy_perform() returned empty response"); + free(chunk.memory); + return NULL; + } + + if (current_ingest != NULL) { + bfree(current_ingest); + } + + current_ingest = load_ingest(chunk.memory); + last_time = time(NULL); + + free(chunk.memory); + blog(LOG_INFO, "nimotv_get_ingest: returning ingest: %s", + current_ingest); + + return current_ingest; +} diff --git a/plugins/orphaned-services/nimotv/nimotv-ingest.h b/plugins/orphaned-services/nimotv/nimotv-ingest.h new file mode 100644 index 00000000000000..4faede6466ae09 --- /dev/null +++ b/plugins/orphaned-services/nimotv/nimotv-ingest.h @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2020 dgeibi +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +const char *nimotv_get_ingest(const char *key); diff --git a/plugins/orphaned-services/nimotv/nimotv.c b/plugins/orphaned-services/nimotv/nimotv.c new file mode 100644 index 00000000000000..8b8404aaef4f78 --- /dev/null +++ b/plugins/orphaned-services/nimotv/nimotv.c @@ -0,0 +1,137 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "nimotv.h" +#include "nimotv-ingest.h" + +static const char *AUTO = "auto"; +static const char *VIDEO_CODEC[] = {"h264", NULL}; + +struct nimotv { + char *server; + char *key; +}; + +static const char *nimotv_getname(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return "Nimo TV"; +} + +static void nimotv_update(void *data, obs_data_t *settings) +{ + UNUSED_PARAMETER(settings); + + struct nimotv *service = data; + bfree(service->server); + bfree(service->key); + service->server = bstrdup(obs_data_get_string(settings, "server")); + service->key = bstrdup(obs_data_get_string(settings, "key")); +} + +static void *nimotv_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct nimotv *data = bzalloc(sizeof(struct nimotv)); + nimotv_update(data, settings); + + return data; +} + +static void nimotv_destroy(void *data) +{ + struct nimotv *service = data; + bfree(service->server); + bfree(service->key); + bfree(service); +} + +static const char *nimotv_get_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RTMP"; +} + +static const char *nimotv_get_connect_info(void *data, uint32_t type) +{ + struct nimotv *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + if (service->server && strcmp(service->server, AUTO) == 0) + return nimotv_get_ingest(service->key); + return service->server; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return service->key; + default: + return NULL; + } +} + +static bool nimotv_can_try_to_connect(void *data) +{ + const char *server = nimotv_get_connect_info( + data, OBS_SERVICE_CONNECT_INFO_SERVER_URL); + const char *key = nimotv_get_connect_info( + data, OBS_SERVICE_CONNECT_INFO_STREAM_KEY); + + return (server != NULL && server[0] != '\0') && + (key != NULL && key[0] != '\0'); +} + +static const char **nimotv_get_supported_video_codecs(void *data) +{ + UNUSED_PARAMETER(data); + return VIDEO_CODEC; +} + +static obs_properties_t *nimotv_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + obs_property_t *p; + + p = obs_properties_add_list(ppts, "server", obs_module_text("Server"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + obs_property_list_add_string(p, obs_module_text("Server.Auto"), AUTO); + + obs_property_list_add_string(p, "Global:1", + "rtmp://wspush.rtmp.nimo.tv/live/"); + obs_property_list_add_string(p, "Global:2", + "rtmp://txpush.rtmp.nimo.tv/live/"); + obs_property_list_add_string(p, "Global:3", + "rtmp://alpush.rtmp.nimo.tv/live/"); + + obs_properties_add_text(ppts, "key", obs_module_text("Stream.Key"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +void load_nimotv(void) +{ + struct obs_service_info nimotv_service = { + .id = "nimotv", + .flags = OBS_SERVICE_UNCOMMON | OBS_SERVICE_DEPRECATED, + .get_name = nimotv_getname, + .create = nimotv_create, + .destroy = nimotv_destroy, + .update = nimotv_update, + .get_properties = nimotv_properties, + .get_protocol = nimotv_get_protocol, + .get_connect_info = nimotv_get_connect_info, + .get_supported_video_codecs = nimotv_get_supported_video_codecs, + .can_try_to_connect = nimotv_can_try_to_connect, + .supported_protocols = "RTMP", + }; + + obs_register_service(&nimotv_service); +} diff --git a/plugins/orphaned-services/nimotv/nimotv.h b/plugins/orphaned-services/nimotv/nimotv.h new file mode 100644 index 00000000000000..ee7680293da686 --- /dev/null +++ b/plugins/orphaned-services/nimotv/nimotv.h @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +void load_nimotv(void); diff --git a/plugins/orphaned-services/orphaned-services.c b/plugins/orphaned-services/orphaned-services.c new file mode 100644 index 00000000000000..7a5bfcb871b9b9 --- /dev/null +++ b/plugins/orphaned-services/orphaned-services.c @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "dacast/dacast.h" +#include "nimotv/nimotv.h" +#include "showroom/showroom.h" +#include "younow/younow.h" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("orphaned-services", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "Services not maintained by OBS Project"; +} + +bool obs_module_load(void) +{ + load_dacast(); + load_nimotv(); + load_showroom(); + load_younow(); + return true; +} + +void obs_module_unload(void) +{ + unload_dacast(); + unload_showroom(); +} diff --git a/plugins/orphaned-services/showroom/showroom-ingest.c b/plugins/orphaned-services/showroom/showroom-ingest.c new file mode 100644 index 00000000000000..06c5d5b755fbe7 --- /dev/null +++ b/plugins/orphaned-services/showroom/showroom-ingest.c @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2020 Toasterapp +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include "util/base.h" +#include +#include +#include "showroom-ingest.h" +#include + +struct showroom_ingest_info { + char *access_key; + uint64_t last_time; + struct showroom_ingest ingest; +}; + +static DARRAY(struct showroom_ingest_info) cur_ingests = {0}; + +struct showroom_ingest invalid_ingest = {"", ""}; + +void free_showroom_data(void) +{ + for (size_t i = 0; i < cur_ingests.num; i++) { + struct showroom_ingest_info *info = &cur_ingests.array[i]; + bfree(info->access_key); + bfree((void *)info->ingest.key); + bfree((void *)info->ingest.url); + } + + da_free(cur_ingests); +} + +static size_t showroom_write_cb(void *data, size_t size, size_t nmemb, + void *user_pointer) +{ + struct dstr *json = user_pointer; + size_t realsize = size * nmemb; + dstr_ncat(json, data, realsize); + return realsize; +} + +static struct showroom_ingest_info *find_ingest(const char *access_key) +{ + struct showroom_ingest_info *ret = NULL; + for (size_t i = 0; i < cur_ingests.num; i++) { + struct showroom_ingest_info *info = &cur_ingests.array[i]; + if (strcmp(info->access_key, access_key) == 0) { + ret = info; + break; + } + } + + return ret; +} + +#ifndef SEC_TO_NSEC +#define SEC_TO_NSEC 1000000000ULL +#endif + +static struct showroom_ingest_info *get_ingest_from_json(char *str, + const char *access_key) +{ + json_error_t error; + json_t *root; + root = json_loads(str, JSON_REJECT_DUPLICATES, &error); + if (!root) { + return NULL; + } + + const char *url_str = + json_string_value(json_object_get(root, "streaming_url_rtmp")); + const char *key_str = + json_string_value(json_object_get(root, "streaming_key")); + + struct showroom_ingest_info *info = find_ingest(access_key); + if (!info) { + info = da_push_back_new(cur_ingests); + info->access_key = bstrdup(access_key); + } + + bfree((void *)info->ingest.url); + bfree((void *)info->ingest.key); + info->ingest.url = bstrdup(url_str); + info->ingest.key = bstrdup(key_str); + info->last_time = os_gettime_ns() / SEC_TO_NSEC; + + json_decref(root); + return info; +} + +struct showroom_ingest *showroom_get_ingest(const char *server, + const char *access_key) +{ + struct showroom_ingest_info *info = find_ingest(access_key); + CURL *curl_handle; + CURLcode res; + struct dstr json = {0}; + struct dstr uri = {0}; + long response_code; + + if (info) { + /* this function is called a bunch of times for the same data, + * so in order to prevent multiple unnecessary queries in a + * short period of time, return the same data for 10 seconds */ + + uint64_t ts_sec = os_gettime_ns() / SEC_TO_NSEC; + if (ts_sec - info->last_time < 10) { + return &info->ingest; + } else { + info = NULL; + } + } + + curl_handle = curl_easy_init(); + + dstr_copy(&uri, server); + dstr_cat(&uri, access_key); + curl_easy_setopt(curl_handle, CURLOPT_URL, uri.array); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, true); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 30L); + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, showroom_write_cb); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&json); + curl_obs_set_revoke_setting(curl_handle); + + res = curl_easy_perform(curl_handle); + dstr_free(&uri); + if (res != CURLE_OK) { + blog(LOG_WARNING, + "showroom_get_ingest: curl_easy_perform() failed: %s", + curl_easy_strerror(res)); + goto cleanup; + } + + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) { + blog(LOG_WARNING, + "showroom_get_ingest: curl_easy_perform() returned " + "code: %ld", + response_code); + goto cleanup; + } + + if (json.len == 0) { + blog(LOG_WARNING, + "showroom_get_ingest: curl_easy_perform() returned " + "empty response"); + goto cleanup; + } + + info = get_ingest_from_json(json.array, access_key); + +cleanup: + curl_easy_cleanup(curl_handle); + dstr_free(&json); + return info ? &info->ingest : &invalid_ingest; +} diff --git a/plugins/orphaned-services/showroom/showroom-ingest.h b/plugins/orphaned-services/showroom/showroom-ingest.h new file mode 100644 index 00000000000000..c675f7782c857d --- /dev/null +++ b/plugins/orphaned-services/showroom/showroom-ingest.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2020 Toasterapp +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +struct showroom_ingest { + const char *url; + const char *key; +}; + +struct showroom_ingest *showroom_get_ingest(const char *server, + const char *access_key); + +void free_showroom_data(); diff --git a/plugins/orphaned-services/showroom/showroom.c b/plugins/orphaned-services/showroom/showroom.c new file mode 100644 index 00000000000000..7e91a79fa76a5d --- /dev/null +++ b/plugins/orphaned-services/showroom/showroom.c @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "showroom.h" +#include "showroom-ingest.h" + +static const char *API_URL = + "https://www.showroom-live.com/api/obs/streaming_info?obs_key="; +static const char *VIDEO_CODEC[] = {"h264", NULL}; + +struct showroom { + char *key; + struct showroom_ingest *ingest; +}; + +static const char *showroom_getname(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return "SHOWROOM"; +} + +static void showroom_update(void *data, obs_data_t *settings) +{ + UNUSED_PARAMETER(settings); + + struct showroom *service = data; + bfree(service->key); + service->key = bstrdup(obs_data_get_string(settings, "key")); +} + +static void *showroom_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct showroom *data = bzalloc(sizeof(struct showroom)); + showroom_update(data, settings); + + return data; +} + +static void showroom_destroy(void *data) +{ + struct showroom *service = data; + bfree(service->key); + bfree(service); +} + +static const char *showroom_get_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "RTMP"; +} + +static const char *showroom_get_connect_info(void *data, uint32_t type) +{ + struct showroom *service = data; + if (!service->ingest) { + service->ingest = showroom_get_ingest(API_URL, service->key); + } + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->ingest->url; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return service->ingest->key; + default: + return NULL; + } +} + +static bool showroom_can_try_to_connect(void *data) +{ + struct showroom *service = data; + if (!service->ingest) { + service->ingest = showroom_get_ingest(API_URL, service->key); + } + + return (service->ingest->key != NULL && + service->ingest->key[0] != '\0'); +} + +static const char **showroom_get_supported_video_codecs(void *data) +{ + UNUSED_PARAMETER(data); + return VIDEO_CODEC; +} + +static obs_properties_t *showroom_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + obs_properties_add_text(ppts, "key", obs_module_text("Access.Key"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +void load_showroom(void) +{ + struct obs_service_info showroom_service = { + .id = "showroom", + .flags = OBS_SERVICE_UNCOMMON | OBS_SERVICE_DEPRECATED, + .get_name = showroom_getname, + .create = showroom_create, + .destroy = showroom_destroy, + .update = showroom_update, + .get_properties = showroom_properties, + .get_protocol = showroom_get_protocol, + .get_connect_info = showroom_get_connect_info, + .get_supported_video_codecs = + showroom_get_supported_video_codecs, + .can_try_to_connect = showroom_can_try_to_connect, + .supported_protocols = "RTMP", + }; + + obs_register_service(&showroom_service); +} + +void unload_showroom(void) +{ + free_showroom_data(); +} diff --git a/plugins/orphaned-services/showroom/showroom.h b/plugins/orphaned-services/showroom/showroom.h new file mode 100644 index 00000000000000..f7ac09841ddb60 --- /dev/null +++ b/plugins/orphaned-services/showroom/showroom.h @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +void load_showroom(void); +void unload_showroom(void); diff --git a/plugins/orphaned-services/younow/younow-ingest.c b/plugins/orphaned-services/younow/younow-ingest.c new file mode 100644 index 00000000000000..abc20c828b50ea --- /dev/null +++ b/plugins/orphaned-services/younow/younow-ingest.c @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2020 Roman Sivriver +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include +#include "util/base.h" +#include "younow-ingest.h" + +struct younow_mem_struct { + char *memory; + size_t size; +}; + +static char *current_ingest = NULL; + +static size_t younow_write_cb(void *contents, size_t size, size_t nmemb, + void *userp) +{ + size_t realsize = size * nmemb; + struct younow_mem_struct *mem = (struct younow_mem_struct *)userp; + + mem->memory = realloc(mem->memory, mem->size + realsize + 1); + if (mem->memory == NULL) { + blog(LOG_WARNING, "yyounow_write_cb: realloc returned NULL"); + return 0; + } + + memcpy(&(mem->memory[mem->size]), contents, realsize); + mem->size += realsize; + mem->memory[mem->size] = 0; + + return realsize; +} + +const char *younow_get_ingest(const char *server, const char *key) +{ + CURL *curl_handle; + CURLcode res; + struct younow_mem_struct chunk; + struct dstr uri; + long response_code; + + // find the delimiter in stream key + const char *delim = strchr(key, '_'); + if (delim == NULL) { + blog(LOG_WARNING, + "younow_get_ingest: delimiter not found in stream key"); + return server; + } + + curl_handle = curl_easy_init(); + + chunk.memory = malloc(1); /* will be grown as needed by realloc */ + chunk.size = 0; /* no data at this point */ + + dstr_init(&uri); + dstr_copy(&uri, server); + dstr_ncat(&uri, key, delim - key); + + curl_easy_setopt(curl_handle, CURLOPT_URL, uri.array); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, true); + curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 2L); + curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 3L); + curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, younow_write_cb); + curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk); + curl_obs_set_revoke_setting(curl_handle); + + res = curl_easy_perform(curl_handle); + dstr_free(&uri); + + if (res != CURLE_OK) { + blog(LOG_WARNING, + "younow_get_ingest: curl_easy_perform() failed: %s", + curl_easy_strerror(res)); + curl_easy_cleanup(curl_handle); + free(chunk.memory); + return server; + } + + curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &response_code); + if (response_code != 200) { + blog(LOG_WARNING, + "younow_get_ingest: curl_easy_perform() returned code: %ld", + response_code); + curl_easy_cleanup(curl_handle); + free(chunk.memory); + return server; + } + + curl_easy_cleanup(curl_handle); + + if (chunk.size == 0) { + blog(LOG_WARNING, + "younow_get_ingest: curl_easy_perform() returned empty response"); + free(chunk.memory); + return server; + } + + if (current_ingest) { + free(current_ingest); + current_ingest = NULL; + } + + current_ingest = strdup(chunk.memory); + free(chunk.memory); + blog(LOG_INFO, "younow_get_ingest: returning ingest: %s", + current_ingest); + return current_ingest; +} diff --git a/plugins/orphaned-services/younow/younow-ingest.h b/plugins/orphaned-services/younow/younow-ingest.h new file mode 100644 index 00000000000000..a699aca24ace4d --- /dev/null +++ b/plugins/orphaned-services/younow/younow-ingest.h @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2020 Roman Sivriver +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +const char *younow_get_ingest(const char *server, const char *key); diff --git a/plugins/orphaned-services/younow/younow.c b/plugins/orphaned-services/younow/younow.c new file mode 100644 index 00000000000000..2afdc967ba8f96 --- /dev/null +++ b/plugins/orphaned-services/younow/younow.c @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "younow.h" +#include "younow-ingest.h" + +static const char *API_URL = + "https://api.younow.com/php/api/broadcast/ingest?id="; + +struct younow { + char *key; +}; + +static const char *younow_getname(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return "YouNow"; +} + +static void younow_update(void *data, obs_data_t *settings) +{ + UNUSED_PARAMETER(settings); + + struct younow *service = data; + bfree(service->key); + service->key = bstrdup(obs_data_get_string(settings, "key")); +} + +static void *younow_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct younow *data = bzalloc(sizeof(struct younow)); + younow_update(data, settings); + + return data; +} + +static void younow_destroy(void *data) +{ + struct younow *service = data; + bfree(service->key); + bfree(service); +} + +static const char *younow_get_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "FTL"; +} + +static const char *younow_get_connect_info(void *data, uint32_t type) +{ + struct younow *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return younow_get_ingest(API_URL, service->key); + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + return service->key; + default: + return NULL; + } +} + +static bool younow_can_try_to_connect(void *data) +{ + struct younow *service = data; + + return (service->key != NULL && service->key[0] != '\0'); +} + +static obs_properties_t *younow_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + obs_properties_add_text(ppts, "key", obs_module_text("Api.Key"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +void load_younow(void) +{ + struct obs_service_info younow_service = { + .id = "younow", + .flags = OBS_SERVICE_UNCOMMON | OBS_SERVICE_DEPRECATED, + .get_name = younow_getname, + .create = younow_create, + .destroy = younow_destroy, + .update = younow_update, + .get_properties = younow_properties, + .get_protocol = younow_get_protocol, + .get_connect_info = younow_get_connect_info, + .can_try_to_connect = younow_can_try_to_connect, + .supported_protocols = "FTL", + }; + + obs_register_service(&younow_service); +} diff --git a/plugins/orphaned-services/younow/younow.h b/plugins/orphaned-services/younow/younow.h new file mode 100644 index 00000000000000..f087f2f8384fc0 --- /dev/null +++ b/plugins/orphaned-services/younow/younow.h @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +void load_younow(void); From 7959dc53072d2f35c0fc4c499ef48ccd51dd6dc6 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 3 Jun 2023 13:31:04 +0200 Subject: [PATCH 23/65] plugins: Add obs-youtube --- plugins/CMakeLists.txt | 2 + plugins/obs-youtube/CMakeLists.txt | 24 +++ plugins/obs-youtube/cmake/macos/Info.plist.in | 28 ++++ .../cmake/windows/obs-module.rc.in | 24 +++ plugins/obs-youtube/data/locale/en-US.ini | 8 + plugins/obs-youtube/obs-youtube.cpp | 16 ++ plugins/obs-youtube/youtube-config.cpp | 148 ++++++++++++++++++ plugins/obs-youtube/youtube-config.hpp | 31 ++++ plugins/obs-youtube/youtube-service-info.cpp | 75 +++++++++ plugins/obs-youtube/youtube-service.cpp | 41 +++++ plugins/obs-youtube/youtube-service.hpp | 39 +++++ 11 files changed, 436 insertions(+) create mode 100644 plugins/obs-youtube/CMakeLists.txt create mode 100644 plugins/obs-youtube/cmake/macos/Info.plist.in create mode 100644 plugins/obs-youtube/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-youtube/data/locale/en-US.ini create mode 100644 plugins/obs-youtube/obs-youtube.cpp create mode 100644 plugins/obs-youtube/youtube-config.cpp create mode 100644 plugins/obs-youtube/youtube-config.hpp create mode 100644 plugins/obs-youtube/youtube-service-info.cpp create mode 100644 plugins/obs-youtube/youtube-service.cpp create mode 100644 plugins/obs-youtube/youtube-service.hpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index feaa7c690a7d2e..7a58aee2ac9aa9 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -83,6 +83,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) add_subdirectory(obs-webrtc) check_obs_websocket() add_subdirectory(obs-x264) + add_subdirectory(obs-youtube) add_subdirectory(orphaned-services) add_subdirectory(rtmp-services) if(OS_LINUX) @@ -202,3 +203,4 @@ add_subdirectory(obs-webrtc) add_subdirectory(obs-services) add_subdirectory(custom-services) add_subdirectory(orphaned-services) +add_subdirectory(obs-youtube) diff --git a/plugins/obs-youtube/CMakeLists.txt b/plugins/obs-youtube/CMakeLists.txt new file mode 100644 index 00000000000000..75e2610d06ddae --- /dev/null +++ b/plugins/obs-youtube/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(obs-youtube MODULE) +add_library(OBS::obs-youtube ALIAS obs-youtube) + +target_sources( + obs-youtube + PRIVATE # cmake-format: sortable + obs-youtube.cpp youtube-config.cpp youtube-config.hpp youtube-service-info.cpp youtube-service.cpp + youtube-service.hpp) + +target_link_libraries(obs-youtube PRIVATE OBS::libobs) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-youtube.rc) + target_sources(obs-youtube PRIVATE obs-youtube.rc) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs(obs-youtube PROPERTIES FOLDER plugins PREFIX "") +else() + set_target_properties(obs-youtube PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(obs-youtube) +endif() diff --git a/plugins/obs-youtube/cmake/macos/Info.plist.in b/plugins/obs-youtube/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..be2629bff70378 --- /dev/null +++ b/plugins/obs-youtube/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-youtube + CFBundleIdentifier + com.obsproject.obs-youtube + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + obs-youtube + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/obs-youtube/cmake/windows/obs-module.rc.in b/plugins/obs-youtube/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..e5dbe686cbafee --- /dev/null +++ b/plugins/obs-youtube/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS YouTube service" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-youtube" + VALUE "OriginalFilename", "obs-youtube" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-youtube/data/locale/en-US.ini b/plugins/obs-youtube/data/locale/en-US.ini new file mode 100644 index 00000000000000..49a366e13d7e50 --- /dev/null +++ b/plugins/obs-youtube/data/locale/en-US.ini @@ -0,0 +1,8 @@ +YouTube.RTMP="RTMP (legacy)" +YouTube.Ingest.Primary="Primary YouTube ingest server" +YouTube.Ingest.Backup="Backup YouTube ingest server" +YouTube.MoreInfo="More Info" +YouTube.Protocol="Protocol" +YouTube.Server="Server" +YouTube.StreamKey="Stream Key" +YouTube.GetStreamKey="Get Stream Key" diff --git a/plugins/obs-youtube/obs-youtube.cpp b/plugins/obs-youtube/obs-youtube.cpp new file mode 100644 index 00000000000000..b9bb420113e917 --- /dev/null +++ b/plugins/obs-youtube/obs-youtube.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "youtube-service.hpp" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-youtube", "en-US") + +bool obs_module_load(void) +{ + YouTubeService::Register(); + return true; +} diff --git a/plugins/obs-youtube/youtube-config.cpp b/plugins/obs-youtube/youtube-config.cpp new file mode 100644 index 00000000000000..bb30d97e374524 --- /dev/null +++ b/plugins/obs-youtube/youtube-config.cpp @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "youtube-config.hpp" + +struct YouTubeIngest { + const char *rtmp; + const char *rtmps; + const char *hls; +}; + +constexpr const char *MORE_INFO_LINK = + "https://developers.google.com/youtube/v3/live/guides/ingestion-protocol-comparison"; +constexpr const char *STREAM_KEY_LINK = + "https://www.youtube.com/live_dashboard"; + +/* Note: There is no API to fetch those easily without authentification */ +constexpr YouTubeIngest PRIMARY_INGESTS = { + "rtmp://a.rtmp.youtube.com/live2", + "rtmps://a.rtmps.youtube.com:443/live2", + "https://a.upload.youtube.com/http_upload_hls?cid={stream_key}©=0&file=out.m3u8"}; +constexpr YouTubeIngest BACKUP_INGESTS = { + "rtmp://b.rtmp.youtube.com/live2?backup=1", + "rtmps://b.rtmps.youtube.com:443/live2?backup=1", + "https://b.upload.youtube.com/http_upload_hls?cid={stream_key}©=1&file=out.m3u8"}; + +YouTubeConfig::YouTubeConfig(obs_data_t *settings, obs_service_t * /* self */) +{ + Update(settings); +} + +void YouTubeConfig::Update(obs_data_t *settings) +{ + protocol = obs_data_get_string(settings, "protocol"); + serverUrl = obs_data_get_string(settings, "server"); + + streamKey = obs_data_get_string(settings, "stream_key"); +} + +void YouTubeConfig::InfoGetDefault(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "protocol", "RTMPS"); + obs_data_set_default_string(settings, "server", PRIMARY_INGESTS.rtmps); +} + +const char *YouTubeConfig::ConnectInfo(uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + if (!serverUrl.empty()) + return serverUrl.c_str(); + break; + case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + if (!streamKey.empty()) + return streamKey.c_str(); + break; + default: + break; + } + + return nullptr; +} + +bool YouTubeConfig::CanTryToConnect() +{ + if (serverUrl.empty() || streamKey.empty()) + return false; + + return true; +} + +static bool ModifiedProtocolCb(obs_properties_t *props, obs_property_t *, + obs_data_t *settings) +{ + blog(LOG_DEBUG, "ModifiedProtocolCb"); + std::string protocol = obs_data_get_string(settings, "protocol"); + obs_property_t *p = obs_properties_get(props, "server"); + + if (protocol.empty()) + return false; + + const char *primary_ingest = nullptr; + const char *backup_ingest = nullptr; + if (protocol == "RTMPS") { + primary_ingest = PRIMARY_INGESTS.rtmps; + backup_ingest = BACKUP_INGESTS.rtmps; + + } else if (protocol == "RTMP") { + primary_ingest = PRIMARY_INGESTS.rtmp; + backup_ingest = BACKUP_INGESTS.rtmp; + } else if (protocol == "HLS") { + primary_ingest = PRIMARY_INGESTS.hls; + backup_ingest = BACKUP_INGESTS.hls; + } + + obs_property_list_clear(p); + obs_property_list_add_string( + p, obs_module_text("YouTube.Ingest.Primary"), primary_ingest); + obs_property_list_add_string( + p, obs_module_text("YouTube.Ingest.Backup"), backup_ingest); + + return true; +} + +obs_properties_t *YouTubeConfig::GetProperties() +{ + obs_properties_t *ppts = obs_properties_create(); + obs_property_t *p; + + p = obs_properties_add_list(ppts, "protocol", + obs_module_text("YouTube.Protocol"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + if (obs_is_output_protocol_registered("RTMPS")) + obs_property_list_add_string(p, "RTMPS", "RTMPS"); + + if (obs_is_output_protocol_registered("RTMP")) + obs_property_list_add_string(p, obs_module_text("YouTube.RTMP"), + "RTMP"); + + if (obs_is_output_protocol_registered("HLS")) + obs_property_list_add_string(p, "HLS", "HLS"); + + obs_property_set_modified_callback(p, ModifiedProtocolCb); + + p = obs_properties_add_button(ppts, "more_info", + obs_module_text("YouTube.MoreInfo"), + nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, (char *)MORE_INFO_LINK); + + obs_properties_add_list(ppts, "server", + obs_module_text("YouTube.Server"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + obs_properties_add_text(ppts, "stream_key", + obs_module_text("YouTube.StreamKey"), + OBS_TEXT_PASSWORD); + + p = obs_properties_add_button(ppts, "get_stream_key", + obs_module_text("YouTube.GetStreamKey"), + nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); + + return ppts; +} diff --git a/plugins/obs-youtube/youtube-config.hpp b/plugins/obs-youtube/youtube-config.hpp new file mode 100644 index 00000000000000..de41698d0eb050 --- /dev/null +++ b/plugins/obs-youtube/youtube-config.hpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class YouTubeConfig { + std::string protocol; + std::string serverUrl; + + std::string streamKey; + +public: + YouTubeConfig(obs_data_t *settings, obs_service_t *self); + inline ~YouTubeConfig(){}; + + void Update(obs_data_t *settings); + + static void InfoGetDefault(obs_data_t *settings); + + const char *Protocol() { return protocol.c_str(); }; + + const char *ConnectInfo(uint32_t type); + + bool CanTryToConnect(); + + obs_properties_t *GetProperties(); +}; diff --git a/plugins/obs-youtube/youtube-service-info.cpp b/plugins/obs-youtube/youtube-service-info.cpp new file mode 100644 index 00000000000000..cdd128800fbdb8 --- /dev/null +++ b/plugins/obs-youtube/youtube-service-info.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "youtube-service.hpp" + +#include "youtube-config.hpp" + +static const char *SUPPORTED_VIDEO_CODECS[] = {"h264", "hevc", "av1", NULL}; + +void YouTubeService::InfoFreeTypeData(void *typeData) +{ + if (typeData) + delete reinterpret_cast(typeData); +} + +const char *YouTubeService::InfoGetName(void *) +{ + return "YouTube"; +} + +void *YouTubeService::InfoCreate(obs_data_t *settings, obs_service_t *service) +{ + return reinterpret_cast(new YouTubeConfig(settings, service)); + ; +} + +void YouTubeService::InfoDestroy(void *data) +{ + if (data) + delete reinterpret_cast(data); +} + +void YouTubeService::InfoUpdate(void *data, obs_data_t *settings) +{ + YouTubeConfig *priv = reinterpret_cast(data); + if (priv) + priv->Update(settings); +} + +const char *YouTubeService::InfoGetConnectInfo(void *data, uint32_t type) +{ + YouTubeConfig *priv = reinterpret_cast(data); + if (priv) + return priv->ConnectInfo(type); + return nullptr; +} + +const char *YouTubeService::InfoGetProtocol(void *data) +{ + YouTubeConfig *priv = reinterpret_cast(data); + if (priv) + return priv->Protocol(); + return nullptr; +} + +const char **YouTubeService::InfoGetSupportedVideoCodecs(void *) +{ + return SUPPORTED_VIDEO_CODECS; +} + +bool YouTubeService::InfoCanTryToConnect(void *data) +{ + YouTubeConfig *priv = reinterpret_cast(data); + if (priv) + return priv->CanTryToConnect(); + return false; +} + +obs_properties_t *YouTubeService::InfoGetProperties(void *data) +{ + if (data) + return reinterpret_cast(data)->GetProperties(); + return nullptr; +} diff --git a/plugins/obs-youtube/youtube-service.cpp b/plugins/obs-youtube/youtube-service.cpp new file mode 100644 index 00000000000000..2db06891e2810e --- /dev/null +++ b/plugins/obs-youtube/youtube-service.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "youtube-service.hpp" + +#include "youtube-config.hpp" + +YouTubeService::YouTubeService() +{ + info.type_data = this; + info.free_type_data = InfoFreeTypeData; + + info.id = "youtube"; + info.supported_protocols = "RTMPS;RTMP;HLS"; + + info.get_name = InfoGetName; + info.create = InfoCreate; + info.destroy = InfoDestroy; + info.update = InfoUpdate; + + info.get_connect_info = InfoGetConnectInfo; + + info.get_protocol = InfoGetProtocol; + + info.can_try_to_connect = InfoCanTryToConnect; + + info.flags = 0; + + info.get_defaults = YouTubeConfig::InfoGetDefault; + info.get_properties = InfoGetProperties; + + obs_register_service(&info); +} + +YouTubeService::~YouTubeService() {} + +void YouTubeService::Register() +{ + new YouTubeService(); +} diff --git a/plugins/obs-youtube/youtube-service.hpp b/plugins/obs-youtube/youtube-service.hpp new file mode 100644 index 00000000000000..d4a2849ee80b92 --- /dev/null +++ b/plugins/obs-youtube/youtube-service.hpp @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class YouTubeService { + obs_service_info info = {0}; + + static void InfoFreeTypeData(void *typeData); + + static const char *InfoGetName(void *typeData); + static void *InfoCreate(obs_data_t *settings, obs_service_t *service); + static void InfoDestroy(void *data); + static void InfoUpdate(void *data, obs_data_t *settings); + + static const char *InfoGetConnectInfo(void *data, uint32_t type); + + static const char *InfoGetProtocol(void *data); + + static const char **InfoGetSupportedVideoCodecs(void *data); + + static bool InfoCanTryToConnect(void *data); + + static obs_properties_t *InfoGetProperties(void *data); + +public: + YouTubeService(); + ~YouTubeService(); + + static void Register(); + + void GetDefaults(obs_data_t *settings); + + obs_properties_t *GetProperties(); +}; From 97505701d647b041df5dc26d3cd8a759968e0bc2 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 13:40:34 +0200 Subject: [PATCH 24/65] custom-service: Add WHIP support --- plugins/custom-services/CMakeLists.txt | 11 +- plugins/custom-services/custom-service.c | 21 ++- plugins/custom-services/custom-services.c | 2 + plugins/custom-services/custom-whip.c | 128 ++++++++++++++++++ plugins/custom-services/data/locale/en-US.ini | 2 + 5 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 plugins/custom-services/custom-whip.c diff --git a/plugins/custom-services/CMakeLists.txt b/plugins/custom-services/CMakeLists.txt index 238ef1ca1688c6..c008fcf69ba41d 100644 --- a/plugins/custom-services/CMakeLists.txt +++ b/plugins/custom-services/CMakeLists.txt @@ -4,8 +4,15 @@ add_library(custom-services MODULE) add_library(OBS::custom-services ALIAS custom-services) target_sources( - custom-services PRIVATE # cmake-format: sortable - custom-hls.c custom-rist.c custom-service.c custom-services.c custom-srt.c rtmp-services.c) + custom-services + PRIVATE # cmake-format: sortable + custom-hls.c + custom-rist.c + custom-service.c + custom-services.c + custom-srt.c + custom-whip.c + rtmp-services.c) target_link_libraries(custom-services PRIVATE OBS::libobs) diff --git a/plugins/custom-services/custom-service.c b/plugins/custom-services/custom-service.c index 28ea319c71e9d1..7a960de4a17ece 100644 --- a/plugins/custom-services/custom-service.c +++ b/plugins/custom-services/custom-service.c @@ -12,6 +12,7 @@ struct custom_service { char *username; char *password; char *encrypt_passphrase; + char *bearer_token; }; static const char *custom_service_name(void *type_data) @@ -35,6 +36,7 @@ static void custom_service_update(void *data, obs_data_t *settings) bfree(service->username); bfree(service->password); bfree(service->encrypt_passphrase); + bfree(service->bearer_token); #define GET_STRING_SETTINGS(name) \ service->name = \ @@ -55,6 +57,8 @@ static void custom_service_update(void *data, obs_data_t *settings) GET_STRING_SETTINGS(encrypt_passphrase); + GET_STRING_SETTINGS(bearer_token); + #undef GET_STRING_SETTINGS } @@ -68,6 +72,7 @@ static void custom_service_destroy(void *data) bfree(service->username); bfree(service->password); bfree(service->encrypt_passphrase); + bfree(service->bearer_token); bfree(service); } @@ -104,7 +109,7 @@ static const char *custom_service_connect_info(void *data, uint32_t type) case OBS_SERVICE_CONNECT_INFO_ENCRYPT_PASSPHRASE: return service->encrypt_passphrase; case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: - break; + return service->bearer_token; } return NULL; @@ -147,6 +152,9 @@ bool custom_service_can_try_to_connect(void *data) return false; } + if (strcmp(service->protocol, "WHIP") == 0 && !service->bearer_token) + return false; + return true; } @@ -167,6 +175,7 @@ bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, GET_AND_HIDE(username); GET_AND_HIDE(password); GET_AND_HIDE(encrypt_passphrase); + GET_AND_HIDE(bearer_token); GET_AND_HIDE(third_party_field_manager); @@ -204,6 +213,8 @@ bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, SHOW(encrypt_passphrase); SHOW(username); SHOW(password); + } else if (strcmp(protocol, "WHIP") == 0) { + SHOW(bearer_token); } #undef SHOW @@ -216,6 +227,7 @@ bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, ERASE_DATA_IF_HIDDEN(username); ERASE_DATA_IF_HIDDEN(password); ERASE_DATA_IF_HIDDEN(encrypt_passphrase); + ERASE_DATA_IF_HIDDEN(bearer_token); #undef ERASE_DATA_IF_HIDDEN @@ -245,6 +257,8 @@ static obs_properties_t *custom_service_properties(void *data) obs_property_list_add_string(p, "SRT", "SRT"); if (obs_is_output_protocol_registered("RIST")) obs_property_list_add_string(p, "RIST", "RIST"); + if (obs_is_output_protocol_registered("WHIP")) + obs_property_list_add_string(p, "WHIP", "WHIP"); char *string = NULL; bool add_ftl = false; @@ -303,13 +317,16 @@ static obs_properties_t *custom_service_properties(void *data) ppts, "encrypt_passphrase", obs_module_text("CustomServices.EncryptPassphrase.Optional"), OBS_TEXT_PASSWORD); + obs_properties_add_text(ppts, "bearer_token", + obs_module_text("CustomServices.BearerToken"), + OBS_TEXT_PASSWORD); return ppts; } const struct obs_service_info custom_service = { .id = "custom_service", - .supported_protocols = "RTMP;RTMPS;SRT;RIST", + .supported_protocols = "RTMP;RTMPS;SRT;RIST;WHIP", .get_name = custom_service_name, .create = custom_service_create, .destroy = custom_service_destroy, diff --git a/plugins/custom-services/custom-services.c b/plugins/custom-services/custom-services.c index 65d08528dee069..b9ebb556e05f1d 100644 --- a/plugins/custom-services/custom-services.c +++ b/plugins/custom-services/custom-services.c @@ -18,6 +18,7 @@ extern struct obs_service_info custom_rtmps; extern struct obs_service_info custom_hls; extern struct obs_service_info custom_srt; extern struct obs_service_info custom_rist; +extern struct obs_service_info custom_whip; bool obs_module_load(void) { @@ -28,6 +29,7 @@ bool obs_module_load(void) obs_register_service(&custom_hls); obs_register_service(&custom_srt); obs_register_service(&custom_rist); + obs_register_service(&custom_whip); return true; } diff --git a/plugins/custom-services/custom-whip.c b/plugins/custom-services/custom-whip.c new file mode 100644 index 00000000000000..c34fbe073f6214 --- /dev/null +++ b/plugins/custom-services/custom-whip.c @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +struct whip_service { + char *server; + char *bearer_token; +}; + +static const char *whip_service_name(void *type_data) +{ + UNUSED_PARAMETER(type_data); + return obs_module_text("CustomServices.CustomWHIP"); +} + +static void whip_service_update(void *data, obs_data_t *settings) +{ + struct whip_service *service = data; + + bfree(service->server); + bfree(service->bearer_token); + +#define GET_STRING_SETTINGS(name) \ + service->name = \ + obs_data_has_user_value(settings, #name) && \ + (strcmp(obs_data_get_string(settings, #name), \ + "") != 0) \ + ? bstrdup(obs_data_get_string(settings, #name)) \ + : NULL + + GET_STRING_SETTINGS(server); + + GET_STRING_SETTINGS(bearer_token); + +#undef GET_STRING_SETTINGS +} + +static void whip_service_destroy(void *data) +{ + struct whip_service *service = data; + + bfree(service->server); + bfree(service->bearer_token); + bfree(service); +} + +static void *whip_service_create(obs_data_t *settings, obs_service_t *service) +{ + UNUSED_PARAMETER(service); + + struct whip_service *data = bzalloc(sizeof(struct whip_service)); + whip_service_update(data, settings); + + return data; +} + +static const char *whip_service_protocol(void *data) +{ + UNUSED_PARAMETER(data); + return "WHIP"; +} + +static const char *whip_service_connect_info(void *data, uint32_t type) +{ + struct whip_service *service = data; + + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + return service->server; + case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + return service->bearer_token; + default: + return NULL; + } +} + +enum obs_service_audio_track_cap whip_service_audio_track_cap(void *data) +{ + UNUSED_PARAMETER(data); + + return OBS_SERVICE_AUDIO_SINGLE_TRACK; +} + +bool whip_service_can_try_to_connect(void *data) +{ + struct whip_service *service = data; + + if (!service->server || !service->bearer_token) + return false; + + return true; +} + +static obs_properties_t *whip_service_properties(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *ppts = obs_properties_create(); + + /* Add server field */ + obs_properties_add_text(ppts, "server", + obs_module_text("CustomServices.Server"), + OBS_TEXT_DEFAULT); + + /* Add connect info fields */ + obs_properties_add_text(ppts, "bearer_token", + obs_module_text("CustomServices.BearerToken"), + OBS_TEXT_PASSWORD); + + return ppts; +} + +const struct obs_service_info custom_whip = { + .id = "custom_whip", + .flags = OBS_SERVICE_INTERNAL, + .supported_protocols = "WHIP", + .get_name = whip_service_name, + .create = whip_service_create, + .destroy = whip_service_destroy, + .update = whip_service_update, + .get_protocol = whip_service_protocol, + .get_properties = whip_service_properties, + .get_connect_info = whip_service_connect_info, + .can_try_to_connect = whip_service_can_try_to_connect, + .get_audio_track_cap = whip_service_audio_track_cap, +}; diff --git a/plugins/custom-services/data/locale/en-US.ini b/plugins/custom-services/data/locale/en-US.ini index 7167044b054fff..a93317b3f6cbee 100644 --- a/plugins/custom-services/data/locale/en-US.ini +++ b/plugins/custom-services/data/locale/en-US.ini @@ -4,12 +4,14 @@ CustomServices.StreamID.Key="Stream key" CustomServices.Username.Optional="Username (Optional)" CustomServices.Password.Optional="Password (Optional)" CustomServices.EncryptPassphrase.Optional="Encryption Passphrase (Optional)" +CustomServices.BearerToken="Bearer Token" CustomServices.CustomRTMP="Custom RTMP" CustomServices.CustomRTMPS="Custom RTMPS" CustomServices.CustomHLS="Custom HLS" CustomServices.HLS.Warning="Replace the stream key in the server field with \"{stream_key}\" and put the key in the dedicated field" CustomServices.CustomSRT="Custom SRT" CustomServices.CustomRIST="Custom RIST" +CustomServices.CustomWHIP="Custom WHIP" CustomServices.CustomService.Name="Custom…" CustomServices.CustomService.Protocol="Protocol" CustomServices.CustomService.FTL="FTL (Deprecated)" From fc68ffc2477481570a4aec1cdc00f0c0698456b7 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 14:01:02 +0200 Subject: [PATCH 25/65] obs-services,json-schema: Add WHIP Support --- build-aux/json-schema/protocolDefs.json | 4 ++-- plugins/obs-services/data/locale/en-US.ini | 1 + plugins/obs-services/generated/services-json.hpp | 4 +++- plugins/obs-services/service-config.cpp | 8 ++++++++ plugins/obs-services/service-config.hpp | 2 ++ plugins/obs-services/service-instance.cpp | 7 +++++++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/build-aux/json-schema/protocolDefs.json b/build-aux/json-schema/protocolDefs.json index bc4df9bb5d7bcc..a7a19e6ff2a81c 100644 --- a/build-aux/json-schema/protocolDefs.json +++ b/build-aux/json-schema/protocolDefs.json @@ -7,11 +7,11 @@ "$defs": { "protocolEnum": { "$comment": "Enumeration of protocols", - "enum": ["RTMP","RTMPS","HLS","SRT","RIST"] + "enum": ["RTMP","RTMPS","HLS","SRT","RIST","WHIP"] }, "protocolMapEnum": { "$comment": "Enumeration of protocols (with '*' as any) that have various compatible codecs", - "enum": ["*","RTMP","RTMPS","HLS","SRT","RIST"] + "enum": ["*","RTMP","RTMPS","HLS","SRT","RIST","WHIP"] }, "serverPattern": { "$comment": "Pattern to enforce a supported server URL", diff --git a/plugins/obs-services/data/locale/en-US.ini b/plugins/obs-services/data/locale/en-US.ini index 96c672ff988f6b..44ae5a4a7b4441 100644 --- a/plugins/obs-services/data/locale/en-US.ini +++ b/plugins/obs-services/data/locale/en-US.ini @@ -8,3 +8,4 @@ Services.GetStreamKey="Get Stream Key" Services.Username="Username" Services.Password="Password" Services.EncryptPassphrase="Encryption Passphrase" +Services.BearerToken="Bearer Token" diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp index 015b8a639ad14f..a2162bf983c748 100644 --- a/plugins/obs-services/generated/services-json.hpp +++ b/plugins/obs-services/generated/services-json.hpp @@ -92,7 +92,7 @@ namespace OBSServices { bool srpUsernamePassword; }; - enum class ServerProtocol : int { HLS, RIST, RTMP, RTMPS, SRT }; + enum class ServerProtocol : int { HLS, RIST, RTMP, RTMPS, SRT, WHIP }; struct Server { /** @@ -307,6 +307,7 @@ namespace OBSServices { else if (j == "RTMP") x = ServerProtocol::RTMP; else if (j == "RTMPS") x = ServerProtocol::RTMPS; else if (j == "SRT") x = ServerProtocol::SRT; + else if (j == "WHIP") x = ServerProtocol::WHIP; else { throw std::runtime_error("Input JSON does not conform to schema!"); } } @@ -317,6 +318,7 @@ namespace OBSServices { case ServerProtocol::RTMP: j = "RTMP"; break; case ServerProtocol::RTMPS: j = "RTMPS"; break; case ServerProtocol::SRT: j = "SRT"; break; + case ServerProtocol::WHIP: j = "WHIP"; break; default: throw std::runtime_error("This should not happen"); } } diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index 6cf9601ae06431..3bd5e66e0e6c1c 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -38,6 +38,8 @@ void ServiceConfig::Update(obs_data_t *settings) password = obs_data_get_string(settings, "password"); encryptPassphrase = obs_data_get_string(settings, "encrypt_passphrase"); + + bearerToken = obs_data_get_string(settings, "bearer_token"); } const char *ServiceConfig::ConnectInfo(uint32_t type) @@ -64,6 +66,8 @@ const char *ServiceConfig::ConnectInfo(uint32_t type) return encryptPassphrase.c_str(); break; case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: + if (!bearerToken.empty()) + return bearerToken.c_str(); break; } @@ -108,6 +112,10 @@ bool ServiceConfig::CanTryToConnect() break; } + case OBSServices::ServerProtocol::WHIP: + if (bearerToken.empty()) + return false; + break; } return true; diff --git a/plugins/obs-services/service-config.hpp b/plugins/obs-services/service-config.hpp index 1afc5260d57aa3..8ac9130fceac2e 100644 --- a/plugins/obs-services/service-config.hpp +++ b/plugins/obs-services/service-config.hpp @@ -25,6 +25,8 @@ class ServiceConfig { std::string encryptPassphrase; + std::string bearerToken; + public: ServiceConfig(obs_data_t *settings, obs_service_t *self); inline ~ServiceConfig(){}; diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp index cb31639fc07b5b..f04a9ea7577a81 100644 --- a/plugins/obs-services/service-instance.cpp +++ b/plugins/obs-services/service-instance.cpp @@ -197,6 +197,7 @@ bool ModifiedProtocolCb(void *service_, obs_properties_t *props, ADD_TO_MAP("username"); ADD_TO_MAP("password"); ADD_TO_MAP("encrypt_passphrase"); + ADD_TO_MAP("bearer_token"); #undef ADD_TO_MAP if (propGetStreamKey) @@ -242,6 +243,9 @@ bool ModifiedProtocolCb(void *service_, obs_properties_t *props, hasProps ? service->rist->srpUsernamePassword : false); break; } + case OBSServices::ServerProtocol::WHIP: + obs_property_set_visible(properties["bearer_token"], true); + break; } return true; @@ -332,6 +336,9 @@ obs_properties_t *ServiceInstance::GetProperties() obs_properties_add_text(ppts, "encrypt_passphrase", obs_module_text("Services.EncryptPassphrase"), OBS_TEXT_PASSWORD); + obs_properties_add_text(ppts, "bearer_token", + obs_module_text("Services.BearerToken"), + OBS_TEXT_PASSWORD); return ppts; } From 5c79aa288e14858a0511707f5a5d411275484b1b Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 14:06:18 +0200 Subject: [PATCH 26/65] obs-webrtc: Remove WHIP custom service This type of services is provided through the custom-services plugin. --- plugins/obs-webrtc/CMakeLists.txt | 3 +- plugins/obs-webrtc/cmake/legacy.cmake | 3 +- plugins/obs-webrtc/data/locale/en-US.ini | 2 - plugins/obs-webrtc/obs-webrtc.cpp | 2 - plugins/obs-webrtc/whip-service.cpp | 108 ----------------------- plugins/obs-webrtc/whip-service.h | 21 ----- 6 files changed, 2 insertions(+), 137 deletions(-) delete mode 100644 plugins/obs-webrtc/whip-service.cpp delete mode 100644 plugins/obs-webrtc/whip-service.h diff --git a/plugins/obs-webrtc/CMakeLists.txt b/plugins/obs-webrtc/CMakeLists.txt index c16fb995f6fbb0..d0d045f57c9671 100644 --- a/plugins/obs-webrtc/CMakeLists.txt +++ b/plugins/obs-webrtc/CMakeLists.txt @@ -14,8 +14,7 @@ find_package(CURL REQUIRED) add_library(obs-webrtc MODULE) add_library(OBS::webrtc ALIAS obs-webrtc) -target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-service.cpp whip-service.h - whip-utils.h) +target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-utils.h) target_link_libraries(obs-webrtc PRIVATE OBS::libobs LibDataChannel::LibDataChannel CURL::libcurl) diff --git a/plugins/obs-webrtc/cmake/legacy.cmake b/plugins/obs-webrtc/cmake/legacy.cmake index c2e3bef6d98f5d..a0e053b9d218ad 100644 --- a/plugins/obs-webrtc/cmake/legacy.cmake +++ b/plugins/obs-webrtc/cmake/legacy.cmake @@ -12,8 +12,7 @@ find_package(CURL REQUIRED) add_library(obs-webrtc MODULE) add_library(OBS::webrtc ALIAS obs-webrtc) -target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-service.cpp whip-service.h - whip-utils.h) +target_sources(obs-webrtc PRIVATE obs-webrtc.cpp whip-output.cpp whip-output.h whip-utils.h) target_link_libraries(obs-webrtc PRIVATE OBS::libobs LibDataChannel::LibDataChannel CURL::libcurl) diff --git a/plugins/obs-webrtc/data/locale/en-US.ini b/plugins/obs-webrtc/data/locale/en-US.ini index 617c20e2ee5d42..60d76c81964dcd 100644 --- a/plugins/obs-webrtc/data/locale/en-US.ini +++ b/plugins/obs-webrtc/data/locale/en-US.ini @@ -1,3 +1 @@ Output.Name="WHIP Output" -Service.Name="WHIP Service" -Service.BearerToken="Bearer Token" diff --git a/plugins/obs-webrtc/obs-webrtc.cpp b/plugins/obs-webrtc/obs-webrtc.cpp index ebb2eb4fdc751e..ea22e53a621594 100644 --- a/plugins/obs-webrtc/obs-webrtc.cpp +++ b/plugins/obs-webrtc/obs-webrtc.cpp @@ -1,7 +1,6 @@ #include #include "whip-output.h" -#include "whip-service.h" OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("obs-webrtc", "en-US") @@ -13,7 +12,6 @@ MODULE_EXPORT const char *obs_module_description(void) bool obs_module_load() { register_whip_output(); - register_whip_service(); return true; } diff --git a/plugins/obs-webrtc/whip-service.cpp b/plugins/obs-webrtc/whip-service.cpp deleted file mode 100644 index 7e73bd63dd74c6..00000000000000 --- a/plugins/obs-webrtc/whip-service.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "whip-service.h" - -const char *audio_codecs[MAX_CODECS] = {"opus"}; -const char *video_codecs[MAX_CODECS] = {"h264"}; - -WHIPService::WHIPService(obs_data_t *settings, obs_service_t *) - : server(), - bearer_token() -{ - Update(settings); -} - -void WHIPService::Update(obs_data_t *settings) -{ - server = obs_data_get_string(settings, "server"); - bearer_token = obs_data_get_string(settings, "bearer_token"); -} - -obs_properties_t *WHIPService::Properties() -{ - obs_properties_t *ppts = obs_properties_create(); - - obs_properties_add_text(ppts, "server", "URL", OBS_TEXT_DEFAULT); - obs_properties_add_text(ppts, "bearer_token", - obs_module_text("Service.BearerToken"), - OBS_TEXT_PASSWORD); - - return ppts; -} - -void WHIPService::ApplyEncoderSettings(obs_data_t *video_settings, obs_data_t *) -{ - // For now, ensure maximum compatibility with webrtc peers - if (video_settings) { - obs_data_set_int(video_settings, "bf", 0); - obs_data_set_string(video_settings, "rate_control", "CBR"); - obs_data_set_bool(video_settings, "repeat_headers", true); - } -} - -const char *WHIPService::GetConnectInfo(enum obs_service_connect_info type) -{ - switch (type) { - case OBS_SERVICE_CONNECT_INFO_SERVER_URL: - return server.c_str(); - case OBS_SERVICE_CONNECT_INFO_BEARER_TOKEN: - return bearer_token.c_str(); - default: - return nullptr; - } -} - -bool WHIPService::CanTryToConnect() -{ - return !server.empty(); -} - -void register_whip_service() -{ - struct obs_service_info info = {}; - - info.id = "whip_custom"; - info.get_name = [](void *) -> const char * { - return obs_module_text("Service.Name"); - }; - info.create = [](obs_data_t *settings, - obs_service_t *service) -> void * { - return new WHIPService(settings, service); - }; - info.destroy = [](void *priv_data) { - delete static_cast(priv_data); - }; - info.update = [](void *priv_data, obs_data_t *settings) { - static_cast(priv_data)->Update(settings); - }; - info.get_properties = [](void *) -> obs_properties_t * { - return WHIPService::Properties(); - }; - info.get_protocol = [](void *) -> const char * { - return "WHIP"; - }; - info.get_url = [](void *priv_data) -> const char * { - return static_cast(priv_data)->server.c_str(); - }; - info.get_output_type = [](void *) -> const char * { - return "whip_output"; - }; - info.apply_encoder_settings = [](void *, obs_data_t *video_settings, - obs_data_t *audio_settings) { - WHIPService::ApplyEncoderSettings(video_settings, - audio_settings); - }; - info.get_supported_video_codecs = [](void *) -> const char ** { - return video_codecs; - }; - info.get_supported_audio_codecs = [](void *) -> const char ** { - return audio_codecs; - }; - info.can_try_to_connect = [](void *priv_data) -> bool { - return static_cast(priv_data)->CanTryToConnect(); - }; - info.get_connect_info = [](void *priv_data, - uint32_t type) -> const char * { - return static_cast(priv_data)->GetConnectInfo( - (enum obs_service_connect_info)type); - }; - obs_register_service(&info); -} diff --git a/plugins/obs-webrtc/whip-service.h b/plugins/obs-webrtc/whip-service.h deleted file mode 100644 index 28e6a7c6cdbdb5..00000000000000 --- a/plugins/obs-webrtc/whip-service.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include -#include - -#define MAX_CODECS 3 - -struct WHIPService { - std::string server; - std::string bearer_token; - - WHIPService(obs_data_t *settings, obs_service_t *service); - - void Update(obs_data_t *settings); - static obs_properties_t *Properties(); - static void ApplyEncoderSettings(obs_data_t *video_settings, - obs_data_t *audio_settings); - bool CanTryToConnect(); - const char *GetConnectInfo(enum obs_service_connect_info type); -}; - -void register_whip_service(); From 4e991a16102b47b933dfedbd801c45080f3ebd48 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 14:11:09 +0200 Subject: [PATCH 27/65] deps: Move obf in its own subdirectory --- UI/CMakeLists.txt | 9 ++++++--- UI/auth-restream.cpp | 2 +- UI/auth-twitch.cpp | 2 +- UI/auth-youtube.cpp | 2 +- UI/cmake/legacy.cmake | 17 +++++++++++++---- UI/obf.h | 13 ------------- UI/youtube-api-wrappers.cpp | 2 +- deps/obf/CMakeLists.txt | 7 +++++++ {UI => deps/obf}/obf.c | 4 ++++ deps/obf/obf.h | 17 +++++++++++++++++ 10 files changed, 51 insertions(+), 24 deletions(-) delete mode 100644 UI/obf.h create mode 100644 deps/obf/CMakeLists.txt rename {UI => deps/obf}/obf.c (81%) create mode 100644 deps/obf/obf.h diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 10de47680421f5..973f698bb6c3f0 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -24,6 +24,10 @@ if(NOT TARGET OBS::json11) add_subdirectory("${CMAKE_SOURCE_DIR}/deps/json11" "${CMAKE_BINARY_DIR}/deps/json11") endif() +if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") +endif() + add_executable(obs-studio) add_executable(OBS::studio ALIAS obs-studio) @@ -36,7 +40,8 @@ target_link_libraries( OBS::libobs OBS::frontend-api OBS::libff-util - OBS::json11) + OBS::json11 + OBS::obf) include(cmake/ui-qt.cmake) include(cmake/ui-elements.cmake) @@ -73,8 +78,6 @@ target_sources( display-helpers.hpp multiview.cpp multiview.hpp - obf.c - obf.h obs-app.cpp obs-app.hpp obs-proxy-style.cpp diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp index 9d7c4be4d8b108..de3ab60e21309a 100644 --- a/UI/auth-restream.cpp +++ b/UI/auth-restream.cpp @@ -13,7 +13,7 @@ #include "window-basic-main.hpp" #include "remote-text.hpp" #include "ui-config.h" -#include "obf.h" +#include using namespace json11; diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp index c7a2dbe6f0cebf..28d7f5e1112b81 100644 --- a/UI/auth-twitch.cpp +++ b/UI/auth-twitch.cpp @@ -16,7 +16,7 @@ #include #include "ui-config.h" -#include "obf.h" +#include using namespace json11; diff --git a/UI/auth-youtube.cpp b/UI/auth-youtube.cpp index de88a999c1a521..4dc7b8f1cee0ba 100644 --- a/UI/auth-youtube.cpp +++ b/UI/auth-youtube.cpp @@ -22,7 +22,7 @@ #include "ui-config.h" #include "youtube-api-wrappers.hpp" #include "window-basic-main.hpp" -#include "obf.h" +#include #ifdef BROWSER_AVAILABLE #include "window-dock-browser.hpp" diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 236717164acfca..5cb2219c7f7404 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -130,8 +130,6 @@ target_sources( auth-oauth.hpp auth-listener.cpp auth-listener.hpp - obf.c - obf.h obs-app.cpp obs-app.hpp obs-proxy-style.cpp @@ -293,8 +291,19 @@ target_compile_features(obs PRIVATE cxx_std_17) target_include_directories(obs PRIVATE ${CMAKE_SOURCE_DIR}/deps/json11 ${CMAKE_SOURCE_DIR}/deps/libff) -target_link_libraries(obs PRIVATE CURL::libcurl FFmpeg::avcodec FFmpeg::avutil FFmpeg::avformat OBS::libobs - OBS::frontend-api) +if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") +endif() + +target_link_libraries( + obs + PRIVATE CURL::libcurl + FFmpeg::avcodec + FFmpeg::avutil + FFmpeg::avformat + OBS::libobs + OBS::frontend-api + OBS::obf) set_target_properties(obs PROPERTIES FOLDER "frontend") diff --git a/UI/obf.h b/UI/obf.h deleted file mode 100644 index e264e19c43b22b..00000000000000 --- a/UI/obf.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -extern void deobfuscate_str(char *str, uint64_t val); - -#ifdef __cplusplus -} -#endif diff --git a/UI/youtube-api-wrappers.cpp b/UI/youtube-api-wrappers.cpp index 876ecad4f26495..8b79276cbb0bc4 100644 --- a/UI/youtube-api-wrappers.cpp +++ b/UI/youtube-api-wrappers.cpp @@ -12,7 +12,7 @@ #include "qt-wrappers.hpp" #include "remote-text.hpp" #include "ui-config.h" -#include "obf.h" +#include using namespace json11; diff --git a/deps/obf/CMakeLists.txt b/deps/obf/CMakeLists.txt new file mode 100644 index 00000000000000..d69a0a8ca1a345 --- /dev/null +++ b/deps/obf/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(obf INTERFACE) +add_library(OBS::obf ALIAS obf) + +target_sources(obf INTERFACE obf.c obf.h) +target_include_directories(obf INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/UI/obf.c b/deps/obf/obf.c similarity index 81% rename from UI/obf.c rename to deps/obf/obf.c index a5bf6fe2ae4db8..1ac7c342087360 100644 --- a/UI/obf.c +++ b/deps/obf/obf.c @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Lain Bailey +// +// SPDX-License-Identifier: GPL-2.0-or-later + #include "obf.h" #include diff --git a/deps/obf/obf.h b/deps/obf/obf.h new file mode 100644 index 00000000000000..cfe43ec5cd9fdd --- /dev/null +++ b/deps/obf/obf.h @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Lain Bailey +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void deobfuscate_str(char *str, uint64_t val); + +#ifdef __cplusplus +} +#endif From 4f2ace00fc87fb8bad893f44f654c3cff0b1dc4a Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 15 Jun 2023 15:35:37 +0200 Subject: [PATCH 28/65] deps: Add OAuth This add the necessary to do OAuth Authorization Code Grants --- deps/oauth-service/oauth/CMakeLists.txt | 20 ++ deps/oauth-service/oauth/oauth.cpp | 294 ++++++++++++++++++++++++ deps/oauth-service/oauth/oauth.hpp | 165 +++++++++++++ 3 files changed, 479 insertions(+) create mode 100644 deps/oauth-service/oauth/CMakeLists.txt create mode 100644 deps/oauth-service/oauth/oauth.cpp create mode 100644 deps/oauth-service/oauth/oauth.hpp diff --git a/deps/oauth-service/oauth/CMakeLists.txt b/deps/oauth-service/oauth/CMakeLists.txt new file mode 100644 index 00000000000000..b4e84945129fc0 --- /dev/null +++ b/deps/oauth-service/oauth/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +find_package(CURL) +find_package(nlohmann_json) + +if(NOT TARGET OBS::curl-helper) + add_library(curl-helper INTERFACE) + add_library(OBS::curl-helper ALIAS curl-helper) + target_sources(curl-helper INTERFACE "${CMAKE_SOURCE_DIR}/libobs/util/curl/curl-helper.h") + target_include_directories(curl-helper INTERFACE "${CMAKE_SOURCE_DIR}/libobs/util/curl") + target_link_libraries(curl-helper INTERFACE CURL::libcurl) +endif() + +add_library(oauth INTERFACE) +add_library(OBS::oauth ALIAS oauth) + +target_sources(oauth INTERFACE oauth.cpp oauth.hpp) +target_include_directories(oauth INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(oauth INTERFACE OBS::curl-helper CURL::libcurl nlohmann_json::nlohmann_json) diff --git a/deps/oauth-service/oauth/oauth.cpp b/deps/oauth-service/oauth/oauth.cpp new file mode 100644 index 00000000000000..c5c297021f28e6 --- /dev/null +++ b/deps/oauth-service/oauth/oauth.cpp @@ -0,0 +1,294 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "oauth.hpp" + +#include + +RequestError::RequestError(const RequestErrorType &type, + const std::string &error) + : type(type), error(error) +{ + switch (type) { + case RequestErrorType::UNKNOWN_OR_CUSTOM: + break; + case RequestErrorType::CURL_REQUEST_FAILED: + message = "Failed Curl request"; + break; + case RequestErrorType::JSON_PARSING_FAILED: + message = "Failed to parse JSON response"; + break; + case RequestErrorType::OAUTH_REQUEST_FAILED: + message = "Failed OAuth request"; + break; + case RequestErrorType::ERROR_JSON_PARSING_FAILED: + message = "Failed to parse JSON error response"; + break; + case RequestErrorType::UNMANAGED_HTTP_RESPONSE_CODE: + message = "Request returned an unexpected HTTP reponse"; + break; + } +} + +namespace OAuth { + +static std::string +AccessTokenErrorToQuotedStdString(const AccessTokenError &error) +{ + switch (error) { + case AccessTokenError::INVALID_REQUEST: + return "\"invalid_request\""; + case AccessTokenError::INVALID_CLIENT: + return "\"invalid_client\""; + case AccessTokenError::INVALID_GRANT: + return "\"invalid_grant\""; + case AccessTokenError::UNAUTHORIZED_CLIENT: + return "\"unauthorized_client\""; + case AccessTokenError::UNSUPPORTED_GRANT_TYPE: + return "\"unsupported_grant_type\""; + case AccessTokenError::INVALID_SCOPE: + return "\"invalid_scope\""; + } + + /* NOTE: It will never go there but GCC and MSVC do not see it this way */ + return {}; +} + +static bool AccessTokenRequestInternal(AccessTokenResponse &response, + RequestError &error, + const std::string &userAgent, + const std::string &tokenUrl, + const std::string &postData) +{ + std::string output; + std::string errorStr; + long responseCode = 0; + + CURLcode curlCode = GetRemoteFile( + userAgent.c_str(), tokenUrl.c_str(), output, errorStr, + &responseCode, "application/x-www-form-urlencoded", "", + postData.c_str(), std::vector(), nullptr, 5); + + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + error = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + errorStr); + return false; + } + + switch (responseCode) { + case 200: + try { + response = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + error = RequestError( + RequestErrorType::JSON_PARSING_FAILED, + e.what()); + } + break; + case 400: + try { + AccessTokenErrorResponse errorResponse = + nlohmann::json::parse(output); + error = RequestError( + RequestErrorType::OAUTH_REQUEST_FAILED, + errorResponse.errorDescription.value_or( + AccessTokenErrorToQuotedStdString( + errorResponse.error))); + } catch (nlohmann::json::exception &e) { + error = RequestError( + RequestErrorType::ERROR_JSON_PARSING_FAILED, + e.what()); + } + break; + default: + error = RequestError( + RequestErrorType::UNMANAGED_HTTP_RESPONSE_CODE, + errorStr); + break; + } + + return false; +} + +bool AccessTokenRequest(AccessTokenResponse &response, RequestError &error, + const std::string &userAgent, + const std::string &tokenUrl, const std::string &code, + const std::string &redirectUri, + const std::string &clientId, + const std::string &clientSecret) +{ + std::string postData = "action=redirect&client_id="; + postData += clientId; + /* NOTE: In RFC 6749, Authorization Code Grant do not list client_secret in its request format */ + if (!clientSecret.empty()) { + postData += "&client_secret="; + postData += clientSecret; + } + if (!redirectUri.empty()) { + postData += "&redirect_uri="; + postData += redirectUri; + } + postData += "&grant_type=authorization_code&code="; + postData += code; + + return AccessTokenRequestInternal(response, error, userAgent, tokenUrl, + postData); +} + +bool RefreshAccessToken(AccessTokenResponse &response, RequestError &error, + const std::string &userAgent, + const std::string &tokenUrl, + const std::string &refreshToken, + const std::string &clientId, + const std::string &clientSecret) +{ + std::string postData = "action=redirect&client_id="; + postData += clientId; + /* NOTE: In RFC 6749, Authorization Code Grant do not list client_secret in its request format */ + if (!clientSecret.empty()) { + postData += "&client_secret="; + postData += clientSecret; + } + postData += "&grant_type=refresh_token&refresh_token="; + postData += refreshToken; + + return AccessTokenRequestInternal(response, error, userAgent, tokenUrl, + postData); +} + +} + +/***************************************************/ + +static auto curl_deleter = [](CURL *curl) { curl_easy_cleanup(curl); }; +using Curl = std::unique_ptr; + +static size_t string_write(char *ptr, size_t size, size_t nmemb, + std::string &str) +{ + size_t total = size * nmemb; + if (total) + str.append(ptr, total); + + return total; +} + +static size_t header_write(char *ptr, size_t size, size_t nmemb, + std::vector &list) +{ + std::string str; + + size_t total = size * nmemb; + if (total) + str.append(ptr, total); + + if (str.back() == '\n') + str.resize(str.size() - 1); + if (str.back() == '\r') + str.resize(str.size() - 1); + + list.push_back(std::move(str)); + return total; +} + +CURLcode GetRemoteFile(const char *userAgent_, const char *url, + std::string &str, std::string &error, long *responseCode, + const char *contentType, std::string request_type, + const char *postData, + std::vector extraHeaders, + std::string *signature, int timeoutSec, int postDataSize) +{ + std::vector header_in_list; + char error_in[CURL_ERROR_SIZE]; + + error_in[0] = 0; + + std::string userAgent("User-Agent: "); + userAgent += userAgent_; + + std::string contentTypeString; + if (contentType) { + contentTypeString += "Content-Type: "; + contentTypeString += contentType; + } + + Curl curl{curl_easy_init(), curl_deleter}; + if (!curl) + return CURLE_FAILED_INIT; + + CURLcode code = CURLE_FAILED_INIT; + struct curl_slist *header = nullptr; + + header = curl_slist_append(header, userAgent.c_str()); + + if (!contentTypeString.empty()) { + header = curl_slist_append(header, contentTypeString.c_str()); + } + + for (std::string &h : extraHeaders) + header = curl_slist_append(header, h.c_str()); + + curl_easy_setopt(curl.get(), CURLOPT_URL, url); + curl_easy_setopt(curl.get(), CURLOPT_ACCEPT_ENCODING, ""); + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, header); + curl_easy_setopt(curl.get(), CURLOPT_ERRORBUFFER, error_in); + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, string_write); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &str); + curl_obs_set_revoke_setting(curl.get()); + + if (signature) { + curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, + header_write); + curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, + &header_in_list); + } + + if (timeoutSec) + curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, timeoutSec); + + if (!request_type.empty()) { + if (request_type != "GET") + curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, + request_type.c_str()); + + // Special case of "POST" + if (request_type == "POST") { + curl_easy_setopt(curl.get(), CURLOPT_POST, 1); + if (!postData) + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, + "{}"); + } + } + if (postData) { + if (postDataSize > 0) { + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, + (long)postDataSize); + } + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, postData); + } + + code = curl_easy_perform(curl.get()); + if (responseCode) + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, + responseCode); + + if (code != CURLE_OK) { + error = strlen(error_in) ? error_in : curl_easy_strerror(code); + } else if (signature) { + for (std::string &h : header_in_list) { + std::string name = h.substr(0, 13); + // HTTP headers are technically case-insensitive + if (name == "X-Signature: " || + name == "x-signature: ") { + *signature = h.substr(13); + break; + } + } + } + + curl_slist_free_all(header); + + return code; +} diff --git a/deps/oauth-service/oauth/oauth.hpp b/deps/oauth-service/oauth/oauth.hpp new file mode 100644 index 00000000000000..082131c9224329 --- /dev/null +++ b/deps/oauth-service/oauth/oauth.hpp @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#ifndef NLOHMANN_OPT_HELPER +#define NLOHMANN_OPT_HELPER +namespace nlohmann { +template struct adl_serializer> { + static void to_json(json &j, const std::optional &opt) + { + if (!opt) + j = nullptr; + else + j = *opt; + } + + static std::optional from_json(const json &j) + { + if (j.is_null()) + return std::make_optional(); + else + return std::make_optional(j.get()); + } +}; +} +#endif + +enum class RequestErrorType : int { + UNKNOWN_OR_CUSTOM, + CURL_REQUEST_FAILED, + JSON_PARSING_FAILED, + OAUTH_REQUEST_FAILED, + ERROR_JSON_PARSING_FAILED, + UNMANAGED_HTTP_RESPONSE_CODE, +}; + +struct RequestError { + RequestErrorType type = RequestErrorType::UNKNOWN_OR_CUSTOM; + std::string message; + std::string error; + + RequestError() {} + + RequestError(const RequestErrorType &type, const std::string &error); + + RequestError(const std::string &message, const std::string &error) + : message(message), error(error) + { + } +}; + +namespace OAuth { +using nlohmann::json; + +#ifndef NLOHMANN_OPTIONAL_OAuth_HELPER +#define NLOHMANN_OPTIONAL_OAuth_HELPER +template +inline std::optional get_stack_optional(const json &j, const char *property) +{ + auto it = j.find(property); + if (it != j.end() && !it->is_null()) { + return j.at(property).get>(); + } + return std::optional(); +} +#endif + +/* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4 + * https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 */ +struct AccessTokenResponse { + std::string accessToken; + std::string tokenType; + std::optional expiresIn; + std::optional refreshToken; + std::optional scope; +}; + +/* https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 */ +enum class AccessTokenError : int { + INVALID_REQUEST, + INVALID_CLIENT, + INVALID_GRANT, + UNAUTHORIZED_CLIENT, + UNSUPPORTED_GRANT_TYPE, + INVALID_SCOPE, +}; + +/* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4 + * https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 */ +struct AccessTokenErrorResponse { + AccessTokenError error; + std::optional errorDescription; + std::optional errorUri; +}; + +inline void from_json(const json &j, AccessTokenResponse &s) +{ + s.accessToken = j.at("access_token").get(); + /* NOTE: token_type is case insensitive, force to lower for easier compare check */ + s.tokenType = j.at("token_type").get(); + std::transform(s.tokenType.begin(), s.tokenType.end(), + s.tokenType.begin(), + [](unsigned char c) { return std::tolower(c); }); + s.expiresIn = get_stack_optional(j, "expires_in"); + s.refreshToken = get_stack_optional(j, "refresh_token"); + s.scope = get_stack_optional(j, "scope"); +} + +inline void from_json(const json &j, AccessTokenError &e) +{ + if (j == "invalid_request") + e = AccessTokenError::INVALID_REQUEST; + else if (j == "invalid_client") + e = AccessTokenError::INVALID_CLIENT; + else if (j == "invalid_grant") + e = AccessTokenError::INVALID_GRANT; + else if (j == "unauthorized_client") + e = AccessTokenError::UNAUTHORIZED_CLIENT; + else if (j == "unsupported_grant_type") + e = AccessTokenError::UNSUPPORTED_GRANT_TYPE; + else if (j == "invalid_scope") + e = AccessTokenError::INVALID_SCOPE; + else { + throw std::runtime_error( + "Input JSON does not conform to the RFC 6749!"); + } +} + +inline void from_json(const json &j, AccessTokenErrorResponse &s) +{ + s.error = j.at("error").get(); + s.errorDescription = + get_stack_optional(j, "error_description"); + s.errorUri = get_stack_optional(j, "error_uri"); +} + +bool AccessTokenRequest(AccessTokenResponse &response, RequestError &error, + const std::string &userAgent, + const std::string &tokenUrl, const std::string &code, + const std::string &redirectUri, + const std::string &clientId, + const std::string &clientSecret = {}); +bool RefreshAccessToken(AccessTokenResponse &response, RequestError &error, + const std::string &userAgent, + const std::string &tokenUrl, + const std::string &refreshToken, + const std::string &clientId, + const std::string &clientSecret = {}); + +} + +CURLcode GetRemoteFile( + const char *userAgent, const char *url, std::string &str, + std::string &error, long *responseCode = nullptr, + const char *contentType = nullptr, std::string request_type = "", + const char *postData = nullptr, + std::vector extraHeaders = std::vector(), + std::string *signature = nullptr, int timeoutSec = 0, + int postDataSize = 0); From 08adc0924f18ac93021ace43e094635af55776b6 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 15 Jun 2023 15:48:30 +0200 Subject: [PATCH 29/65] oauth-service: Add OAuth service base Common code for service integration plugins --- deps/oauth-service/oauth/oauth.cpp | 4 + deps/oauth-service/oauth/oauth.hpp | 1 + .../oauth-service/service-base/CMakeLists.txt | 13 + .../service-base/oauth-service-base.cpp | 344 ++++++++++++++++++ .../service-base/oauth-service-base.hpp | 98 +++++ 5 files changed, 460 insertions(+) create mode 100644 deps/oauth-service/service-base/CMakeLists.txt create mode 100644 deps/oauth-service/service-base/oauth-service-base.cpp create mode 100644 deps/oauth-service/service-base/oauth-service-base.hpp diff --git a/deps/oauth-service/oauth/oauth.cpp b/deps/oauth-service/oauth/oauth.cpp index c5c297021f28e6..a9c82fd95b23b8 100644 --- a/deps/oauth-service/oauth/oauth.cpp +++ b/deps/oauth-service/oauth/oauth.cpp @@ -28,6 +28,10 @@ RequestError::RequestError(const RequestErrorType &type, case RequestErrorType::UNMANAGED_HTTP_RESPONSE_CODE: message = "Request returned an unexpected HTTP reponse"; break; + case RequestErrorType::MISSING_REQUIRED_OPT_PARAMETER: + message = + "Request response is missing a required (optional by spec) parameter"; + break; } } diff --git a/deps/oauth-service/oauth/oauth.hpp b/deps/oauth-service/oauth/oauth.hpp index 082131c9224329..c1386762dc75d6 100644 --- a/deps/oauth-service/oauth/oauth.hpp +++ b/deps/oauth-service/oauth/oauth.hpp @@ -38,6 +38,7 @@ enum class RequestErrorType : int { OAUTH_REQUEST_FAILED, ERROR_JSON_PARSING_FAILED, UNMANAGED_HTTP_RESPONSE_CODE, + MISSING_REQUIRED_OPT_PARAMETER, }; struct RequestError { diff --git a/deps/oauth-service/service-base/CMakeLists.txt b/deps/oauth-service/service-base/CMakeLists.txt new file mode 100644 index 00000000000000..6fb4982df71e97 --- /dev/null +++ b/deps/oauth-service/service-base/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +if(NOT TARGET OBS::oauth) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/oauth" "${CMAKE_BINARY_DIR}/deps/oauth-service/oauth") +endif() + +add_library(oauth-service-base INTERFACE) +add_library(OBS::oauth-service-base ALIAS oauth-service-base) + +target_sources(oauth-service-base INTERFACE oauth-service-base.cpp oauth-service-base.hpp) +target_include_directories(oauth-service-base INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(oauth-service-base INTERFACE OBS::oauth OBS::frontend-api) diff --git a/deps/oauth-service/service-base/oauth-service-base.cpp b/deps/oauth-service/service-base/oauth-service-base.cpp new file mode 100644 index 00000000000000..11a460916c95d6 --- /dev/null +++ b/deps/oauth-service/service-base/oauth-service-base.cpp @@ -0,0 +1,344 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "oauth-service-base.hpp" + +#include + +constexpr const char *DATA_NAME_OAUTH = "oauth"; +constexpr const char *DATA_NAME_SETTINGS = "settings"; + +constexpr const char *DATA_NAME_ACCESS_TOKEN = "access_token"; +constexpr const char *DATA_NAME_EXPIRE_TIME = "expire_time"; +constexpr const char *DATA_NAME_REFRESH_TOKEN = "refresh_token"; +constexpr const char *DATA_NAME_SCOPE_VERSION = "scope_version"; + +namespace OAuth { + +void ServiceBase::Setup(obs_data_t *data, bool deferUiFunction) +{ + if (!data) + return; + + SetSettings(obs_data_get_obj(data, DATA_NAME_SETTINGS)); + + OBSDataAutoRelease oauthData = obs_data_get_obj(data, DATA_NAME_OAUTH); + if (!oauthData) + return; + + /* Check if values in config exist if not consider, 'not connected' */ + if (!(obs_data_has_user_value(oauthData, DATA_NAME_ACCESS_TOKEN) && + obs_data_has_user_value(oauthData, DATA_NAME_SCOPE_VERSION))) { + return; + } + + if (AccessTokenHasExpireTime() && + !obs_data_has_user_value(oauthData, DATA_NAME_EXPIRE_TIME)) { + return; + } + + if (AccessTokenHasRefreshToken() && + !obs_data_has_user_value(oauthData, DATA_NAME_REFRESH_TOKEN)) { + return; + } + + /* Check scope version and re-login if mismatch */ + int64_t dataScopeVersion = + obs_data_get_int(oauthData, DATA_NAME_SCOPE_VERSION); + if (dataScopeVersion != ScopeVersion()) { + blog(LOG_WARNING, + "[%s][%s]: Old scope version detected, the user will be asked to re-login", + PluginLogName(), __FUNCTION__); + ReLogin(deferUiFunction); + return; + } + + accessToken = obs_data_get_string(oauthData, DATA_NAME_ACCESS_TOKEN); + + if (AccessTokenHasExpireTime()) + expiresTime = + obs_data_get_int(oauthData, DATA_NAME_EXPIRE_TIME); + + if (AccessTokenHasRefreshToken()) + refreshToken = + obs_data_get_string(oauthData, DATA_NAME_REFRESH_TOKEN); + + /* Check if the access token has expired */ + if (AccessTokenHasExpireTime() && IsAccessTokenExpired()) { + RequestError error; + if (!(AccessTokenHasRefreshToken() && + RefreshAccessToken(error))) { + blog(LOG_WARNING, + "[%s][%s]: Access Token has expired, the user will be asked to re-login", + PluginLogName(), __FUNCTION__); + ReLogin(deferUiFunction); + return; + } + } + + connected = true; + + if (deferUiFunction) { + defferedLoadFrontend = true; + } else { + LoadFrontend(); + } +} + +void ServiceBase::ReLogin(bool deferUiFunction) +{ + if (deferUiFunction) { + defferedLogin = true; + } else { + Login(); + } +} + +void ServiceBase::ApplyNewTokens(const OAuth::AccessTokenResponse &response) +{ + accessToken = response.accessToken; + if (AccessTokenHasExpireTime()) + expiresTime = time(nullptr) + response.expiresIn.value(); + if (AccessTokenHasRefreshToken() && response.refreshToken.has_value()) + refreshToken = response.refreshToken.value(); +} + +obs_data_t *ServiceBase::GetOAuthData() +{ + if (!connected || accessToken.empty()) + return nullptr; + + OBSData data = obs_data_create(); + obs_data_set_string(data, DATA_NAME_ACCESS_TOKEN, accessToken.c_str()); + obs_data_set_int(data, DATA_NAME_SCOPE_VERSION, ScopeVersion()); + + if (AccessTokenHasExpireTime() && expiresTime > 0) + obs_data_set_int(data, DATA_NAME_EXPIRE_TIME, expiresTime); + + if (AccessTokenHasRefreshToken() && !refreshToken.empty()) + obs_data_set_string(data, DATA_NAME_REFRESH_TOKEN, + refreshToken.c_str()); + + return data; +} + +void ServiceBase::LoadFrontend() +{ + if (frontendLoaded) + return; + + LoadFrontendInternal(); + + for (size_t i = 0; i < bondedServices.size(); i++) + AddBondedServiceFrontend(bondedServices[i]); + + frontendLoaded = true; +} + +void ServiceBase::UnloadFrontend() +{ + if (!frontendLoaded) + return; + + UnloadFrontendInternal(); + + for (size_t i = 0; i < bondedServices.size(); i++) + RemoveBondedServiceFrontend(bondedServices[i]); + + frontendLoaded = false; +} + +void ServiceBase::DuplicationReset() +{ + if (!duplicationMarker) + return; + + connected = false; + + accessToken.clear(); + expiresTime = 0; + refreshToken.clear(); + + DuplicationResetInternal(); + + duplicationMarker = false; +} + +bool ServiceBase::IsAccessTokenExpired() +{ + if (accessToken.empty()) + return true; + + return time(nullptr) > expiresTime - 5; +} + +bool ServiceBase::RefreshAccessToken(RequestError &error) +{ + OAuth::AccessTokenResponse response; + if (!OAuth::RefreshAccessToken(response, error, UserAgent(), TokenUrl(), + refreshToken, ClientId(), + ClientSecret())) { + blog(LOG_WARNING, "[%s][%s]: %s: %s", PluginLogName(), + __FUNCTION__, error.message.c_str(), error.error.c_str()); + return false; + } + + if (AccessTokenHasExpireTime() && !response.expiresIn.has_value()) { + error = RequestError( + RequestErrorType::MISSING_REQUIRED_OPT_PARAMETER, + "Missing \"expiresIn\" in the response"); + blog(LOG_WARNING, "[%s][%s]: %s: %s", PluginLogName(), + __FUNCTION__, error.message.c_str(), error.error.c_str()); + return false; + } + + blog(LOG_DEBUG, "[%s][%s]: Access token successfully refreshed", + PluginLogName(), __FUNCTION__); + + ApplyNewTokens(response); + + return true; +} + +obs_data_t *ServiceBase::GetData() +{ + OBSDataAutoRelease oauthData = GetOAuthData(); + OBSDataAutoRelease settingsData = GetSettingsData(); + + if (oauthData == nullptr && settingsData == nullptr) + return nullptr; + + OBSData data = obs_data_create(); + if (oauthData != nullptr) + obs_data_set_obj(data, DATA_NAME_OAUTH, oauthData); + + if (settingsData != nullptr) + obs_data_set_obj(data, DATA_NAME_SETTINGS, settingsData); + + return data; +} + +void ServiceBase::OBSEvent(obs_frontend_event event) +{ + switch (event) { + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + /* If duplication, reset token and ask to login */ + if (duplicationMarker) { + DuplicationReset(); + Login(); + } + [[fallthrough]]; + case OBS_FRONTEND_EVENT_FINISHED_LOADING: + if (defferedLogin) { + Login(); + defferedLogin = false; + } + + if (defferedLoadFrontend) { + LoadFrontend(); + defferedLoadFrontend = false; + } + break; + case OBS_FRONTEND_EVENT_PROFILE_CHANGING: + /* Mark OAuth to detect profile duplication */ + duplicationMarker = true; + [[fallthrough]]; + case OBS_FRONTEND_EVENT_EXIT: + UnloadFrontend(); + break; + default: + break; + } +} + +void ServiceBase::AddBondedService(obs_service_t *service) +{ + for (size_t i = 0; i < bondedServices.size(); i++) { + if (bondedServices[i] == service) + return; + } + + bondedServices.push_back(service); + if (frontendLoaded) + AddBondedServiceFrontend(service); +} + +void ServiceBase::RemoveBondedService(obs_service_t *service) +{ + for (size_t i = 0; i < bondedServices.size(); i++) { + if (bondedServices[i] == service) { + if (frontendLoaded) + RemoveBondedServiceFrontend(service); + bondedServices.erase(bondedServices.begin() + i); + } + } +} + +bool ServiceBase::Login() +try { + if (connected) + return true; + + std::string code; + std::string redirectUri; + if (!LoginInternal(code, redirectUri)) + return false; + + OAuth::AccessTokenResponse response; + RequestError error; + if (!OAuth::AccessTokenRequest(response, error, UserAgent(), TokenUrl(), + code, redirectUri, ClientId(), + ClientSecret())) { + throw error; + } + + if (AccessTokenHasExpireTime() && !response.expiresIn.has_value()) { + throw RequestError( + RequestErrorType::MISSING_REQUIRED_OPT_PARAMETER, + "Missing \"expires_in\" in the response"); + } else if (AccessTokenHasRefreshToken() && + !response.refreshToken.has_value()) { + throw RequestError( + RequestErrorType::MISSING_REQUIRED_OPT_PARAMETER, + "Missing \"refresh_token\" in the response"); + } + + if (response.scope.has_value() && response.scope.value() != Scope()) { + std::string scopeError = "Requested "; + scopeError += Scope(); + scopeError += ", returned "; + scopeError += response.scope.value(); + throw RequestError("Invalid returned scope", scopeError); + } + + ApplyNewTokens(response); + + connected = true; + + LoadFrontend(); + + return true; +} catch (RequestError &e) { + blog(LOG_WARNING, "[%s][%s]: %s: %s", PluginLogName(), __FUNCTION__, + e.message.c_str(), e.error.c_str()); + LoginError(e); + return false; +} + +bool ServiceBase::SignOut() +{ + if (!connected) + return true; + + if (!SignOutInternal()) + return false; + + UnloadFrontend(); + + connected = false; + + return true; +} + +} diff --git a/deps/oauth-service/service-base/oauth-service-base.hpp b/deps/oauth-service/service-base/oauth-service-base.hpp new file mode 100644 index 00000000000000..53a2a354ec528a --- /dev/null +++ b/deps/oauth-service/service-base/oauth-service-base.hpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +#include + +namespace OAuth { + +class ServiceBase { + /* NOTE: To support service copy (e.g. settings) */ + std::vector bondedServices; + + bool duplicationMarker = false; + + bool connected = false; + bool frontendLoaded = false; + + std::string accessToken; + int64_t expiresTime = 0; + std::string refreshToken; + + bool defferedLogin = false; + bool defferedLoadFrontend = false; + + inline void ReLogin(bool deferUiFunction); + + inline void ApplyNewTokens(const OAuth::AccessTokenResponse &response); + obs_data_t *GetOAuthData(); + + void LoadFrontend(); + void UnloadFrontend(); + + void DuplicationReset(); + +protected: + inline std::string AccessToken() { return accessToken; } + bool IsAccessTokenExpired(); + bool RefreshAccessToken(RequestError &error); + + virtual inline bool AccessTokenHasExpireTime() { return true; } + virtual inline bool AccessTokenHasRefreshToken() { return true; } + + virtual std::string UserAgent() = 0; + + virtual const char *TokenUrl() = 0; + + virtual std::string ClientId() = 0; + virtual inline std::string ClientSecret() { return {}; } + + virtual std::string Scope() = 0; + virtual int64_t ScopeVersion() = 0; + + virtual bool LoginInternal(std::string &code, + std::string &redirectUri) = 0; + virtual bool SignOutInternal() = 0; + + virtual inline void SetSettings(obs_data_t * /* settingsData */){}; + virtual inline obs_data_t *GetSettingsData() { return nullptr; } + + virtual inline void LoadFrontendInternal() {} + virtual inline void UnloadFrontendInternal() {} + + virtual void DuplicationResetInternal() {} + + virtual inline void + AddBondedServiceFrontend(obs_service_t * /* service */){}; + virtual inline void + RemoveBondedServiceFrontend(obs_service_t * /* service */){}; + + virtual const char *PluginLogName() = 0; + + virtual inline void LoginError(RequestError & /* error */) {} + +public: + ServiceBase() {} + virtual ~ServiceBase(){}; + + void Setup(obs_data_t *data, bool deferUiFunction = false); + obs_data_t *GetData(); + + void OBSEvent(obs_frontend_event event); + + void AddBondedService(obs_service_t *service); + void RemoveBondedService(obs_service_t *service); + + bool Connected() { return connected; } + + bool Login(); + bool SignOut(); +}; + +} From d92a9d4a22baffc8a83d19973f68cf14c620918c Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 13:22:59 +0200 Subject: [PATCH 30/65] oauth-service: Remove scope from access token response --- deps/oauth-service/oauth/oauth.hpp | 6 +++--- deps/oauth-service/service-base/oauth-service-base.cpp | 8 -------- deps/oauth-service/service-base/oauth-service-base.hpp | 1 - 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/deps/oauth-service/oauth/oauth.hpp b/deps/oauth-service/oauth/oauth.hpp index c1386762dc75d6..f4c8b29abc2b4f 100644 --- a/deps/oauth-service/oauth/oauth.hpp +++ b/deps/oauth-service/oauth/oauth.hpp @@ -73,13 +73,14 @@ inline std::optional get_stack_optional(const json &j, const char *property) #endif /* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.4 - * https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 */ + * https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 + * NOTE: Scope is removed because some services (e.g. Twitch) + * does not provide a string but an array. */ struct AccessTokenResponse { std::string accessToken; std::string tokenType; std::optional expiresIn; std::optional refreshToken; - std::optional scope; }; /* https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 */ @@ -110,7 +111,6 @@ inline void from_json(const json &j, AccessTokenResponse &s) [](unsigned char c) { return std::tolower(c); }); s.expiresIn = get_stack_optional(j, "expires_in"); s.refreshToken = get_stack_optional(j, "refresh_token"); - s.scope = get_stack_optional(j, "scope"); } inline void from_json(const json &j, AccessTokenError &e) diff --git a/deps/oauth-service/service-base/oauth-service-base.cpp b/deps/oauth-service/service-base/oauth-service-base.cpp index 11a460916c95d6..5cdcf89613778a 100644 --- a/deps/oauth-service/service-base/oauth-service-base.cpp +++ b/deps/oauth-service/service-base/oauth-service-base.cpp @@ -304,14 +304,6 @@ try { "Missing \"refresh_token\" in the response"); } - if (response.scope.has_value() && response.scope.value() != Scope()) { - std::string scopeError = "Requested "; - scopeError += Scope(); - scopeError += ", returned "; - scopeError += response.scope.value(); - throw RequestError("Invalid returned scope", scopeError); - } - ApplyNewTokens(response); connected = true; diff --git a/deps/oauth-service/service-base/oauth-service-base.hpp b/deps/oauth-service/service-base/oauth-service-base.hpp index 53a2a354ec528a..0f9829af61e2a2 100644 --- a/deps/oauth-service/service-base/oauth-service-base.hpp +++ b/deps/oauth-service/service-base/oauth-service-base.hpp @@ -53,7 +53,6 @@ class ServiceBase { virtual std::string ClientId() = 0; virtual inline std::string ClientSecret() { return {}; } - virtual std::string Scope() = 0; virtual int64_t ScopeVersion() = 0; virtual bool LoginInternal(std::string &code, From 9f79b739c83a595de6ab2f74253f2e7bb880451e Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 15 Jun 2023 15:51:53 +0200 Subject: [PATCH 31/65] oauth-service: Add OAuth local redirect --- .../local-redirect/CMakeLists.txt | 15 ++ .../local-redirect/oauth-local-redirect.cpp | 175 ++++++++++++++++++ .../local-redirect/oauth-local-redirect.hpp | 77 ++++++++ 3 files changed, 267 insertions(+) create mode 100644 deps/oauth-service/local-redirect/CMakeLists.txt create mode 100644 deps/oauth-service/local-redirect/oauth-local-redirect.cpp create mode 100644 deps/oauth-service/local-redirect/oauth-local-redirect.hpp diff --git a/deps/oauth-service/local-redirect/CMakeLists.txt b/deps/oauth-service/local-redirect/CMakeLists.txt new file mode 100644 index 00000000000000..54c5a66f4cf26a --- /dev/null +++ b/deps/oauth-service/local-redirect/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +find_qt(COMPONENTS Core Widgets Network) + +if(NOT TARGET OBS::oauth) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/oauth" "${CMAKE_BINARY_DIR}/deps/oauth-service/oauth") +endif() + +add_library(oauth-local-redirect INTERFACE) +add_library(OBS::oauth-local-redirect ALIAS oauth-local-redirect) + +target_sources(oauth-local-redirect INTERFACE oauth-local-redirect.cpp oauth-local-redirect.hpp) +target_include_directories(oauth-local-redirect INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(oauth-local-redirect INTERFACE OBS::libobs OBS::oauth Qt::Core Qt::Widgets Qt::Network) diff --git a/deps/oauth-service/local-redirect/oauth-local-redirect.cpp b/deps/oauth-service/local-redirect/oauth-local-redirect.cpp new file mode 100644 index 00000000000000..a950d6bb164845 --- /dev/null +++ b/deps/oauth-service/local-redirect/oauth-local-redirect.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "oauth-local-redirect.hpp" + +#include +#include +#include +#include +#include + +constexpr const char *REDIRECT_URI_TEMPLATE = "http://127.0.0.1:%1"; + +constexpr const char *QUERY_TEMPLATE = + "?response_type=code&client_id=%1&redirect_uri=%2&scope=%3"; + +constexpr const char *RESPONSE_HEADER = + "HTTP/1.0 200 OK\n" + "Connection: close\n" + "Content-Type: text/html; charset=UTF-8\n" + "Server: %1\n" + "\n" + "%1" + ""; + +constexpr const char *RESPONSE_LOGO_TEMPLATE = + "
" + "\"%2\"" + "
"; +constexpr const char *RESPONSE_TEMPLATE = + "

%1

"; + +OAuth::LocalRedirect::LocalRedirect(const QString &baseAuthUrl_, + const QString &clientId_, + const QString &scope_, bool useState_, + QWidget *parent) + : QMessageBox(QMessageBox::NoIcon, "placeholder", "placeholder", + QMessageBox::NoButton, parent), + baseAuthUrl(baseAuthUrl_), + clientId(clientId_), + scope(scope_), + useState(useState_) +{ + setWindowFlag(Qt::WindowCloseButtonHint, false); + setTextFormat(Qt::RichText); + + reOpenUrl = addButton("placeholder", QMessageBox::HelpRole); + QPushButton *cancel = addButton(QMessageBox::Cancel); + cancel->setText(tr("Cancel")); + + /* Prevent the re-open URL button to close the dialog */ + reOpenUrl->disconnect(SIGNAL(clicked(bool))); + + connect(reOpenUrl, &QPushButton::clicked, this, + &OAuth::LocalRedirect::OpenUrl); + connect(&server, &QTcpServer::newConnection, this, + &OAuth::LocalRedirect::NewConnection); +} + +void OAuth::LocalRedirect::NewConnection() +{ + if (!server.hasPendingConnections()) + return; + + QTcpSocket *socket = server.nextPendingConnection(); + connect(socket, &QTcpSocket::disconnected, socket, + &QTcpSocket::deleteLater); + socket->waitForReadyRead(); + + QByteArray buffer; + while (socket->bytesAvailable() > 0) + buffer.append(socket->readAll()); + + socket->write(QString(RESPONSE_HEADER) + .arg(ServerPageTitle()) + .toUtf8() + .constData()); + + QString bufferStr = QString::fromLatin1(buffer); + + QRegularExpressionMatch regexState = + QRegularExpression("(&|\\?)state=(?[^&]+)") + .match(bufferStr); + QRegularExpressionMatch regexCode = + QRegularExpression("(&|\\?)code=(?[^&]+)") + .match(bufferStr); + + if (useState) { + if (!regexState.hasMatch()) { + lastError = "No 'state' in server redirect"; + } else if (state != regexState.captured("state")) { + lastError = "State and returned state mismatch"; + } + } + + if (!regexCode.hasMatch()) { + lastError = "No 'code' in server redirect"; + } else { + code = regexCode.captured("code"); + } + + QString responseTemplate; + if (!ServerResponsePageLogoUrl().isEmpty()) + responseTemplate = QString(RESPONSE_LOGO_TEMPLATE) + .arg(ServerResponsePageLogoUrl()) + .arg(ServerResponsePageLogoAlt()); + + responseTemplate += RESPONSE_TEMPLATE; + + if (code.isEmpty()) + lastError = "'code' was found empty"; + + socket->write(responseTemplate + .arg(lastError.isEmpty() + ? ServerResponseSuccessText() + : ServerResponseFailureText()) + .toUtf8() + .constData()); + + socket->flush(); + socket->close(); + + if (!lastError.isEmpty()) + reject(); + else + accept(); +} + +int OAuth::LocalRedirect::exec() +{ + redirectUri.clear(); + code.clear(); + + lastError.clear(); + + setWindowTitle(DialogTitle()); + setText(DialogText()); + reOpenUrl->setText(ReOpenUrlButtonText()); + + if (!server.listen(QHostAddress::LocalHost, 0)) { + lastError = "Server could not start"; + return QDialog::Rejected; + } + + DebugLog(QString("Server started at port %1").arg(server.serverPort())); + + redirectUri = QString(REDIRECT_URI_TEMPLATE).arg(server.serverPort()); + + QString urlStr = baseAuthUrl; + urlStr += QUERY_TEMPLATE; + urlStr = urlStr.arg(clientId).arg(redirectUri).arg(scope); + + if (useState) { + state = QUuid::createUuid().toString(QUuid::Id128); + + urlStr += "&state="; + urlStr += state; + } + + url = QUrl(urlStr, QUrl::StrictMode); + AutoOpenUrlThread autoOpenUrl(url); + + autoOpenUrl.start(); + int ret = QMessageBox::exec(); + + if (ret == QMessageBox::Cancel) + ret = QDialog::Rejected; + + autoOpenUrl.wait(); + server.close(); + url.clear(); + state.clear(); + return ret; +} diff --git a/deps/oauth-service/local-redirect/oauth-local-redirect.hpp b/deps/oauth-service/local-redirect/oauth-local-redirect.hpp new file mode 100644 index 00000000000000..e35011a26d2bc3 --- /dev/null +++ b/deps/oauth-service/local-redirect/oauth-local-redirect.hpp @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace OAuth { + +class AutoOpenUrlThread : public QThread { + Q_OBJECT + + QUrl url; + virtual void run() override { QDesktopServices::openUrl(url); } + +public: + explicit inline AutoOpenUrlThread(const QUrl &url_) : url(url_){}; +}; + +class LocalRedirect : public QMessageBox { + Q_OBJECT + + QString baseAuthUrl; + QString clientId; + QString scope; + bool useState; + + QPushButton *reOpenUrl; + QTcpServer server; + QString state; + + QUrl url; + + QString redirectUri; + QString code; + + QString lastError; + +protected: + virtual QString DialogTitle() = 0; + virtual QString DialogText() = 0; + virtual QString ReOpenUrlButtonText() = 0; + + virtual QString ServerPageTitle() = 0; + virtual inline QString ServerResponsePageLogoUrl() { return {}; } + virtual inline QString ServerResponsePageLogoAlt() { return {}; } + virtual QString ServerResponseSuccessText() = 0; + virtual QString ServerResponseFailureText() = 0; + + virtual inline void DebugLog(const QString & /* info */){}; + +public: + LocalRedirect(const QString &baseAuthUrl, const QString &clientId, + const QString &scope, bool useState, QWidget *parent); + ~LocalRedirect() {} + + inline QString GetRedirectUri() { return redirectUri; } + inline QString GetCode() { return code; } + + inline QString GetLastError() { return lastError; } + +private slots: + inline void OpenUrl() { QDesktopServices::openUrl(url); } + void NewConnection(); + +public slots: + /* Return QDialog::Accepted if redirect succeeded */ + int exec() override; +}; +} From 354bd03535eee6bebf3a1337f61533041218e890 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 22 Oct 2022 15:39:31 +0200 Subject: [PATCH 32/65] UI,docs: Add frontend API browser functions --- UI/api-interface.cpp | 57 +++++++++++++++++++ UI/obs-frontend-api/obs-frontend-api.cpp | 41 +++++++++++++ UI/obs-frontend-api/obs-frontend-api.h | 19 +++++++ UI/obs-frontend-api/obs-frontend-internal.hpp | 8 +++ UI/window-basic-main-browser.cpp | 53 +++++++++++++++++ UI/window-basic-main.hpp | 3 + docs/sphinx/reference-frontend-api.rst | 46 +++++++++++++++ 7 files changed, 227 insertions(+) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index 8fb3bea3a3816d..a2f9149a808695 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -6,6 +6,10 @@ #include +#ifdef ENABLE_WAYLAND +#include +#endif + using namespace std; Q_DECLARE_METATYPE(OBSScene); @@ -445,6 +449,59 @@ struct OBSStudioAPI : obs_frontend_callbacks { return true; } + bool obs_frontend_is_browser_available(void) override + { +#ifdef BROWSER_AVAILABLE +#ifdef ENABLE_WAYLAND + return (obs_get_nix_platform() != OBS_NIX_PLATFORM_WAYLAND); +#else + return true; +#endif +#else + return false; +#endif + } + + void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, + size_t size) override + { +#ifdef BROWSER_AVAILABLE +#ifdef ENABLE_WAYLAND + if (!obs_frontend_is_browser_available()) + return nullptr; +#endif + struct obs_frontend_browser_params data = {0}; + if (size > sizeof(data)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_get_browser_widget with size " + "%llu which is more than OBS Studio currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(data)); + return nullptr; + } + + memcpy(&data, params, size); + + return (void *)main->GetBrowserWidget(data); +#else + UNUSED_PARAMETER(params); + UNUSED_PARAMETER(size); + return nullptr; +#endif + } + + void obs_frontend_delete_browser_cookie(const char *url) override + { + if (!url) + return; + + std::string urlStr = url; + if (!urlStr.empty()) + main->DeleteCookie(urlStr); + } + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override { diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp index df753551b10e05..6a22de85b54fd3 100644 --- a/UI/obs-frontend-api/obs-frontend-api.cpp +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -350,6 +350,47 @@ bool obs_frontend_add_custom_qdock(const char *id, void *dock) : false; } +bool obs_frontend_is_browser_available(void) +{ + return !!callbacks_valid() ? c->obs_frontend_is_browser_available() + : false; +} + +void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, size_t size) +{ + if (!callbacks_valid()) + return nullptr; + + if ((offsetof(struct obs_frontend_browser_params, url) + + sizeof(params->url) > + size) || + !params->url) { + blog(LOG_ERROR, "Required value 'url' not found." + " obs_frontend_get_browser_widget failed."); + return nullptr; + } + + struct obs_frontend_browser_params zeroed = {0}; + if (size > sizeof(zeroed)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_get_browser_widget with size " + "%llu which is more than obs-frontend-api currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(zeroed)); + return nullptr; + } + + return c->obs_frontend_get_browser_widget_s(params, size); +} + +void obs_frontend_delete_browser_cookie(const char *url) +{ + if (callbacks_valid()) + c->obs_frontend_delete_browser_cookie(url); +} + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) { diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h index a0913e683359f1..0ca07ff5d076e8 100644 --- a/UI/obs-frontend-api/obs-frontend-api.h +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -81,6 +82,13 @@ obs_frontend_source_list_free(struct obs_frontend_source_list *source_list) da_free(source_list->sources); } +struct obs_frontend_browser_params { + const char *url; + const char *startup_script; + const char **force_popup_urls; + const char **popup_whitelist_urls; + bool enable_cookie; +}; #endif //!SWIG /* ------------------------------------------------------------------------- */ @@ -150,6 +158,17 @@ EXPORT void obs_frontend_remove_dock(const char *id); /* takes QDockWidget for dock */ EXPORT bool obs_frontend_add_custom_qdock(const char *id, void *dock); +EXPORT bool obs_frontend_is_browser_available(void); + +/* returns QAction */ +EXPORT void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, size_t size); +#define obs_frontend_get_browser_widget(params) \ + obs_frontend_get_browser_widget_s( \ + params, sizeof(struct obs_frontend_browser_params)) + +EXPORT void obs_frontend_delete_browser_cookie(const char *url); + typedef void (*obs_frontend_event_cb)(enum obs_frontend_event event, void *private_data); diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp index 90a0e06ea9dd5b..dfb82dac0ea4d0 100644 --- a/UI/obs-frontend-api/obs-frontend-internal.hpp +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -73,6 +73,14 @@ struct obs_frontend_callbacks { virtual bool obs_frontend_add_custom_qdock(const char *id, void *dock) = 0; + virtual bool obs_frontend_is_browser_available(void) = 0; + + virtual void *obs_frontend_get_browser_widget_s( + const struct obs_frontend_browser_params *params, + size_t size) = 0; + + virtual void obs_frontend_delete_browser_cookie(const char *url) = 0; + virtual void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) = 0; diff --git a/UI/window-basic-main-browser.cpp b/UI/window-basic-main-browser.cpp index ec99f141e4987b..9673064528e91d 100644 --- a/UI/window-basic-main-browser.cpp +++ b/UI/window-basic-main-browser.cpp @@ -162,3 +162,56 @@ void OBSBasic::InitBrowserPanelSafeBlock() InitPanelCookieManager(); #endif } + +QWidget *OBSBasic::GetBrowserWidget(const obs_frontend_browser_params ¶ms) +{ +#ifdef BROWSER_AVAILABLE + if (!cef) + return nullptr; + + if (params.enable_cookie) + OBSBasic::InitBrowserPanelSafeBlock(); + + static int panel_version = -1; + if (panel_version == -1) { + panel_version = obs_browser_qcef_version(); + } + + QCefWidget *browser = cef->create_widget( + nullptr, params.url, + params.enable_cookie ? panel_cookies : nullptr); + if (browser && panel_version >= 1) + browser->allowAllPopups(true); + + if (params.startup_script) + browser->setStartupScript(params.startup_script); + + if (params.force_popup_urls) { + const char **urls = params.force_popup_urls; + while (*urls) { + cef->add_force_popup_url(*urls, browser); + urls++; + } + } + + if (params.popup_whitelist_urls) { + const char **urls = params.popup_whitelist_urls; + while (*urls) { + cef->add_popup_whitelist_url(*urls, browser); + urls++; + } + } + + return browser; +#else + return nullptr; +#endif +} + +void OBSBasic::DeleteCookie(const std::string &url) +{ +#ifdef BROWSER_AVAILABLE + if (panel_cookies) + panel_cookies->DeleteCookies(url, std::string()); +#endif +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index c2718ee8f98cff..d86f8aa3a4908a 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -980,6 +980,9 @@ private slots: bool IsDockObjectNameUsed(const QString &name); void AddCustomDockWidget(QDockWidget *dock); + QWidget *GetBrowserWidget(const obs_frontend_browser_params ¶ms); + void DeleteCookie(const std::string &url); + static OBSBasic *Get(); const char *GetCurrentOutputPath(); diff --git a/docs/sphinx/reference-frontend-api.rst b/docs/sphinx/reference-frontend-api.rst index d98340414b4ed1..deb4f51039ddca 100644 --- a/docs/sphinx/reference-frontend-api.rst +++ b/docs/sphinx/reference-frontend-api.rst @@ -233,6 +233,31 @@ Structures/Enumerations Undo redo callback +.. struct:: obs_frontend_browser_params + + Parameters for the browser widget + + .. member:: const char *obs_frontend_browser_params.url + + Url openned in the widget. + + .. member:: const char *obs_frontend_browser_params.startup_script + + Javascript run at startup of the widget. + + .. member:: const char **obs_frontend_browser_params.force_popup_urls + + NULL-terminated array of URLs to force popup. + + .. member:: const char **obs_frontend_browser_params.popup_whitelist_urls + + NULL-terminated array of URLs to popup whitelist. + + .. member:: bool obs_frontend_browser_params.enable_cookie + + Enable cookie. Note that wdiget created with this enabled must be destroyed + while the profile is changing since that the cookie manager is tied the profile. + Functions --------- @@ -494,6 +519,27 @@ Functions --------------------------------------- +.. function:: bool obs_frontend_is_browser_available(void) + + :return: If browser feature is available (built with obs-browser or + not runnning under Wayland) + +--------------------------------------- + +.. function:: void *obs_frontend_get_browser_widget(const obs_frontend_browser_params *params) + + Creates and returns a pointer of a browser widget with the given parameters. + + :return: Pointer to the created QWidget or NULL + +--------------------------------------- + +.. function:: void obs_frontend_delete_browser_cookie(const char *url) + + Delete cookies related to the URL. + +--------------------------------------- + .. function:: void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) Adds a callback that will be called when a frontend event occurs. From 5cfbee0029b370d63d7a3e1b65c20cac42ebb854 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sun, 18 Jun 2023 10:32:01 +0200 Subject: [PATCH 33/65] UI: Remove integrated YouTube integration - It will be replaced by a plugin based integration - Migration will be done in another commit - UI is moved to be refactored for the new plugin - Remains of the integration will be turned into a new API to allow service plugin to have a broadcast flow --- UI/CMakeLists.txt | 1 - UI/auth-youtube.cpp | 409 ------------ UI/auth-youtube.hpp | 65 -- UI/cmake/feature-youtube.cmake | 13 - UI/cmake/legacy.cmake | 22 - UI/cmake/ui-qt.cmake | 1 - UI/data/locale/en-US.ini | 98 --- UI/window-basic-main.cpp | 11 - UI/youtube-api-wrappers.cpp | 605 ------------------ UI/youtube-api-wrappers.hpp | 96 --- .../obs-youtube}/forms/OBSYoutubeActions.ui | 0 .../obs-youtube}/window-youtube-actions.cpp | 0 .../obs-youtube}/window-youtube-actions.hpp | 0 13 files changed, 1321 deletions(-) delete mode 100644 UI/auth-youtube.cpp delete mode 100644 UI/auth-youtube.hpp delete mode 100644 UI/cmake/feature-youtube.cmake delete mode 100644 UI/youtube-api-wrappers.cpp delete mode 100644 UI/youtube-api-wrappers.hpp rename {UI => plugins/obs-youtube}/forms/OBSYoutubeActions.ui (100%) rename {UI => plugins/obs-youtube}/window-youtube-actions.cpp (100%) rename {UI => plugins/obs-youtube}/window-youtube-actions.hpp (100%) diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 973f698bb6c3f0..13f39f84213b32 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -57,7 +57,6 @@ if(NOT OAUTH_BASE_URL) endif() include(cmake/feature-twitch.cmake) include(cmake/feature-restream.cmake) -include(cmake/feature-youtube.cmake) include(cmake/feature-sparkle.cmake) include(cmake/feature-whatsnew.cmake) diff --git a/UI/auth-youtube.cpp b/UI/auth-youtube.cpp deleted file mode 100644 index 4dc7b8f1cee0ba..00000000000000 --- a/UI/auth-youtube.cpp +++ /dev/null @@ -1,409 +0,0 @@ -#include "auth-youtube.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include - -#pragma comment(lib, "shell32") -#endif - -#include "auth-listener.hpp" -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "ui-config.h" -#include "youtube-api-wrappers.hpp" -#include "window-basic-main.hpp" -#include - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#endif - -using namespace json11; - -/* ------------------------------------------------------------------------- */ -#define YOUTUBE_AUTH_URL "https://accounts.google.com/o/oauth2/v2/auth" -#define YOUTUBE_TOKEN_URL "https://www.googleapis.com/oauth2/v4/token" -#define YOUTUBE_SCOPE_VERSION 1 -#define YOUTUBE_API_STATE_LENGTH 32 -#define SECTION_NAME "YouTube" - -#define YOUTUBE_CHAT_PLACEHOLDER_URL \ - "https://obsproject.com/placeholders/youtube-chat" -#define YOUTUBE_CHAT_POPOUT_URL \ - "https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v=%1" - -#define YOUTUBE_CHAT_DOCK_NAME "ytChat" - -static const char allowedChars[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -static const int allowedCount = static_cast(sizeof(allowedChars) - 1); -/* ------------------------------------------------------------------------- */ - -static inline void OpenBrowser(const QString auth_uri) -{ - QUrl url(auth_uri, QUrl::StrictMode); - QDesktopServices::openUrl(url); -} - -void RegisterYoutubeAuth() -{ - for (auto &service : youtubeServices) { - OAuth::RegisterOAuth( - service, - [service]() { - return std::make_shared( - service); - }, - YoutubeAuth::Login, []() { return; }); - } -} - -YoutubeAuth::YoutubeAuth(const Def &d) - : OAuthStreamKey(d), - section(SECTION_NAME) -{ -} - -YoutubeAuth::~YoutubeAuth() -{ - if (!uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(YOUTUBE_CHAT_DOCK_NAME); - chat = nullptr; -#endif -} - -bool YoutubeAuth::RetryLogin() -{ - return true; -} - -void YoutubeAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - - const char *section_name = section.c_str(); - config_set_string(main->Config(), section_name, "RefreshToken", - refresh_token.c_str()); - config_set_string(main->Config(), section_name, "Token", token.c_str()); - config_set_uint(main->Config(), section_name, "ExpireTime", - expire_time); - config_set_int(main->Config(), section_name, "ScopeVer", - currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool YoutubeAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - - const char *section_name = section.c_str(); - refresh_token = get_config_str(main, section_name, "RefreshToken"); - token = get_config_str(main, section_name, "Token"); - expire_time = - config_get_uint(main->Config(), section_name, "ExpireTime"); - currentScopeVer = - (int)config_get_int(main->Config(), section_name, "ScopeVer"); - firstLoad = false; - return implicit ? !token.empty() : !refresh_token.empty(); -} - -#ifdef BROWSER_AVAILABLE -static const char *ytchat_script = "\ -const obsCSS = document.createElement('style');\ -obsCSS.innerHTML = \"#panel-pages.yt-live-chat-renderer {display: none;}\ -yt-live-chat-viewer-engagement-message-renderer {display: none;}\";\ -document.querySelector('head').appendChild(obsCSS);"; -#endif - -void YoutubeAuth::LoadUI() -{ - if (uiLoaded) - return; - -#ifdef BROWSER_AVAILABLE - if (!cef) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - chat = new YoutubeChatDock(); - chat->setObjectName(YOUTUBE_CHAT_DOCK_NAME); - chat->resize(300, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, YOUTUBE_CHAT_PLACEHOLDER_URL, - panel_cookies); - browser->setStartupScript(ytchat_script); - - chat->SetWidget(browser); - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - chat->setFloating(true); - chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); - - if (firstLoad) { - chat->setVisible(true); - } else if (!config_has_user_value(main->Config(), "BasicWindow", - "DockState")) { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - - config_set_string(main->Config(), "BasicWindow", "DockState", - dockStateStr); - } -#endif - - uiLoaded = true; -} - -void YoutubeAuth::SetChatId(const QString &chat_id, - const std::string &api_chat_id) -{ -#ifdef BROWSER_AVAILABLE - QString chat_url = QString(YOUTUBE_CHAT_POPOUT_URL).arg(chat_id); - - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(chat_url.toStdString()); - chat->SetApiChatId(api_chat_id); - } -#else - UNUSED_PARAMETER(chat_id); - UNUSED_PARAMETER(api_chat_id); -#endif -} - -void YoutubeAuth::ResetChat() -{ -#ifdef BROWSER_AVAILABLE - if (chat && chat->cefWidget) { - chat->cefWidget->setURL(YOUTUBE_CHAT_PLACEHOLDER_URL); - } -#endif -} - -QString YoutubeAuth::GenerateState() -{ - char state[YOUTUBE_API_STATE_LENGTH + 1]; - QRandomGenerator *rng = QRandomGenerator::system(); - int i; - - for (i = 0; i < YOUTUBE_API_STATE_LENGTH; i++) - state[i] = allowedChars[rng->bounded(0, allowedCount)]; - state[i] = 0; - - return state; -} - -// Static. -std::shared_ptr YoutubeAuth::Login(QWidget *owner, - const std::string &service) -{ - QString auth_code; - AuthListener server; - - auto it = std::find_if(youtubeServices.begin(), youtubeServices.end(), - [service](auto &item) { - return service == item.service; - }); - if (it == youtubeServices.end()) { - return nullptr; - } - const auto auth = std::make_shared(*it); - - QString redirect_uri = - QString("http://127.0.0.1:%1").arg(server.GetPort()); - - QMessageBox dlg(owner); - dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); - dlg.setWindowTitle(QTStr("YouTube.Auth.WaitingAuth.Title")); - - std::string clientid = YOUTUBE_CLIENTID; - std::string secret = YOUTUBE_SECRET; - deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); - deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); - - QString state; - state = auth->GenerateState(); - server.SetState(state); - - QString url_template; - url_template += "%1"; - url_template += "?response_type=code"; - url_template += "&client_id=%2"; - url_template += "&redirect_uri=%3"; - url_template += "&state=%4"; - url_template += "&scope=https://www.googleapis.com/auth/youtube"; - QString url = url_template.arg(YOUTUBE_AUTH_URL, clientid.c_str(), - redirect_uri, state); - - QString text = QTStr("YouTube.Auth.WaitingAuth.Text"); - text = text.arg( - QString("Google OAuth Service").arg(url)); - - dlg.setText(text); - dlg.setTextFormat(Qt::RichText); - dlg.setStandardButtons(QMessageBox::StandardButton::Cancel); - - connect(&dlg, &QMessageBox::buttonClicked, &dlg, - [&](QAbstractButton *) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Action Cancelled."); -#endif - // TODO: Stop server. - dlg.reject(); - }); - - // Async Login. - connect(&server, &AuthListener::ok, &dlg, - [&dlg, &auth_code](QString code) { -#ifdef _DEBUG - blog(LOG_DEBUG, "Got youtube redirected answer: %s", - QT_TO_UTF8(code)); -#endif - auth_code = code; - dlg.accept(); - }); - connect(&server, &AuthListener::fail, &dlg, [&dlg]() { -#ifdef _DEBUG - blog(LOG_DEBUG, "No access granted"); -#endif - dlg.reject(); - }); - - auto open_external_browser = [url]() { - OpenBrowser(url); - }; - QScopedPointer thread(CreateQThread(open_external_browser)); - thread->start(); - - dlg.exec(); - if (dlg.result() == QMessageBox::Cancel || - dlg.result() == QDialog::Rejected) - return nullptr; - - if (!auth->GetToken(YOUTUBE_TOKEN_URL, clientid, secret, - QT_TO_UTF8(redirect_uri), YOUTUBE_SCOPE_VERSION, - QT_TO_UTF8(auth_code), true)) { - return nullptr; - } - - config_t *config = OBSBasic::Get()->Config(); - config_remove_value(config, "YouTube", "ChannelName"); - - ChannelDescription cd; - if (auth->GetChannelDescription(cd)) - config_set_string(config, "YouTube", "ChannelName", - QT_TO_UTF8(cd.title)); - - config_save_safe(config, "tmp", nullptr); - return auth; -} - -#ifdef BROWSER_AVAILABLE -void YoutubeChatDock::SetWidget(QCefWidget *widget_) -{ - lineEdit = new LineEditAutoResize(); - lineEdit->setVisible(false); - lineEdit->setMaxLength(200); - lineEdit->setPlaceholderText(QTStr("YouTube.Chat.Input.Placeholder")); - sendButton = new QPushButton(QTStr("YouTube.Chat.Input.Send")); - sendButton->setVisible(false); - - chatLayout = new QHBoxLayout(); - chatLayout->setContentsMargins(0, 0, 0, 0); - chatLayout->addWidget(lineEdit, 1); - chatLayout->addWidget(sendButton); - - QVBoxLayout *layout = new QVBoxLayout(); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(widget_, 1); - layout->addLayout(chatLayout); - - QWidget *widget = new QWidget(); - widget->setLayout(layout); - setWidget(widget); - - QWidget::connect(lineEdit, SIGNAL(returnPressed()), this, - SLOT(SendChatMessage())); - QWidget::connect(sendButton, SIGNAL(pressed()), this, - SLOT(SendChatMessage())); - - cefWidget.reset(widget_); -} - -void YoutubeChatDock::SetApiChatId(const std::string &id) -{ - this->apiChatId = id; - QMetaObject::invokeMethod(this, "EnableChatInput", - Qt::QueuedConnection); -} - -void YoutubeChatDock::SendChatMessage() -{ - const QString message = lineEdit->text(); - if (message == "") - return; - - OBSBasic *main = OBSBasic::Get(); - YoutubeApiWrappers *apiYouTube( - dynamic_cast(main->GetAuth())); - - ExecuteFuncSafeBlock([&]() { - lineEdit->setText(""); - lineEdit->setPlaceholderText( - QTStr("YouTube.Chat.Input.Sending")); - if (apiYouTube->SendChatMessage(apiChatId, message)) { - os_sleep_ms(3000); - } else { - QString error = apiYouTube->GetLastError(); - apiYouTube->GetTranslatedError(error); - QMetaObject::invokeMethod( - this, "ShowErrorMessage", Qt::QueuedConnection, - Q_ARG(const QString &, error)); - } - lineEdit->setPlaceholderText( - QTStr("YouTube.Chat.Input.Placeholder")); - }); -} - -void YoutubeChatDock::ShowErrorMessage(const QString &error) -{ - QMessageBox::warning(this, QTStr("YouTube.Chat.Error.Title"), - QTStr("YouTube.Chat.Error.Text").arg(error)); -} - -void YoutubeChatDock::EnableChatInput() -{ - lineEdit->setVisible(true); - sendButton->setVisible(true); -} -#endif diff --git a/UI/auth-youtube.hpp b/UI/auth-youtube.hpp deleted file mode 100644 index ffe35c25c39c09..00000000000000 --- a/UI/auth-youtube.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "auth-oauth.hpp" - -#ifdef BROWSER_AVAILABLE -#include "window-dock-browser.hpp" -#include "lineedit-autoresize.hpp" -#include -class YoutubeChatDock : public BrowserDock { - Q_OBJECT - -private: - std::string apiChatId; - LineEditAutoResize *lineEdit; - QPushButton *sendButton; - QHBoxLayout *chatLayout; - -public: - void SetWidget(QCefWidget *widget_); - void SetApiChatId(const std::string &id); - -private slots: - void SendChatMessage(); - void ShowErrorMessage(const QString &error); - void EnableChatInput(); -}; -#endif - -inline const std::vector youtubeServices = { - {"YouTube - RTMP", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - RTMPS", Auth::Type::OAuth_LinkedAccount, true, true}, - {"YouTube - HLS", Auth::Type::OAuth_LinkedAccount, true, true}}; - -class YoutubeAuth : public OAuthStreamKey { - Q_OBJECT - - bool uiLoaded = false; - std::string section; - -#ifdef BROWSER_AVAILABLE - YoutubeChatDock *chat; -#endif - - virtual bool RetryLogin() override; - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - virtual void LoadUI() override; - - QString GenerateState(); - -public: - YoutubeAuth(const Def &d); - ~YoutubeAuth(); - - void SetChatId(const QString &chat_id, const std::string &api_chat_id); - void ResetChat(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service); -}; diff --git a/UI/cmake/feature-youtube.cmake b/UI/cmake/feature-youtube.cmake deleted file mode 100644 index 23d460b70c01c3..00000000000000 --- a/UI/cmake/feature-youtube.cmake +++ /dev/null @@ -1,13 +0,0 @@ -if(YOUTUBE_CLIENTID - AND YOUTUBE_SECRET - AND YOUTUBE_CLIENTID_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND YOUTUBE_SECRET_HASH MATCHES "(0|[a-fA-F0-9]+)") - target_sources(obs-studio PRIVATE auth-youtube.cpp auth-youtube.hpp window-youtube-actions.cpp - window-youtube-actions.hpp youtube-api-wrappers.cpp youtube-api-wrappers.hpp) - - target_enable_feature(obs-studio "YouTube API connection" YOUTUBE_ENABLED) -else() - target_disable_feature(obs-studio "YouTube API connection") - set(YOUTUBE_SECRET_HASH 0) - set(YOUTUBE_CLIENTID_HASH 0) -endif() diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 5cb2219c7f7404..bcf5c14329fb37 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -48,21 +48,6 @@ else() set(RESTREAM_ENABLED ON) endif() -if(NOT DEFINED YOUTUBE_CLIENTID - OR "${YOUTUBE_CLIENTID}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET - OR "${YOUTUBE_SECRET}" STREQUAL "" - OR NOT DEFINED YOUTUBE_CLIENTID_HASH - OR "${YOUTUBE_CLIENTID_HASH}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET_HASH - OR "${YOUTUBE_SECRET_HASH}" STREQUAL "") - set(YOUTUBE_SECRET_HASH "0") - set(YOUTUBE_CLIENTID_HASH "0") - set(YOUTUBE_ENABLED OFF) -else() - set(YOUTUBE_ENABLED ON) -endif() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in ${CMAKE_CURRENT_BINARY_DIR}/ui-config.h) find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) @@ -115,7 +100,6 @@ target_sources( forms/OBSMissingFiles.ui forms/OBSRemux.ui forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui forms/source-toolbar/browser-source-toolbar.ui forms/source-toolbar/color-source-toolbar.ui forms/source-toolbar/device-select-toolbar.ui @@ -339,12 +323,6 @@ if(TARGET OBS::browser-panels) endif() endif() -if(YOUTUBE_ENABLED) - target_compile_definitions(obs PRIVATE YOUTUBE_ENABLED) - target_sources(obs PRIVATE auth-youtube.cpp auth-youtube.hpp youtube-api-wrappers.cpp youtube-api-wrappers.hpp - window-youtube-actions.cpp window-youtube-actions.hpp) -endif() - if(OS_WINDOWS) set_target_properties(obs PROPERTIES WIN32_EXECUTABLE ON OUTPUT_NAME "obs${_ARCH_SUFFIX}") diff --git a/UI/cmake/ui-qt.cmake b/UI/cmake/ui-qt.cmake index 3107f2aaea3fd2..9018e8aa622fca 100644 --- a/UI/cmake/ui-qt.cmake +++ b/UI/cmake/ui-qt.cmake @@ -40,7 +40,6 @@ set(_qt_sources forms/OBSMissingFiles.ui forms/OBSRemux.ui forms/OBSUpdate.ui - forms/OBSYoutubeActions.ui forms/source-toolbar/browser-source-toolbar.ui forms/source-toolbar/color-source-toolbar.ui forms/source-toolbar/device-select-toolbar.ui diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 186b97d16d5497..13b5565bf96d3a 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -1392,101 +1392,3 @@ ContextBar.MediaControls.RestartMedia="Restart Media" ContextBar.MediaControls.PlaylistNext="Next in Playlist" ContextBar.MediaControls.PlaylistPrevious="Previous in Playlist" ContextBar.MediaControls.BlindSeek="Media Seek Widget" - -# YouTube Actions and Auth -YouTube.Auth.Ok="Authorization completed successfully.\nYou can now close this page." -YouTube.Auth.NoCode="The authorization process was not completed." -YouTube.Auth.NoChannels="No channel(s) available on selected account" -YouTube.Auth.WaitingAuth.Title="YouTube User Authorization" -YouTube.Auth.WaitingAuth.Text="Please complete the authorization in your external browser.
If the external browser does not open, follow this link and complete the authorization:
%1" -YouTube.AuthError.Text="Failed to get channel information: %1." - -YouTube.Actions.WindowTitle="YouTube Broadcast Setup - Channel: %1" -YouTube.Actions.CreateNewEvent="Create New Broadcast" -YouTube.Actions.ChooseEvent="Select Existing Broadcast" -YouTube.Actions.Title="Title*" -YouTube.Actions.MyBroadcast="My Broadcast" -YouTube.Actions.Description="Description" -YouTube.Actions.Privacy="Privacy*" -YouTube.Actions.Privacy.Private="Private" -YouTube.Actions.Privacy.Public="Public" -YouTube.Actions.Privacy.Unlisted="Unlisted" -YouTube.Actions.Category="Category" - -YouTube.Actions.Thumbnail="Thumbnail" -YouTube.Actions.Thumbnail.SelectFile="Select file..." -YouTube.Actions.Thumbnail.NoFileSelected="No file selected" -YouTube.Actions.Thumbnail.ClearFile="Clear" - -YouTube.Actions.MadeForKids="Is this video made for kids?*" -YouTube.Actions.MadeForKids.Yes="Yes, it's made for kids" -YouTube.Actions.MadeForKids.No="No, it's not made for kids" -YouTube.Actions.MadeForKids.Help="(?)" -YouTube.Actions.AdditionalSettings="Additional settings" -YouTube.Actions.Latency="Latency" -YouTube.Actions.Latency.Normal="Normal" -YouTube.Actions.Latency.Low="Low" -YouTube.Actions.Latency.UltraLow="Ultra low" -YouTube.Actions.EnableAutoStart="Enable Auto-start" -YouTube.Actions.EnableAutoStop="Enable Auto-stop" -YouTube.Actions.AutoStartStop.TT="Indicates whether this scheduled broadcast should start automatically" -YouTube.Actions.EnableDVR="Enable DVR" -YouTube.Actions.360Video="360 video" -YouTube.Actions.360Video.Help="(?)" -YouTube.Actions.ScheduleForLater="Schedule for later" -YouTube.Actions.RememberSettings="Remember these settings" - -YouTube.Actions.Create_Ready="Create broadcast" -YouTube.Actions.Create_GoLive="Create broadcast and start streaming" -YouTube.Actions.Choose_Ready="Select broadcast" -YouTube.Actions.Choose_GoLive="Select broadcast and start streaming" -YouTube.Actions.Create_Schedule="Schedule broadcast" -YouTube.Actions.Create_Schedule_Ready="Schedule and select broadcast" -YouTube.Actions.Dashboard="Open YouTube Studio" - -YouTube.Actions.Error.Title="Live broadcast creation error" -YouTube.Actions.Error.Text="YouTube access error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" -YouTube.Actions.Error.General="YouTube access error. Please check your network connection or your YouTube server access." -YouTube.Actions.Error.NoBroadcastCreated="Broadcast creation error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" -YouTube.Actions.Error.NoStreamCreated="No stream created. Please relink your account." -YouTube.Actions.Error.YouTubeApi="YouTube API Error. Please see the log file for more information." -YouTube.Actions.Error.BroadcastNotFound="The selected broadcast was not found." -YouTube.Actions.Error.FileMissing="Selected file does not exist." -YouTube.Actions.Error.FileOpeningFailed="Failed opening selected file." -YouTube.Actions.Error.FileTooLarge="Selected file is too large (Limit: 2 MiB)." -YouTube.Actions.Error.BroadcastTransitionFailed="Transitioning the broadcast failed: %1

If this error persists open the broadcast in YouTube Studio and try manually." -YouTube.Actions.Error.BroadcastTestStarting="Broadcast is transitioning to the test stage, this can take some time. Please try again in 10-30 seconds." - -YouTube.Actions.EventsLoading="Loading list of events..." -YouTube.Actions.EventCreated.Title="Event Created" -YouTube.Actions.EventCreated.Text="Event created successfully." - -YouTube.Actions.Stream="Stream" -YouTube.Actions.Stream.ScheduledFor="Scheduled for %1" -YouTube.Actions.Stream.Resume="Resume interrupted stream" -YouTube.Actions.Stream.YTStudio="Automatically created by YouTube Studio" - -YouTube.Actions.Notify.Title="YouTube" -YouTube.Actions.Notify.CreatingBroadcast="Creating a new Live Broadcast, please wait..." - -YouTube.Actions.AutoStartStreamingWarning.Title="Manual start required" -YouTube.Actions.AutoStartStreamingWarning="Auto-start is disabled for this event, click \"Go Live\" to start your broadcast." -YouTube.Actions.AutoStopStreamingWarning="You will not be able to reconnect.
Your stream will stop and you will no longer be live." - -YouTube.Chat.Input.Send="Send" -YouTube.Chat.Input.Placeholder="Enter message here..." -YouTube.Chat.Input.Sending="Sending..." -YouTube.Chat.Error.Title="Error while sending message" -YouTube.Chat.Error.Text="The message couldn't be sent: %1" - -# YouTube API errors in format "YouTube.Errors." -YouTube.Errors.liveStreamingNotEnabled="Live streaming is not enabled on the selected YouTube channel.

See youtube.com/features for more information." -YouTube.Errors.livePermissionBlocked="Live streaming is unavailable on the selected YouTube Channel.
Please note that it may take up to 24 hours for live streaming to become available after enabling it in your channel settings.

See youtube.com/features for details." -YouTube.Errors.errorExecutingTransition="Transition failed due to a backend error. Please try again in a few seconds." -YouTube.Errors.errorStreamInactive="YouTube is not receiving data for your stream. Please check your configuration and try again." -YouTube.Errors.invalidTransition="The attempted transition was invalid. This may be due to the stream not having finished a previous transition. Please wait a few seconds and try again." -# Chat errors -YouTube.Errors.liveChatDisabled="Live chat is disabled on this stream." -YouTube.Errors.liveChatEnded="Live stream has ended." -YouTube.Errors.messageTextInvalid="The message text is not valid." -YouTube.Errors.rateLimitExceeded="You are sending messages too quickly." diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index fc495c32d32cd3..719ff5f7eb0b4e 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -60,11 +60,6 @@ #endif #include "window-projector.hpp" #include "window-remux.hpp" -#ifdef YOUTUBE_ENABLED -#include "auth-youtube.hpp" -#include "window-youtube-actions.hpp" -#include "youtube-api-wrappers.hpp" -#endif #include "qt-wrappers.hpp" #include "context-bar-controls.hpp" #include "obs-proxy-style.hpp" @@ -276,9 +271,6 @@ void setupDockAction(QDockWidget *dock) extern void RegisterTwitchAuth(); extern void RegisterRestreamAuth(); -#ifdef YOUTUBE_ENABLED -extern void RegisterYoutubeAuth(); -#endif OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), @@ -293,9 +285,6 @@ OBSBasic::OBSBasic(QWidget *parent) #ifdef RESTREAM_ENABLED RegisterRestreamAuth(); #endif -#ifdef YOUTUBE_ENABLED - RegisterYoutubeAuth(); -#endif setAcceptDrops(true); diff --git a/UI/youtube-api-wrappers.cpp b/UI/youtube-api-wrappers.cpp deleted file mode 100644 index 8b79276cbb0bc4..00000000000000 --- a/UI/youtube-api-wrappers.cpp +++ /dev/null @@ -1,605 +0,0 @@ -#include "youtube-api-wrappers.hpp" - -#include -#include -#include - -#include -#include - -#include "auth-youtube.hpp" -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include - -using namespace json11; - -/* ------------------------------------------------------------------------- */ -#define YOUTUBE_LIVE_API_URL "https://www.googleapis.com/youtube/v3" - -#define YOUTUBE_LIVE_STREAM_URL YOUTUBE_LIVE_API_URL "/liveStreams" -#define YOUTUBE_LIVE_BROADCAST_URL YOUTUBE_LIVE_API_URL "/liveBroadcasts" -#define YOUTUBE_LIVE_BROADCAST_TRANSITION_URL \ - YOUTUBE_LIVE_BROADCAST_URL "/transition" -#define YOUTUBE_LIVE_BROADCAST_BIND_URL YOUTUBE_LIVE_BROADCAST_URL "/bind" - -#define YOUTUBE_LIVE_CHANNEL_URL YOUTUBE_LIVE_API_URL "/channels" -#define YOUTUBE_LIVE_TOKEN_URL "https://oauth2.googleapis.com/token" -#define YOUTUBE_LIVE_VIDEOCATEGORIES_URL YOUTUBE_LIVE_API_URL "/videoCategories" -#define YOUTUBE_LIVE_VIDEOS_URL YOUTUBE_LIVE_API_URL "/videos" -#define YOUTUBE_LIVE_CHAT_MESSAGES_URL YOUTUBE_LIVE_API_URL "/liveChat/messages" -#define YOUTUBE_LIVE_THUMBNAIL_URL \ - "https://www.googleapis.com/upload/youtube/v3/thumbnails/set" - -#define DEFAULT_BROADCASTS_PER_QUERY \ - "50" // acceptable values are 0 to 50, inclusive -/* ------------------------------------------------------------------------- */ - -bool IsYouTubeService(const std::string &service) -{ - auto it = find_if(youtubeServices.begin(), youtubeServices.end(), - [&service](const Auth::Def &yt) { - return service == yt.service; - }); - return it != youtubeServices.end(); -} - -bool YoutubeApiWrappers::GetTranslatedError(QString &error_message) -{ - QString translated = - QTStr("YouTube.Errors." + lastErrorReason.toUtf8()); - // No translation found - if (translated.startsWith("YouTube.Errors.")) - return false; - error_message = translated; - return true; -} - -YoutubeApiWrappers::YoutubeApiWrappers(const Def &d) : YoutubeAuth(d) {} - -bool YoutubeApiWrappers::TryInsertCommand(const char *url, - const char *content_type, - std::string request_type, - const char *data, Json &json_out, - long *error_code, int data_size) -{ - long httpStatusCode = 0; - -#ifdef _DEBUG - blog(LOG_DEBUG, "YouTube API command URL: %s", url); - if (data && data[0] == '{') // only log JSON data - blog(LOG_DEBUG, "YouTube API command data: %s", data); -#endif - if (token.empty()) - return false; - std::string output; - std::string error; - // Increase timeout by the time it takes to transfer `data_size` at 1 Mbps - int timeout = 5 + data_size / 125000; - bool success = GetRemoteFile(url, output, error, &httpStatusCode, - content_type, request_type, data, - {"Authorization: Bearer " + token}, - nullptr, timeout, false, data_size); - if (error_code) - *error_code = httpStatusCode; - - if (!success || output.empty()) { - if (!error.empty()) - blog(LOG_WARNING, "YouTube API request failed: %s", - error.c_str()); - return false; - } - - json_out = Json::parse(output, error); -#ifdef _DEBUG - blog(LOG_DEBUG, "YouTube API command answer: %s", - json_out.dump().c_str()); -#endif - if (!error.empty()) { - return false; - } - return httpStatusCode < 400; -} - -bool YoutubeApiWrappers::UpdateAccessToken() -{ - if (refresh_token.empty()) { - return false; - } - - std::string clientid = YOUTUBE_CLIENTID; - std::string secret = YOUTUBE_SECRET; - deobfuscate_str(&clientid[0], YOUTUBE_CLIENTID_HASH); - deobfuscate_str(&secret[0], YOUTUBE_SECRET_HASH); - - std::string r_token = - QUrl::toPercentEncoding(refresh_token.c_str()).toStdString(); - const QString url = YOUTUBE_LIVE_TOKEN_URL; - const QString data_template = "client_id=%1" - "&client_secret=%2" - "&refresh_token=%3" - "&grant_type=refresh_token"; - const QString data = data_template.arg(QString(clientid.c_str()), - QString(secret.c_str()), - QString(r_token.c_str())); - Json json_out; - bool success = TryInsertCommand(QT_TO_UTF8(url), - "application/x-www-form-urlencoded", "", - QT_TO_UTF8(data), json_out); - - if (!success || json_out.object_items().find("error") != - json_out.object_items().end()) - return false; - token = json_out["access_token"].string_value(); - return token.empty() ? false : true; -} - -bool YoutubeApiWrappers::InsertCommand(const char *url, - const char *content_type, - std::string request_type, - const char *data, Json &json_out, - int data_size) -{ - long error_code; - bool success = TryInsertCommand(url, content_type, request_type, data, - json_out, &error_code, data_size); - - if (error_code == 401) { - // Attempt to update access token and try again - if (!UpdateAccessToken()) - return false; - success = TryInsertCommand(url, content_type, request_type, - data, json_out, &error_code, - data_size); - } - - if (json_out.object_items().find("error") != - json_out.object_items().end()) { - blog(LOG_ERROR, - "YouTube API error:\n\tHTTP status: %ld\n\tURL: %s\n\tJSON: %s", - error_code, url, json_out.dump().c_str()); - - lastError = json_out["error"]["code"].int_value(); - lastErrorReason = - QString(json_out["error"]["errors"][0]["reason"] - .string_value() - .c_str()); - lastErrorMessage = QString( - json_out["error"]["message"].string_value().c_str()); - - // The existence of an error implies non-success even if the HTTP status code disagrees. - success = false; - } - return success; -} - -bool YoutubeApiWrappers::GetChannelDescription( - ChannelDescription &channel_description) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_CHANNEL_URL - "?part=snippet,contentDetails,statistics" - "&mine=true"; - Json json_out; - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) { - return false; - } - - if (json_out["pageInfo"]["totalResults"].int_value() == 0) { - lastErrorMessage = QTStr("YouTube.Auth.NoChannels"); - return false; - } - - channel_description.id = - QString(json_out["items"][0]["id"].string_value().c_str()); - channel_description.title = QString( - json_out["items"][0]["snippet"]["title"].string_value().c_str()); - return channel_description.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::InsertBroadcast(BroadcastDescription &broadcast) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=snippet,status,contentDetails"; - const Json data = Json::object{ - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(broadcast.title)}, - {"description", QT_TO_UTF8(broadcast.description)}, - {"scheduledStartTime", - QT_TO_UTF8(broadcast.schedul_date_time)}, - }}, - {"status", - Json::object{ - {"privacyStatus", QT_TO_UTF8(broadcast.privacy)}, - {"selfDeclaredMadeForKids", broadcast.made_for_kids}, - }}, - {"contentDetails", - Json::object{ - {"latencyPreference", QT_TO_UTF8(broadcast.latency)}, - {"enableAutoStart", broadcast.auto_start}, - {"enableAutoStop", broadcast.auto_stop}, - {"enableDvr", broadcast.dvr}, - {"projection", QT_TO_UTF8(broadcast.projection)}, - { - "monitorStream", - Json::object{ - {"enableMonitorStream", false}, - }, - }, - }}, - }; - Json json_out; - if (!InsertCommand(url, "application/json", "", data.dump().c_str(), - json_out)) { - return false; - } - broadcast.id = QString(json_out["id"].string_value().c_str()); - return broadcast.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::InsertStream(StreamDescription &stream) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_STREAM_URL - "?part=snippet,cdn,status,contentDetails"; - const Json data = Json::object{ - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(stream.title)}, - }}, - {"cdn", - Json::object{ - {"frameRate", "variable"}, - {"ingestionType", "rtmp"}, - {"resolution", "variable"}, - }}, - {"contentDetails", Json::object{{"isReusable", false}}}, - }; - Json json_out; - if (!InsertCommand(url, "application/json", "", data.dump().c_str(), - json_out)) { - return false; - } - stream.id = QString(json_out["id"].string_value().c_str()); - stream.name = QString(json_out["cdn"]["ingestionInfo"]["streamName"] - .string_value() - .c_str()); - return stream.id.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::BindStream(const QString broadcast_id, - const QString stream_id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QString url_template = YOUTUBE_LIVE_BROADCAST_BIND_URL - "?id=%1" - "&streamId=%2" - "&part=id,snippet,contentDetails,status"; - const QString url = url_template.arg(broadcast_id, stream_id); - const Json data = Json::object{}; - this->broadcast_id = broadcast_id; - return InsertCommand(QT_TO_UTF8(url), "application/json", "", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::GetBroadcastsList(Json &json_out, const QString &page, - const QString &status) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=snippet,contentDetails,status" - "&broadcastType=all&maxResults=" DEFAULT_BROADCASTS_PER_QUERY; - - if (status.isEmpty()) - url += "&mine=true"; - else - url += "&broadcastStatus=" + status.toUtf8(); - - if (!page.isEmpty()) - url += "&pageToken=" + page.toUtf8(); - return InsertCommand(url, "application/json", "", nullptr, json_out); -} - -bool YoutubeApiWrappers::GetVideoCategoriesList( - QVector &category_list_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QString url_template = YOUTUBE_LIVE_VIDEOCATEGORIES_URL - "?part=snippet" - "®ionCode=%1" - "&hl=%2"; - /* - * All OBS locale regions aside from "US" are missing category id 29 - * ("Nonprofits & Activism"), but it is still available to channels - * set to those regions via the YouTube Studio website. - * To work around this inconsistency with the API all locales will - * use the "US" region and only set the language part for localisation. - * It is worth noting that none of the regions available on YouTube - * feature any category not also available to the "US" region. - */ - QString url = url_template.arg("US", QLocale().name()); - - Json json_out; - if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", nullptr, - json_out)) { - if (lastErrorReason != "unsupportedLanguageCode" && - lastErrorReason != "invalidLanguage") - return false; - // Try again with en-US if YouTube error indicates an unsupported locale - url = url_template.arg("US", "en_US"); - if (!InsertCommand(QT_TO_UTF8(url), "application/json", "", - nullptr, json_out)) - return false; - } - category_list_out = {}; - for (auto &j : json_out["items"].array_items()) { - // Assignable only. - if (j["snippet"]["assignable"].bool_value()) { - category_list_out.push_back( - {j["id"].string_value().c_str(), - j["snippet"]["title"].string_value().c_str()}); - } - } - return category_list_out.isEmpty() ? false : true; -} - -bool YoutubeApiWrappers::SetVideoCategory(const QString &video_id, - const QString &video_title, - const QString &video_description, - const QString &categorie_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - const QByteArray url = YOUTUBE_LIVE_VIDEOS_URL "?part=snippet"; - const Json data = Json::object{ - {"id", QT_TO_UTF8(video_id)}, - {"snippet", - Json::object{ - {"title", QT_TO_UTF8(video_title)}, - {"description", QT_TO_UTF8(video_description)}, - {"categoryId", QT_TO_UTF8(categorie_id)}, - }}, - }; - Json json_out; - return InsertCommand(url, "application/json", "PUT", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::SetVideoThumbnail(const QString &video_id, - const QString &thumbnail_file) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - // Make sure the file hasn't been deleted since originally selecting it - if (!QFile::exists(thumbnail_file)) { - lastErrorMessage = QTStr("YouTube.Actions.Error.FileMissing"); - return false; - } - - QFile thumbFile(thumbnail_file); - if (!thumbFile.open(QFile::ReadOnly)) { - lastErrorMessage = - QTStr("YouTube.Actions.Error.FileOpeningFailed"); - return false; - } - - const QByteArray fileContents = thumbFile.readAll(); - const QString mime = - QMimeDatabase().mimeTypeForData(fileContents).name(); - - const QString url = YOUTUBE_LIVE_THUMBNAIL_URL "?videoId=" + video_id; - Json json_out; - return InsertCommand(QT_TO_UTF8(url), QT_TO_UTF8(mime), "POST", - fileContents.constData(), json_out, - fileContents.size()); -} - -bool YoutubeApiWrappers::StartBroadcast(const QString &broadcast_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - Json json_out; - if (!FindBroadcast(broadcast_id, json_out)) - return false; - - auto lifeCycleStatus = - json_out["items"][0]["status"]["lifeCycleStatus"].string_value(); - - if (lifeCycleStatus == "live" || lifeCycleStatus == "liveStarting") - // Broadcast is already (going to be) live - return true; - else if (lifeCycleStatus == "testStarting") { - // User will need to wait a few seconds before attempting to start broadcast - lastErrorMessage = - QTStr("YouTube.Actions.Error.BroadcastTestStarting"); - lastErrorReason.clear(); - return false; - } - - // Only reset if broadcast has monitoring enabled and is not already in "testing" mode - auto monitorStreamEnabled = - json_out["items"][0]["contentDetails"]["monitorStream"] - ["enableMonitorStream"] - .bool_value(); - if (lifeCycleStatus != "testing" && monitorStreamEnabled && - !ResetBroadcast(broadcast_id, json_out)) - return false; - - const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL - "?id=%1" - "&broadcastStatus=%2" - "&part=status"; - const QString live = url_template.arg(broadcast_id, "live"); - bool success = InsertCommand(QT_TO_UTF8(live), "application/json", - "POST", "{}", json_out); - // Return a success if the command failed, but was redundant (broadcast already live) - return success || lastErrorReason == "redundantTransition"; -} - -bool YoutubeApiWrappers::StartLatestBroadcast() -{ - return StartBroadcast(this->broadcast_id); -} - -bool YoutubeApiWrappers::StopBroadcast(const QString &broadcast_id) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - const QString url_template = YOUTUBE_LIVE_BROADCAST_TRANSITION_URL - "?id=%1" - "&broadcastStatus=complete" - "&part=status"; - const QString url = url_template.arg(broadcast_id); - Json json_out; - bool success = InsertCommand(QT_TO_UTF8(url), "application/json", - "POST", "{}", json_out); - // Return a success if the command failed, but was redundant (broadcast already stopped) - return success || lastErrorReason == "redundantTransition"; -} - -bool YoutubeApiWrappers::StopLatestBroadcast() -{ - return StopBroadcast(this->broadcast_id); -} - -void YoutubeApiWrappers::SetBroadcastId(QString &broadcast_id) -{ - this->broadcast_id = broadcast_id; -} - -QString YoutubeApiWrappers::GetBroadcastId() -{ - return this->broadcast_id; -} - -bool YoutubeApiWrappers::ResetBroadcast(const QString &broadcast_id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - - auto snippet = json_out["items"][0]["snippet"]; - auto status = json_out["items"][0]["status"]; - auto contentDetails = json_out["items"][0]["contentDetails"]; - auto monitorStream = contentDetails["monitorStream"]; - - const Json data = Json::object{ - {"id", QT_TO_UTF8(broadcast_id)}, - {"snippet", - Json::object{ - {"title", snippet["title"]}, - {"description", snippet["description"]}, - {"scheduledStartTime", snippet["scheduledStartTime"]}, - {"scheduledEndTime", snippet["scheduledEndTime"]}, - }}, - {"status", - Json::object{ - {"privacyStatus", status["privacyStatus"]}, - {"madeForKids", status["madeForKids"]}, - {"selfDeclaredMadeForKids", - status["selfDeclaredMadeForKids"]}, - }}, - {"contentDetails", - Json::object{ - { - "monitorStream", - Json::object{ - {"enableMonitorStream", false}, - {"broadcastStreamDelayMs", - monitorStream["broadcastStreamDelayMs"]}, - }, - }, - {"enableAutoStart", contentDetails["enableAutoStart"]}, - {"enableAutoStop", contentDetails["enableAutoStop"]}, - {"enableClosedCaptions", - contentDetails["enableClosedCaptions"]}, - {"enableDvr", contentDetails["enableDvr"]}, - {"enableContentEncryption", - contentDetails["enableContentEncryption"]}, - {"enableEmbed", contentDetails["enableEmbed"]}, - {"recordFromStart", contentDetails["recordFromStart"]}, - {"startWithSlate", contentDetails["startWithSlate"]}, - }}, - }; - - const QString put = YOUTUBE_LIVE_BROADCAST_URL - "?part=id,snippet,contentDetails,status"; - return InsertCommand(QT_TO_UTF8(put), "application/json", "PUT", - data.dump().c_str(), json_out); -} - -bool YoutubeApiWrappers::FindBroadcast(const QString &id, - json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_BROADCAST_URL - "?part=id,snippet,contentDetails,status" - "&broadcastType=all&maxResults=1"; - url += "&id=" + id.toUtf8(); - - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) - return false; - - auto items = json_out["items"].array_items(); - if (items.size() != 1) { - lastErrorMessage = - QTStr("YouTube.Actions.Error.BroadcastNotFound"); - return false; - } - - return true; -} - -bool YoutubeApiWrappers::FindStream(const QString &id, json11::Json &json_out) -{ - lastErrorMessage.clear(); - lastErrorReason.clear(); - QByteArray url = YOUTUBE_LIVE_STREAM_URL "?part=id,snippet,cdn,status" - "&maxResults=1"; - url += "&id=" + id.toUtf8(); - - if (!InsertCommand(url, "application/json", "", nullptr, json_out)) - return false; - - auto items = json_out["items"].array_items(); - if (items.size() != 1) { - lastErrorMessage = "No active broadcast found."; - return false; - } - - return true; -} - -bool YoutubeApiWrappers::SendChatMessage(const std::string &chat_id, - const QString &message) -{ - QByteArray url = YOUTUBE_LIVE_CHAT_MESSAGES_URL "?part=snippet"; - - json11::Json json_in = Json::object{ - {"snippet", - Json::object{ - {"liveChatId", chat_id}, - {"type", "textMessageEvent"}, - {"textMessageDetails", - Json::object{{"messageText", QT_TO_UTF8(message)}}}, - }}}; - - json11::Json json_out; - return InsertCommand(url, "application/json", "POST", - json_in.dump().c_str(), json_out); -} diff --git a/UI/youtube-api-wrappers.hpp b/UI/youtube-api-wrappers.hpp deleted file mode 100644 index dcabc7020c73c4..00000000000000 --- a/UI/youtube-api-wrappers.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once - -#include "auth-youtube.hpp" - -#include -#include - -struct ChannelDescription { - QString id; - QString title; -}; - -struct StreamDescription { - QString id; - QString name; - QString title; -}; - -struct CategoryDescription { - QString id; - QString title; -}; - -struct BroadcastDescription { - QString id; - QString title; - QString description; - QString privacy; - CategoryDescription category; - QString latency; - bool made_for_kids; - bool auto_start; - bool auto_stop; - bool dvr; - bool schedul_for_later; - QString schedul_date_time; - QString projection; -}; - -bool IsYouTubeService(const std::string &service); - -class YoutubeApiWrappers : public YoutubeAuth { - Q_OBJECT - - bool TryInsertCommand(const char *url, const char *content_type, - std::string request_type, const char *data, - json11::Json &ret, long *error_code = nullptr, - int data_size = 0); - bool UpdateAccessToken(); - bool InsertCommand(const char *url, const char *content_type, - std::string request_type, const char *data, - json11::Json &ret, int data_size = 0); - -public: - YoutubeApiWrappers(const Def &d); - - bool GetChannelDescription(ChannelDescription &channel_description); - bool InsertBroadcast(BroadcastDescription &broadcast); - bool InsertStream(StreamDescription &stream); - bool BindStream(const QString broadcast_id, const QString stream_id, - json11::Json &json_out); - bool GetBroadcastsList(json11::Json &json_out, const QString &page, - const QString &status); - bool - GetVideoCategoriesList(QVector &category_list_out); - bool SetVideoCategory(const QString &video_id, - const QString &video_title, - const QString &video_description, - const QString &categorie_id); - bool SetVideoThumbnail(const QString &video_id, - const QString &thumbnail_file); - bool StartBroadcast(const QString &broadcast_id); - bool StopBroadcast(const QString &broadcast_id); - bool ResetBroadcast(const QString &broadcast_id, - json11::Json &json_out); - bool StartLatestBroadcast(); - bool StopLatestBroadcast(); - bool SendChatMessage(const std::string &chat_id, - const QString &message); - - void SetBroadcastId(QString &broadcast_id); - QString GetBroadcastId(); - - bool FindBroadcast(const QString &id, json11::Json &json_out); - bool FindStream(const QString &id, json11::Json &json_out); - - QString GetLastError() { return lastErrorMessage; }; - bool GetTranslatedError(QString &error_message); - -private: - QString broadcast_id; - - int lastError; - QString lastErrorMessage; - QString lastErrorReason; -}; diff --git a/UI/forms/OBSYoutubeActions.ui b/plugins/obs-youtube/forms/OBSYoutubeActions.ui similarity index 100% rename from UI/forms/OBSYoutubeActions.ui rename to plugins/obs-youtube/forms/OBSYoutubeActions.ui diff --git a/UI/window-youtube-actions.cpp b/plugins/obs-youtube/window-youtube-actions.cpp similarity index 100% rename from UI/window-youtube-actions.cpp rename to plugins/obs-youtube/window-youtube-actions.cpp diff --git a/UI/window-youtube-actions.hpp b/plugins/obs-youtube/window-youtube-actions.hpp similarity index 100% rename from UI/window-youtube-actions.hpp rename to plugins/obs-youtube/window-youtube-actions.hpp From c2666132847d0c485f9e4c6df228a0a9d8b7dd92 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 21 Jun 2023 18:53:58 +0200 Subject: [PATCH 34/65] service-base: Add login reason Allow services using this base to gave the user explaination of why the user might to login again. --- .../service-base/oauth-service-base.cpp | 24 ++++++++++++------- .../service-base/oauth-service-base.hpp | 14 ++++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/deps/oauth-service/service-base/oauth-service-base.cpp b/deps/oauth-service/service-base/oauth-service-base.cpp index 5cdcf89613778a..efb3ace5c73f50 100644 --- a/deps/oauth-service/service-base/oauth-service-base.cpp +++ b/deps/oauth-service/service-base/oauth-service-base.cpp @@ -21,7 +21,11 @@ void ServiceBase::Setup(obs_data_t *data, bool deferUiFunction) if (!data) return; - SetSettings(obs_data_get_obj(data, DATA_NAME_SETTINGS)); + if (obs_data_has_user_value(data, DATA_NAME_SETTINGS)) { + OBSDataAutoRelease settingsData = + obs_data_get_obj(data, DATA_NAME_SETTINGS); + SetSettings(settingsData); + } OBSDataAutoRelease oauthData = obs_data_get_obj(data, DATA_NAME_OAUTH); if (!oauthData) @@ -50,7 +54,7 @@ void ServiceBase::Setup(obs_data_t *data, bool deferUiFunction) blog(LOG_WARNING, "[%s][%s]: Old scope version detected, the user will be asked to re-login", PluginLogName(), __FUNCTION__); - ReLogin(deferUiFunction); + ReLogin(LoginReason::SCOPE_CHANGE, deferUiFunction); return; } @@ -72,7 +76,8 @@ void ServiceBase::Setup(obs_data_t *data, bool deferUiFunction) blog(LOG_WARNING, "[%s][%s]: Access Token has expired, the user will be asked to re-login", PluginLogName(), __FUNCTION__); - ReLogin(deferUiFunction); + ReLogin(LoginReason::REFRESH_TOKEN_FAILED, + deferUiFunction); return; } } @@ -86,12 +91,13 @@ void ServiceBase::Setup(obs_data_t *data, bool deferUiFunction) } } -void ServiceBase::ReLogin(bool deferUiFunction) +void ServiceBase::ReLogin(const LoginReason &reason, bool deferUiFunction) { if (deferUiFunction) { defferedLogin = true; + defferedLoginReason = reason; } else { - Login(); + Login(reason); } } @@ -226,12 +232,12 @@ void ServiceBase::OBSEvent(obs_frontend_event event) /* If duplication, reset token and ask to login */ if (duplicationMarker) { DuplicationReset(); - Login(); + Login(LoginReason::PROFILE_DUPLICATION); } [[fallthrough]]; case OBS_FRONTEND_EVENT_FINISHED_LOADING: if (defferedLogin) { - Login(); + Login(defferedLoginReason); defferedLogin = false; } @@ -275,14 +281,14 @@ void ServiceBase::RemoveBondedService(obs_service_t *service) } } -bool ServiceBase::Login() +bool ServiceBase::Login(const LoginReason &reason) try { if (connected) return true; std::string code; std::string redirectUri; - if (!LoginInternal(code, redirectUri)) + if (!LoginInternal(reason, code, redirectUri)) return false; OAuth::AccessTokenResponse response; diff --git a/deps/oauth-service/service-base/oauth-service-base.hpp b/deps/oauth-service/service-base/oauth-service-base.hpp index 0f9829af61e2a2..0f7a337873390f 100644 --- a/deps/oauth-service/service-base/oauth-service-base.hpp +++ b/deps/oauth-service/service-base/oauth-service-base.hpp @@ -12,6 +12,13 @@ namespace OAuth { +enum class LoginReason : int { + CONNECT, + SCOPE_CHANGE, + REFRESH_TOKEN_FAILED, + PROFILE_DUPLICATION, +}; + class ServiceBase { /* NOTE: To support service copy (e.g. settings) */ std::vector bondedServices; @@ -26,9 +33,10 @@ class ServiceBase { std::string refreshToken; bool defferedLogin = false; + LoginReason defferedLoginReason; bool defferedLoadFrontend = false; - inline void ReLogin(bool deferUiFunction); + inline void ReLogin(const LoginReason &reason, bool deferUiFunction); inline void ApplyNewTokens(const OAuth::AccessTokenResponse &response); obs_data_t *GetOAuthData(); @@ -55,7 +63,7 @@ class ServiceBase { virtual int64_t ScopeVersion() = 0; - virtual bool LoginInternal(std::string &code, + virtual bool LoginInternal(const LoginReason &reason, std::string &code, std::string &redirectUri) = 0; virtual bool SignOutInternal() = 0; @@ -90,7 +98,7 @@ class ServiceBase { bool Connected() { return connected; } - bool Login(); + bool Login(const LoginReason &reason = LoginReason::CONNECT); bool SignOut(); }; From 7f4c1da0b1d3c5305e1698f05818b3e97fa78545 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 22 Jun 2023 11:27:44 +0200 Subject: [PATCH 35/65] UI,docs: Add broadcast flow in the frontend API --- UI/CMakeLists.txt | 2 + UI/api-interface.cpp | 27 + UI/auth-base.cpp | 4 - UI/auth-base.hpp | 2 - UI/broadcast-flow.cpp | 116 +++++ UI/broadcast-flow.hpp | 46 ++ UI/cmake/legacy.cmake | 2 + UI/data/locale/en-US.ini | 4 + UI/forms/OBSBasic.ui | 5 +- UI/obs-frontend-api/obs-frontend-api.cpp | 83 +++ UI/obs-frontend-api/obs-frontend-api.h | 61 +++ UI/obs-frontend-api/obs-frontend-internal.hpp | 7 + UI/window-basic-main-service.cpp | 78 ++- UI/window-basic-main.cpp | 477 ++++++++---------- UI/window-basic-main.hpp | 36 +- docs/sphinx/index.rst | 1 + .../reference-frontend-broadcast-flow-api.rst | 219 ++++++++ 17 files changed, 867 insertions(+), 303 deletions(-) create mode 100644 UI/broadcast-flow.cpp create mode 100644 UI/broadcast-flow.hpp create mode 100644 docs/sphinx/reference-frontend-broadcast-flow-api.rst diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 13f39f84213b32..a4c54effb474f6 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -74,6 +74,8 @@ target_sources( auth-listener.hpp auth-oauth.cpp auth-oauth.hpp + broadcast-flow.cpp + broadcast-flow.hpp display-helpers.hpp multiview.cpp multiview.hpp diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index a2f9149a808695..06540d4d3969df 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -804,6 +804,33 @@ struct OBSStudioAPI : obs_frontend_callbacks { undo_data, redo_data, repeatable); } + void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, + size_t size) override + { + struct obs_frontend_broadcast_flow data = {0}; + if (size > sizeof(data)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_broadcast_flow with size " + "%llu which is more than OBS Studio currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(data)); + return; + } + + memcpy(&data, flow, size); + + main->AddBroadcastFlow(service, data); + } + + void obs_frontend_remove_broadcast_flow( + const obs_service_t *service) override + { + main->RemoveBroadcastFlow(service); + } + void on_load(obs_data_t *settings) override { for (size_t i = saveCallbacks.size(); i > 0; i--) { diff --git a/UI/auth-base.cpp b/UI/auth-base.cpp index b1a4315fd29dae..8ffc68c5344994 100644 --- a/UI/auth-base.cpp +++ b/UI/auth-base.cpp @@ -61,11 +61,7 @@ void Auth::Load() if (main->auth) { if (main->auth->LoadInternal()) { main->auth->LoadUI(); - main->SetBroadcastFlowEnabled( - main->auth->broadcastFlow()); } - } else { - main->SetBroadcastFlowEnabled(false); } } diff --git a/UI/auth-base.hpp b/UI/auth-base.hpp index 8d727f6cf7e6cc..97a2148406af15 100644 --- a/UI/auth-base.hpp +++ b/UI/auth-base.hpp @@ -35,7 +35,6 @@ class Auth : public QObject { std::string service; Type type; bool externalOAuth; - bool usesBroadcastFlow; }; typedef std::function()> create_cb; @@ -46,7 +45,6 @@ class Auth : public QObject { inline Type type() const { return def.type; } inline const char *service() const { return def.service.c_str(); } inline bool external() const { return def.externalOAuth; } - inline bool broadcastFlow() const { return def.usesBroadcastFlow; } virtual void LoadUI() {} diff --git a/UI/broadcast-flow.cpp b/UI/broadcast-flow.cpp new file mode 100644 index 00000000000000..8af6441337d176 --- /dev/null +++ b/UI/broadcast-flow.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "broadcast-flow.hpp" + +void OBSBroadcastFlow::ManageBroadcast(bool streamingActive) +{ + if (flow.manage_broadcast2) { + flow.manage_broadcast2(flow.priv, streamingActive); + } else if (!streamingActive) { + flow.manage_broadcast(flow.priv); + } + + if (streamingActive) + return; + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); +} + +bool OBSBroadcastFlow::AllowManagingWhileStreaming() +{ + return (flow.flags & OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) != + 0; +} + +obs_broadcast_state OBSBroadcastFlow::BroadcastState() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) == + 0 && + broadcastState == OBS_BROADCAST_INACTIVE) { + return OBS_BROADCAST_NONE; + } + + return broadcastState; +} + +obs_broadcast_start OBSBroadcastFlow::BroadcastStartType() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) == + 0 && + broadcastStartType == OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_START_WITH_STREAM; + } + + return broadcastStartType; +} + +obs_broadcast_stop OBSBroadcastFlow::BroadcastStopType() +{ + if ((flow.flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) == + 0 && + broadcastStopType == OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_STOP_NEVER; + } + + return broadcastStopType; +} +bool OBSBroadcastFlow::DifferedStartBroadcast() +{ + if (BroadcastStartType() != OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return false; + } + + flow.differed_start_broadcast(flow.priv); + broadcastState = flow.get_broadcast_state(flow.priv); + + /* If not active after start, failure */ + return broadcastState == OBS_BROADCAST_ACTIVE; +} + +obs_broadcast_stream_state OBSBroadcastFlow::IsBroadcastStreamActive() +{ + if (BroadcastStartType() != OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + return OBS_BROADCAST_STREAM_FAILURE; + } + + return flow.is_broadcast_stream_active(flow.priv); +} + +bool OBSBroadcastFlow::DifferedStopBroadcast() +{ + if (BroadcastStopType() != OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) + return false; + + bool ret = flow.differed_stop_broadcast(flow.priv); + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); + + return ret; +} + +void OBSBroadcastFlow::StopStreaming() +{ + flow.stopped_streaming(flow.priv); + + broadcastState = flow.get_broadcast_state(flow.priv); + broadcastStartType = flow.get_broadcast_start_type(flow.priv); + broadcastStopType = flow.get_broadcast_stop_type(flow.priv); +} + +std::string OBSBroadcastFlow::GetLastError() +{ + if (!flow.get_last_error) + return ""; + + const char *error = flow.get_last_error(flow.priv); + if (!error) + return ""; + + return error; +} diff --git a/UI/broadcast-flow.hpp b/UI/broadcast-flow.hpp new file mode 100644 index 00000000000000..8df5ff6750439e --- /dev/null +++ b/UI/broadcast-flow.hpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +class OBSBroadcastFlow { + const obs_service_t *boundedService = nullptr; + + obs_frontend_broadcast_flow flow = {0}; + + obs_broadcast_state broadcastState; + obs_broadcast_start broadcastStartType; + obs_broadcast_stop broadcastStopType; + +public: + inline OBSBroadcastFlow(const obs_service_t *service, + const obs_frontend_broadcast_flow &flow) + : boundedService(service), + flow(flow), + broadcastState(flow.get_broadcast_state(flow.priv)), + broadcastStartType(flow.get_broadcast_start_type(flow.priv)), + broadcastStopType(flow.get_broadcast_stop_type(flow.priv)){}; + inline ~OBSBroadcastFlow() {} + + const obs_service_t *GetBondedService() { return boundedService; } + + void ManageBroadcast(bool streamingActive); + + bool AllowManagingWhileStreaming(); + + obs_broadcast_state BroadcastState(); + obs_broadcast_start BroadcastStartType(); + obs_broadcast_stop BroadcastStopType(); + + bool DifferedStartBroadcast(); + obs_broadcast_stream_state IsBroadcastStreamActive(); + bool DifferedStopBroadcast(); + + void StopStreaming(); + + std::string GetLastError(); +}; diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index bcf5c14329fb37..3cd183a92fb213 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -121,6 +121,8 @@ target_sources( api-interface.cpp auth-base.cpp auth-base.hpp + broadcast-flow.cpp + broadcast-flow.hpp display-helpers.hpp platform.hpp qt-display.cpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 13b5565bf96d3a..7dc0876770d3ae 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -415,6 +415,7 @@ Output.NoBroadcast.Title="No Broadcast Configured" Output.NoBroadcast.Text="You need to set up a broadcast before you can start streaming." Output.BroadcastStartFailed="Failed to start broadcast" Output.BroadcastStopFailed="Failed to stop broadcast" +Output.BroadcastUnknownError="An error has occured" # log upload dialog text and messages LogReturnDialog="Log Upload Successful" @@ -710,6 +711,9 @@ Basic.Main.SetupBroadcast="Manage Broadcast" Basic.Main.StopStreaming="Stop Streaming" Basic.Main.StopBroadcast="End Broadcast" Basic.Main.AutoStopEnabled="(Auto Stop)" +Basic.Main.BroadcastManualStartWarning.Title="Manual start required" +Basic.Main.BroadcastManualStartWarning="Auto-start is disabled for this event, click \"Go Live\" to start your broadcast." +Basic.Main.BroadcastEndWarning="You will not be able to reconnect.
Your stream will stop and you will no longer be live." Basic.Main.StoppingStreaming="Stopping Stream..." Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" Basic.Main.ShowContextBar="Show Source Toolbar" diff --git a/UI/forms/OBSBasic.ui b/UI/forms/OBSBasic.ui index 30d3dd69f82b59..716da7ad586af2 100644 --- a/UI/forms/OBSBasic.ui +++ b/UI/forms/OBSBasic.ui @@ -1516,11 +1516,14 @@
- Basic.Main.StartBroadcast + Basic.Main.SetupBroadcast true + + false +
diff --git a/UI/obs-frontend-api/obs-frontend-api.cpp b/UI/obs-frontend-api/obs-frontend-api.cpp index 6a22de85b54fd3..a4d5d23025d076 100644 --- a/UI/obs-frontend-api/obs-frontend-api.cpp +++ b/UI/obs-frontend-api/obs-frontend-api.cpp @@ -672,3 +672,86 @@ void obs_frontend_add_undo_redo_action(const char *name, c->obs_frontend_add_undo_redo_action( name, undo, redo, undo_data, redo_data, repeatable); } + +void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, size_t size) +{ + if (!callbacks_valid()) + return; + + if (obs_frontend_streaming_active()) { + blog(LOG_ERROR, "obs_frontend_broadcast_flow can not be added " + "while streaming is active"); + return; + } + +#define CHECK_REQUIRED_VAL(val) \ + do { \ + if ((offsetof(struct obs_frontend_broadcast_flow, val) + \ + sizeof(flow->val) > \ + size) || \ + !flow->val) { \ + blog(LOG_ERROR, \ + "Required value '" #val "' not found." \ + " obs_frontend_add_broadcast_flow failed."); \ + return; \ + } \ + } while (false) + + CHECK_REQUIRED_VAL(priv); + CHECK_REQUIRED_VAL(get_broadcast_state); + CHECK_REQUIRED_VAL(get_broadcast_start_type); + CHECK_REQUIRED_VAL(get_broadcast_stop_type); + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) != + 0) { + CHECK_REQUIRED_VAL(manage_broadcast2); + } else { + CHECK_REQUIRED_VAL(manage_broadcast); + } + + CHECK_REQUIRED_VAL(stopped_streaming); + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) != + 0) { + CHECK_REQUIRED_VAL(differed_start_broadcast); + CHECK_REQUIRED_VAL(is_broadcast_stream_active); + CHECK_REQUIRED_VAL(get_last_error); + } + + if ((flow->flags & OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) != + 0) { + CHECK_REQUIRED_VAL(differed_stop_broadcast); + CHECK_REQUIRED_VAL(get_last_error); + } +#undef CHECK_REQUIRED_VAL + + struct obs_frontend_broadcast_flow zeroed = {0}; + if (size > sizeof(zeroed)) { + blog(LOG_ERROR, + "Tried to add obs_frontend_broadcast_flow with size " + "%llu which is more than obs-frontend-api currently " + "supports (%llu)", + (long long unsigned)size, + (long long unsigned)sizeof(zeroed)); + return; + } + + c->obs_frontend_add_broadcast_flow_s(service, flow, size); +} + +void obs_frontend_remove_broadcast_flow(const obs_service_t *service) +{ + if (!callbacks_valid()) + return; + + if (obs_frontend_streaming_active()) { + blog(LOG_ERROR, + "obs_frontend_broadcast_flow can not be removed " + "while streaming is active"); + return; + } + + c->obs_frontend_remove_broadcast_flow(service); +} diff --git a/UI/obs-frontend-api/obs-frontend-api.h b/UI/obs-frontend-api/obs-frontend-api.h index 0ca07ff5d076e8..382d0709a10b78 100644 --- a/UI/obs-frontend-api/obs-frontend-api.h +++ b/UI/obs-frontend-api/obs-frontend-api.h @@ -89,6 +89,59 @@ struct obs_frontend_browser_params { const char **popup_whitelist_urls; bool enable_cookie; }; + +enum obs_broadcast_flow_flag { + OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING = (1 << 0), + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START = (1 << 1), + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP = (1 << 2), +}; + +enum obs_broadcast_state { + OBS_BROADCAST_NONE = 0, + OBS_BROADCAST_ACTIVE, + OBS_BROADCAST_INACTIVE, +}; + +enum obs_broadcast_start { + OBS_BROADCAST_START_WITH_STREAM = 0, + OBS_BROADCAST_START_WITH_STREAM_NOW, + OBS_BROADCAST_START_DIFFER_FROM_STREAM, +}; + +enum obs_broadcast_stop { + OBS_BROADCAST_STOP_NEVER = 0, + OBS_BROADCAST_STOP_WITH_STREAM, + OBS_BROADCAST_STOP_DIFFER_FROM_STREAM, +}; + +enum obs_broadcast_stream_state { + OBS_BROADCAST_STREAM_FAILURE = 0, + OBS_BROADCAST_STREAM_INACTIVE, + OBS_BROADCAST_STREAM_ACTIVE, +}; + +struct obs_frontend_broadcast_flow { + void *priv; + + uint32_t flags; + + enum obs_broadcast_state (*get_broadcast_state)(void *priv); + enum obs_broadcast_start (*get_broadcast_start_type)(void *priv); + enum obs_broadcast_stop (*get_broadcast_stop_type)(void *priv); + + void (*manage_broadcast)(void *priv); + void (*manage_broadcast2)(void *priv, bool streaming_active); + + void (*stopped_streaming)(void *priv); + + void (*differed_start_broadcast)(void *priv); + enum obs_broadcast_stream_state (*is_broadcast_stream_active)( + void *priv); + + bool (*differed_stop_broadcast)(void *priv); + + const char *(*get_last_error)(void *priv); +}; #endif //!SWIG /* ------------------------------------------------------------------------- */ @@ -197,6 +250,14 @@ EXPORT void obs_frontend_push_ui_translation(obs_frontend_translate_ui_cb translate); EXPORT void obs_frontend_pop_ui_translation(void); +EXPORT void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, size_t size); +#define obs_frontend_add_broadcast_flow(service, flow) \ + obs_frontend_add_broadcast_flow_s( \ + service, flow, sizeof(struct obs_frontend_broadcast_flow)) +EXPORT void obs_frontend_remove_broadcast_flow(const obs_service_t *service); + #endif //!SWIG EXPORT void obs_frontend_streaming_start(void); diff --git a/UI/obs-frontend-api/obs-frontend-internal.hpp b/UI/obs-frontend-api/obs-frontend-internal.hpp index dfb82dac0ea4d0..186cf305ba8893 100644 --- a/UI/obs-frontend-api/obs-frontend-internal.hpp +++ b/UI/obs-frontend-api/obs-frontend-internal.hpp @@ -175,6 +175,13 @@ struct obs_frontend_callbacks { const char *undo_data, const char *redo_data, bool repeatable) = 0; + + virtual void obs_frontend_add_broadcast_flow_s( + const obs_service_t *service, + const struct obs_frontend_broadcast_flow *flow, + size_t size) = 0; + virtual void + obs_frontend_remove_broadcast_flow(const obs_service_t *service) = 0; }; EXPORT void diff --git a/UI/window-basic-main-service.cpp b/UI/window-basic-main-service.cpp index 27e37469b6e8f6..bd6e75b47b7d3a 100644 --- a/UI/window-basic-main-service.cpp +++ b/UI/window-basic-main-service.cpp @@ -117,7 +117,81 @@ obs_service_t *OBSBasic::GetService() void OBSBasic::SetService(obs_service_t *newService) { - if (newService) { - service = newService; + if (!newService) + return; + + ResetServiceBroadcastFlow(); + + service = newService; + + LoadServiceBroadcastFlow(); +} + +void OBSBasic::ResetServiceBroadcastFlow() +{ + if (!serviceBroadcastFlow) + return; + + serviceBroadcastFlow = nullptr; + + ui->broadcastButton->setVisible(false); + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); +} + +void OBSBasic::LoadServiceBroadcastFlow() +{ + if (serviceBroadcastFlow) + return; + + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service) + continue; + + serviceBroadcastFlow = &broadcastFlows[i]; + ui->broadcastButton->setVisible(true); + + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != + OBS_BROADCAST_NONE); + + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::ManageBroadcastButtonClicked); + break; } } + +bool OBSBasic::AddBroadcastFlow(const obs_service_t *service_, + const obs_frontend_broadcast_flow &flow) +{ + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service_) + continue; + + blog(LOG_WARNING, + "This service already has an broadcast flow added"); + return false; + } + + broadcastFlows.append(OBSBroadcastFlow(service_, flow)); + + LoadServiceBroadcastFlow(); + + return true; +} + +void OBSBasic::RemoveBroadcastFlow(const obs_service_t *service_) +{ + for (int64_t i = 0; i < broadcastFlows.size(); i++) { + if (broadcastFlows[i].GetBondedService() != service_) + continue; + + broadcastFlows.removeAt(i); + + if (GetService() == service_) + ResetServiceBroadcastFlow(); + + return; + } + + blog(LOG_WARNING, "This service has no broadcast flow added to it"); +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 719ff5f7eb0b4e..cdc5b484b4de03 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -297,7 +297,6 @@ OBSBasic::OBSBasic(QWidget *parent) QStyle *contextBarStyle = new OBSContextBarProxyStyle(); contextBarStyle->setParent(ui->contextContainer); ui->contextContainer->setStyle(contextBarStyle); - ui->broadcastButton->setVisible(false); startingDockLayout = saveState(); @@ -493,9 +492,6 @@ OBSBasic::OBSBasic(QWidget *parent) connect(ui->scenes, SIGNAL(scenesReordered()), this, SLOT(ScenesReordered())); - connect(ui->broadcastButton, &QPushButton::clicked, this, - &OBSBasic::BroadcastButtonClicked); - connect(App(), &OBSApp::StyleChanged, this, &OBSBasic::ThemeChanged); QActionGroup *actionGroup = new QActionGroup(this); @@ -4881,14 +4877,12 @@ void OBSBasic::closeEvent(QCloseEvent *event) return; } -#ifdef YOUTUBE_ENABLED - /* Also don't close the window if the youtube stream check is active */ - if (youtubeStreamCheckThread) { + /* Also don't close the window if the broadcast stream check is active */ + if (broadcastStreamCheckThread) { QTimer::singleShot(1000, this, SLOT(close())); event->ignore(); return; } -#endif if (isVisible()) config_set_string(App()->GlobalConfig(), "BasicWindow", @@ -6652,39 +6646,13 @@ void OBSBasic::DisplayStreamStartError() QMessageBox::critical(this, QTStr("Output.StartStreamFailed"), message); } -#ifdef YOUTUBE_ENABLED -void OBSBasic::YouTubeActionDialogOk(const QString &id, const QString &key, - bool autostart, bool autostop, - bool start_now) -{ - //blog(LOG_DEBUG, "Stream key: %s", QT_TO_UTF8(key)); - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = obs_service_get_settings(service_obj); - - const std::string a_key = QT_TO_UTF8(key); - obs_data_set_string(settings, "key", a_key.c_str()); - - const std::string an_id = QT_TO_UTF8(id); - obs_data_set_string(settings, "stream_id", an_id.c_str()); - - obs_service_update(service_obj, settings); - autoStartBroadcast = autostart; - autoStopBroadcast = autostop; - broadcastReady = true; - - if (start_now) - QMetaObject::invokeMethod(this, "StartStreaming"); -} - -void OBSBasic::YoutubeStreamCheck(const std::string &key) +void OBSBasic::BroadcastStreamCheck() { - YoutubeApiWrappers *apiYouTube( - dynamic_cast(GetAuth())); - if (!apiYouTube) { + if (!serviceBroadcastFlow) { /* technically we should never get here -Lain */ QMetaObject::invokeMethod(this, "ForceStopStreaming", Qt::QueuedConnection); - youtubeStreamCheckThread->deleteLater(); + broadcastStreamCheckThread->deleteLater(); blog(LOG_ERROR, "=========================================="); blog(LOG_ERROR, "%s: Uh, hey, we got here", __FUNCTION__); blog(LOG_ERROR, "=========================================="); @@ -6692,9 +6660,6 @@ void OBSBasic::YoutubeStreamCheck(const std::string &key) } int timeout = 0; - json11::Json json; - QString id = key.c_str(); - while (StreamingActive()) { if (timeout == 14) { QMetaObject::invokeMethod(this, "ForceStopStreaming", @@ -6702,62 +6667,34 @@ void OBSBasic::YoutubeStreamCheck(const std::string &key) break; } - if (!apiYouTube->FindStream(id, json)) { + obs_broadcast_stream_state state = + serviceBroadcastFlow->IsBroadcastStreamActive(); + + switch (state) { + case OBS_BROADCAST_STREAM_FAILURE: QMetaObject::invokeMethod(this, "DisplayStreamStartError", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "StopStreaming", Qt::QueuedConnection); break; - } - - auto item = json["items"][0]; - auto status = item["status"]["streamStatus"].string_value(); - if (status == "active") { + case OBS_BROADCAST_STREAM_INACTIVE: + QThread::sleep(1); + timeout++; + break; + case OBS_BROADCAST_STREAM_ACTIVE: QMetaObject::invokeMethod(ui->broadcastButton, "setEnabled", Q_ARG(bool, true)); break; - } else { - QThread::sleep(1); - timeout++; - } - } - - youtubeStreamCheckThread->deleteLater(); -} - -void OBSBasic::ShowYouTubeAutoStartWarning() -{ - auto msgBox = []() { - QMessageBox msgbox(App()->GetMainWindow()); - msgbox.setWindowTitle(QTStr( - "YouTube.Actions.AutoStartStreamingWarning.Title")); - msgbox.setText( - QTStr("YouTube.Actions.AutoStartStreamingWarning")); - msgbox.setIcon(QMessageBox::Icon::Information); - msgbox.addButton(QMessageBox::Ok); - - QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); - msgbox.setCheckBox(cb); - - msgbox.exec(); - - if (cb->isChecked()) { - config_set_bool(App()->GlobalConfig(), "General", - "WarnedAboutYouTubeAutoStart", true); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); } - }; - bool warned = config_get_bool(App()->GlobalConfig(), "General", - "WarnedAboutYouTubeAutoStart"); - if (!warned) { - QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, - Q_ARG(VoidFunc, msgBox)); + if (state != OBS_BROADCAST_STREAM_INACTIVE) + break; } + + broadcastStreamCheckThread->deleteLater(); } -#endif void OBSBasic::StartStreaming() { @@ -6766,8 +6703,9 @@ void OBSBasic::StartStreaming() if (disableOutputsRef) return; - if (auth && auth->broadcastFlow()) { - if (!broadcastActive && !broadcastReady) { + if (serviceBroadcastFlow) { + if (serviceBroadcastFlow->BroadcastState() == + OBS_BROADCAST_NONE) { ui->streamButton->setChecked(false); QMessageBox no_broadcast(this); @@ -6784,8 +6722,8 @@ void OBSBasic::StartStreaming() no_broadcast.exec(); if (no_broadcast.clickedButton() == SetupBroadcast) - QMetaObject::invokeMethod(this, - "SetupBroadcast"); + QMetaObject::invokeMethod( + this, "ManageBroadcastButtonClicked"); return; } } @@ -6803,7 +6741,6 @@ void OBSBasic::StartStreaming() ui->streamButton->setEnabled(false); ui->streamButton->setChecked(false); ui->streamButton->setText(QTStr("Basic.Main.Connecting")); - ui->broadcastButton->setChecked(false); if (sysTrayStream) { sysTrayStream->setEnabled(false); @@ -6815,27 +6752,21 @@ void OBSBasic::StartStreaming() return; } - if (!autoStartBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StartBroadcast")); - ui->broadcastButton->setProperty("broadcastState", "ready"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - // well, we need to disable button while stream is not active - ui->broadcastButton->setEnabled(false); - } else { - if (!autoStopBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StopBroadcast")); - } else { + if (serviceBroadcastFlow) { + ui->broadcastButton->setChecked(false); + + if (serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); ui->broadcastButton->setText( - QTStr("Basic.Main.AutoStopEnabled")); + QTStr("Basic.Main.StartBroadcast")); + connect(ui->broadcastButton, &QPushButton::clicked, + this, &OBSBasic::StartBroadcastButtonClicked); + // well, we need to disable button while stream is not active ui->broadcastButton->setEnabled(false); + } else { + BroadcastStarted(); } - ui->broadcastButton->setProperty("broadcastState", "active"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - broadcastActive = true; } bool recordWhenStreaming = config_get_bool( @@ -6848,135 +6779,148 @@ void OBSBasic::StartStreaming() if (replayBufferWhileStreaming) StartReplayBuffer(); -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) - OBSBasic::ShowYouTubeAutoStartWarning(); -#endif + if (!serviceBroadcastFlow || + serviceBroadcastFlow->BroadcastStartType() != + OBS_BROADCAST_START_DIFFER_FROM_STREAM) + return; + + auto msgBox = []() { + QMessageBox msgbox(App()->GetMainWindow()); + msgbox.setWindowTitle( + QTStr("Basic.Main.BroadcastManualStartWarning.Title")); + msgbox.setText(QTStr("Basic.Main.BroadcastManualStartWarning")); + msgbox.setIcon(QMessageBox::Icon::Information); + msgbox.addButton(QMessageBox::Ok); + + QCheckBox *cb = new QCheckBox(QTStr("DoNotShowAgain")); + msgbox.setCheckBox(cb); + + msgbox.exec(); + + if (cb->isChecked()) { + config_set_bool(App()->GlobalConfig(), "General", + "WarnedAboutYouTubeAutoStart", true); + config_save_safe(App()->GlobalConfig(), "tmp", nullptr); + } + }; + + bool warned = config_get_bool(App()->GlobalConfig(), "General", + "WarnedAboutYouTubeAutoStart"); + if (!warned) { + QMetaObject::invokeMethod(App(), "Exec", Qt::QueuedConnection, + Q_ARG(VoidFunc, msgBox)); + } } -void OBSBasic::BroadcastButtonClicked() +void OBSBasic::ManageBroadcastButtonClicked() { - if (!broadcastReady || - (!broadcastActive && !outputHandler->StreamingActive())) { - SetupBroadcast(); - if (broadcastReady) - ui->broadcastButton->setChecked(true); + bool streamingActive = outputHandler->StreamingActive(); + + serviceBroadcastFlow->ManageBroadcast(streamingActive); + + if (streamingActive) return; - } - if (!autoStartBroadcast) { -#ifdef YOUTUBE_ENABLED - std::shared_ptr ytAuth = - dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StartLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr( - "YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, - ytAuth->GetBroadcastId()); - - OBSMessageBox::warning( - this, - QTStr("Output.BroadcastStartFailed"), - last_error, true); - ui->broadcastButton->setChecked(false); - return; - } - } -#endif - broadcastActive = true; - autoStartBroadcast = true; // and clear the flag + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_NONE); - if (!autoStopBroadcast) { - ui->broadcastButton->setText( - QTStr("Basic.Main.StopBroadcast")); - } else { - ui->broadcastButton->setText( - QTStr("Basic.Main.AutoStopEnabled")); - ui->broadcastButton->setEnabled(false); - } + if (serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_WITH_STREAM_NOW) + QMetaObject::invokeMethod(this, "StartStreaming"); +} - ui->broadcastButton->setProperty("broadcastState", "active"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); - } else if (!autoStopBroadcast) { -#ifdef YOUTUBE_ENABLED - bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", - "WarnBeforeStoppingStream"); - if (confirm && isVisible()) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), - QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No); +void OBSBasic::StartBroadcastButtonClicked() +{ + if (!serviceBroadcastFlow->DifferedStartBroadcast()) { + QString error = QString::fromStdString( + serviceBroadcastFlow->GetLastError()); + OBSMessageBox::warning( + this, QTStr("Output.BroadcastStartFailed"), + error.isEmpty() ? QTStr("Output.BroadcastUnknownError") + : error, + true); + ui->broadcastButton->setChecked(false); + return; + } - if (button == QMessageBox::No) { - ui->broadcastButton->setChecked(true); - return; - } + BroadcastStarted(); +} + +void OBSBasic::BroadcastStarted() +{ + switch (serviceBroadcastFlow->BroadcastStopType()) { + case OBS_BROADCAST_STOP_DIFFER_FROM_STREAM: + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); + ui->broadcastButton->setText(QTStr("Basic.Main.StopBroadcast")); + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::StopBroadcastButtonClicked); + break; + case OBS_BROADCAST_STOP_WITH_STREAM: + if (serviceBroadcastFlow->AllowManagingWhileStreaming()) { + ResetBroadcastButtonState(); + break; + } + ui->broadcastButton->setText( + QTStr("Basic.Main.AutoStopEnabled")); + ui->broadcastButton->setEnabled(false); + break; + case OBS_BROADCAST_STOP_NEVER: + if (serviceBroadcastFlow->AllowManagingWhileStreaming()) { + ResetBroadcastButtonState(); + break; } + ui->broadcastButton->setText( + QTStr("Basic.Main.SetupBroadcast")); + ui->broadcastButton->setEnabled(false); + ui->broadcastButton->setChecked(true); + break; + } +} - std::shared_ptr ytAuth = - dynamic_pointer_cast(auth); - if (ytAuth.get()) { - if (!ytAuth->StopLatestBroadcast()) { - auto last_error = ytAuth->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr( - "YouTube.Actions.Error.YouTubeApi"); - if (!ytAuth->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.BroadcastTransitionFailed") - .arg(last_error, - ytAuth->GetBroadcastId()); - - OBSMessageBox::warning( - this, - QTStr("Output.BroadcastStopFailed"), - last_error, true); - } +void OBSBasic::StopBroadcastButtonClicked() +{ + bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", + "WarnBeforeStoppingStream"); + if (confirm && isVisible()) { + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("ConfirmStop.Title"), + QTStr("Basic.Main.BroadcastEndWarning"), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + if (button == QMessageBox::No) { + ui->broadcastButton->setChecked(true); + return; } -#endif - broadcastActive = false; - broadcastReady = false; + } - autoStopBroadcast = true; - QMetaObject::invokeMethod(this, "StopStreaming"); - SetBroadcastFlowEnabled(true); + if (!serviceBroadcastFlow->DifferedStopBroadcast()) { + QString error = QString::fromStdString( + serviceBroadcastFlow->GetLastError()); + OBSMessageBox::warning( + this, QTStr("Output.BroadcastStopFailed"), + error.isEmpty() ? QTStr("Output.BroadcastUnknownError") + : error, + true); } + + QMetaObject::invokeMethod(this, "StopStreaming"); + + ResetBroadcastButtonState(); } -void OBSBasic::SetBroadcastFlowEnabled(bool enabled) +void OBSBasic::ResetBroadcastButtonState() { - ui->broadcastButton->setEnabled(enabled); - ui->broadcastButton->setVisible(enabled); - ui->broadcastButton->setChecked(broadcastReady); - ui->broadcastButton->setProperty("broadcastState", "idle"); - ui->broadcastButton->style()->unpolish(ui->broadcastButton); - ui->broadcastButton->style()->polish(ui->broadcastButton); + ui->broadcastButton->disconnect(SIGNAL(clicked(bool))); + ui->broadcastButton->setText(QTStr("Basic.Main.SetupBroadcast")); -} + connect(ui->broadcastButton, &QPushButton::clicked, this, + &OBSBasic::ManageBroadcastButtonClicked); -void OBSBasic::SetupBroadcast() -{ -#ifdef YOUTUBE_ENABLED - Auth *const auth = GetAuth(); - if (IsYouTubeService(auth->service())) { - OBSYoutubeActions dialog(this, auth, broadcastReady); - connect(&dialog, &OBSYoutubeActions::ok, this, - &OBSBasic::YouTubeActionDialogOk); - int result = dialog.Valid() ? dialog.exec() : QDialog::Rejected; - if (result != QDialog::Accepted) { - if (!broadcastReady) - ui->broadcastButton->setChecked(false); - } - } -#endif + ui->broadcastButton->setEnabled(true); + ui->broadcastButton->setVisible(true); + + ui->broadcastButton->setChecked( + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_NONE); } #ifdef _WIN32 @@ -7089,19 +7033,8 @@ void OBSBasic::StopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(streamingStopping); - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } + if (serviceBroadcastFlow) + serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7129,19 +7062,8 @@ void OBSBasic::ForceStopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(true); - // special case: force reset broadcast state if - // no autostart and no autostop selected - if (!autoStartBroadcast && !broadcastActive) { - broadcastActive = false; - autoStartBroadcast = true; - autoStopBroadcast = true; - broadcastReady = false; - } - - if (autoStopBroadcast) { - broadcastActive = false; - broadcastReady = false; - } + if (serviceBroadcastFlow) + serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7227,22 +7149,16 @@ void OBSBasic::StreamingStart() sysTrayStream->setEnabled(true); } -#ifdef YOUTUBE_ENABLED - if (!autoStartBroadcast) { - // get a current stream key - obs_service_t *service_obj = GetService(); - OBSDataAutoRelease settings = - obs_service_get_settings(service_obj); - std::string key = obs_data_get_string(settings, "stream_id"); - if (!key.empty() && !youtubeStreamCheckThread) { - youtubeStreamCheckThread = CreateQThread( - [this, key] { YoutubeStreamCheck(key); }); - youtubeStreamCheckThread->setObjectName( - "YouTubeStreamCheckThread"); - youtubeStreamCheckThread->start(); - } + if (serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastStartType() == + OBS_BROADCAST_START_DIFFER_FROM_STREAM && + !broadcastStreamCheckThread) { + broadcastStreamCheckThread = + CreateQThread([this] { BroadcastStreamCheck(); }); + broadcastStreamCheckThread->setObjectName( + "BroadcastStreamCheckThread"); + broadcastStreamCheckThread->start(); } -#endif if (api) api->on_event(OBS_FRONTEND_EVENT_STREAMING_STARTED); @@ -7357,8 +7273,9 @@ void OBSBasic::StreamingStop(int code, QString last_error) } // Reset broadcast button state/text - if (!broadcastActive) - SetBroadcastFlowEnabled(auth && auth->broadcastFlow()); + if (serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_ACTIVE) + ResetBroadcastButtonState(); } void OBSBasic::AutoRemux(QString input, bool no_show) @@ -7879,23 +7796,27 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStoppingStream"); -#ifdef YOUTUBE_ENABLED - if (isVisible() && auth && IsYouTubeService(auth->service()) && - autoStopBroadcast) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmStop.Title"), - QTStr("YouTube.Actions.AutoStopStreamingWarning"), - QMessageBox::Yes | QMessageBox::No, - QMessageBox::No); + if (isVisible() && serviceBroadcastFlow) { + if (serviceBroadcastFlow->BroadcastStopType() == + OBS_BROADCAST_STOP_WITH_STREAM) { + QMessageBox::StandardButton button = + OBSMessageBox::question( + this, + QTStr("ConfirmStop.Title"), + QTStr("Basic.Main.BroadcastEndWarning"), + QMessageBox::Yes | + QMessageBox::No, + QMessageBox::No); + + if (button == QMessageBox::No) { + ui->streamButton->setChecked(true); + return; + } - if (button == QMessageBox::No) { - ui->streamButton->setChecked(true); - return; + confirm = false; } - - confirm = false; } -#endif + if (confirm && isVisible()) { QMessageBox::StandardButton button = OBSMessageBox::question( @@ -7941,11 +7862,11 @@ void OBSBasic::on_streamButton_clicked() bool bwtest = obs_service_bandwidth_test_enabled(service); - if (this->auth) { - // Disable confirmation if this is going to open broadcast setup - if (auth && auth->broadcastFlow() && !broadcastReady && - !broadcastActive) - confirm = false; + // Disable confirmation if this is going to open broadcast setup + if (serviceBroadcastFlow && + serviceBroadcastFlow->BroadcastState() == + OBS_BROADCAST_NONE) { + confirm = false; } if (bwtest && isVisible()) { diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index d86f8aa3a4908a..8dbb260e9f21a8 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -39,6 +39,7 @@ #include "auth-base.hpp" #include "log-viewer.hpp" #include "undo-stack-obs.hpp" +#include "broadcast-flow.hpp" #include @@ -622,20 +623,17 @@ class OBSBasic : public OBSMainWindow { void MoveSceneItem(enum obs_order_movement movement, const QString &action_name); - bool autoStartBroadcast = true; - bool autoStopBroadcast = true; - bool broadcastActive = false; - bool broadcastReady = false; - QPointer youtubeStreamCheckThread; -#ifdef YOUTUBE_ENABLED - void YoutubeStreamCheck(const std::string &key); - void ShowYouTubeAutoStartWarning(); - void YouTubeActionDialogOk(const QString &id, const QString &key, - bool autostart, bool autostop, - bool start_now); -#endif - void BroadcastButtonClicked(); - void SetBroadcastFlowEnabled(bool enabled); + OBSBroadcastFlow *serviceBroadcastFlow = nullptr; + QList broadcastFlows; + QPointer broadcastStreamCheckThread; + + void ResetServiceBroadcastFlow(); + void LoadServiceBroadcastFlow(); + + void BroadcastStarted(); + void ResetBroadcastButtonState(); + + void BroadcastStreamCheck(); void UpdatePreviewSafeAreas(); bool drawSafeAreas = false; @@ -671,8 +669,6 @@ public slots: void DisplayStreamStartError(); - void SetupBroadcast(); - void StartStreaming(); void StopStreaming(); void ForceStopStreaming(); @@ -842,6 +838,10 @@ private slots: void RestartVirtualCam(const VCamConfig &config); void RestartingVirtualCam(); + void ManageBroadcastButtonClicked(); + void StartBroadcastButtonClicked(); + void StopBroadcastButtonClicked(); + private: /* OBS Callbacks */ static void SceneReordered(void *data, calldata_t *params); @@ -895,6 +895,10 @@ private slots: obs_service_t *GetService(); void SetService(obs_service_t *service); + bool AddBroadcastFlow(const obs_service_t *service, + const obs_frontend_broadcast_flow &flow); + void RemoveBroadcastFlow(const obs_service_t *service); + int GetTransitionDuration(); int GetTbarPosition(); diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index a67aade738584a..d1b50bfb7837e8 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -33,6 +33,7 @@ Welcome to OBS Studio's documentation! Graphics (libobs/graphics) Media I/O (libobs/media-io) reference-frontend-api + reference-frontend-broadcast-flow-api .. toctree:: :caption: Additional Resources diff --git a/docs/sphinx/reference-frontend-broadcast-flow-api.rst b/docs/sphinx/reference-frontend-broadcast-flow-api.rst new file mode 100644 index 00000000000000..713477efcbb606 --- /dev/null +++ b/docs/sphinx/reference-frontend-broadcast-flow-api.rst @@ -0,0 +1,219 @@ +OBS Studio Frontend Broadcast Flow API +====================================== + +The OBS Studio frontend broadcast flow API is part of the frontend API which +can enable a deeper integration for streaming services plugin. + +Services usually does not separate the concept of broadcast and stream, but +some does. In this case features related to differed start/stop might need to be +enabled. + +.. code:: cpp + + #include + + +Structures/Enumerations +----------------------- + +.. struct:: obs_frontend_broadcast_flow + + Broadcast Flow definition structure. + + .. member:: uint32_t obs_frontend_broadcast_flow.flags + + Broadcast Flow feature flags. + + A bitwise OR combination of one or more of the following values: + + - **OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING** - Allow broadcast + management while streaming. + + - **OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START** - Broadcast can be + started after the stream. + + - **OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP** - Broadcast can be + stopped after the stream. + + .. member:: obs_broadcast_state (*obs_frontend_broadcast_flow.get_broadcast_state)(void *priv) + + Return the state of the broadcast. Called when the flow is added, after all + other callbacks except manage while streaming, stream state and last error. + + (Required) + + :param priv: The priv variable of this structure + :return: The state of the broadcast + + .. member:: obs_broadcast_start (*obs_frontend_broadcast_flow.get_broadcast_start_type)(void *priv) + + Return the start type of the broadcast. Called when the flow is added, after + manage (while not streaming), stopped streaming and differed stop (if + OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) callbacks. + + (Required) + + :param priv: The priv variable of this structure + :return: The start type of the broadcast + + .. member:: obs_broadcast_stop (*obs_frontend_broadcast_flow.get_broadcast_stop_type)(void *priv) + + Return the stop type of the broadcast. Called when the flow is added, after + manage (while not streaming), stopped streaming and differed stop (if + OBS_BROADCAST_STOP_DIFFER_FROM_STREAM) callbacks. + + (Required) + + :param priv: The priv variable of this structure + :return: The stop type of the broadcast + + .. member:: void (*obs_frontend_broadcast_flow.manage_broadcast)(void *priv) + + Show the broadcast manager of the service. + + (Require if not OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) + + :param priv: The priv variable of this structure + + .. member:: void (*obs_frontend_broadcast_flow.manage_broadcast2)(void *priv, bool streaming_active) + + Show the broadcast manager of the service. + + (Require if OBS_BROADCAST_FLOW_ALLOW_MANAGE_WHILE_STREAMING) + + :param priv: The priv variable of this structure + + .. member:: void (*obs_frontend_broadcast_flow.stopped_streaming)(void *priv) + + Indicate to the broadcast flow that the stream is terminated. + + It allow to update the state of the broadcast before being requested by the UI. + + (Required) + + :param priv: The priv variable of this structure + + .. member:: void (*obs_frontend_broadcast_flow.differed_start_broadcast)(void *priv) + + Make the broadcast start and must change the state to active if it succeed. + + A thread with calling the following member will be started to + check if the stream is active. + + (Required if OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) + + :param priv: The priv variable of this structure + + .. member:: enum obs_broadcast_stream_state (*obs_frontend_broadcast_flow.is_broadcast_stream_active)(void *priv) + + This member will be called in thread to check if the stream has started. + + The thread will timeout if too many inactive state are returned. + + (Required if OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START) + + :param priv: The priv variable of this structure + :return: The state of the broadcast stream + + .. member:: bool (*obs_frontend_broadcast_flow.differed_stop_broadcast)(void *priv) + + Make the broadcast stop. + + (Required if OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) + + :param priv: The priv variable of this structure + :return: If the broadcast was successfully ended + + .. member:: const char *(*obs_frontend_broadcast_flow.get_last_error)(void *priv) + + If differed start/stop failed, this member can return the error. + + (Required if OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START or + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP) + + :param priv: The priv variable of this structure + +--------------------------------------- + +.. enum:: obs_broadcast_state + + - **OBS_BROADCAST_NONE** + + No broadcast is setup. + + - **OBS_BROADCAST_ACTIVE** + + The broadcast is ready and active. + + - **OBS_BROADCAST_INACTIVE** + + The broadcast is ready and awaits a differed start. + Ignored if not OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START. + +.. enum:: obs_broadcast_start + + - **OBS_BROADCAST_START_WITH_STREAM** + + The broadcast is already started or will start with the stream. + + - **OBS_BROADCAST_START_WITH_STREAM_NOW** + + Same as the previous, but the streaming will also be started. + + - **OBS_BROADCAST_START_DIFFER_FROM_STREAM** + + The broadcast requires to be started after the stream. + Ignored if not OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START. + +--------------------------------------- + +.. enum:: obs_broadcast_stop + + - **OBS_BROADCAST_STOP_NEVER** + + The broadcast is not stopped after the stream ended. + + - **OBS_BROADCAST_STOP_WITH_STREAM** + + The broadcast is ended at the time as the stream. + + - **OBS_BROADCAST_STOP_DIFFER_FROM_STREAM** + + The broadcast requires to be stopped separetly from the stream. + Ignored if not OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP. + +--------------------------------------- + +.. enum:: obs_broadcast_stream_state + + - **OBS_BROADCAST_STREAM_FAILURE** + + The broadcast stream could not be activated. + + - **OBS_BROADCAST_STREAM_INACTIVE** + + The broadcast stream is inactive. + + - **OBS_BROADCAST_STREAM_ACTIVE** + + The broadcast stream is active. + +--------------------------------------- + +Functions +--------- + +.. function:: void obs_frontend_add_broadcast_flow(const obs_service_t *service, const struct obs_frontend_broadcast_flow *flow) + + Add a Broadcast Flow bounded to the given service. + + :param service: The service bounded the the flow + :param flow: The flow itself + +--------------------------------------- + +.. function:: void obs_frontend_remove_broadcast_flow(const obs_service_t *service) + + Remove the Broadcast Flow bounded to the given service. + + :param service: The service bounded the the flow From 79f2381b57737d68d3f68d7e30818cf2ae738ca2 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 22 Jun 2023 14:09:57 +0200 Subject: [PATCH 36/65] obs-youtube: Add account integration --- plugins/obs-youtube/CMakeLists.txt | 2 + plugins/obs-youtube/clickable-label.hpp | 1 + plugins/obs-youtube/cmake/feature-oauth.cmake | 65 + plugins/obs-youtube/data/locale/en-US.ini | 109 ++ plugins/obs-youtube/lineedit-autoresize.cpp | 1 + plugins/obs-youtube/lineedit-autoresize.hpp | 1 + .../obs-youtube/window-youtube-actions.cpp | 805 ++++++----- .../obs-youtube/window-youtube-actions.hpp | 38 +- plugins/obs-youtube/youtube-api.hpp | 923 +++++++++++++ plugins/obs-youtube/youtube-chat.cpp | 182 +++ plugins/obs-youtube/youtube-chat.hpp | 39 + plugins/obs-youtube/youtube-config.cpp | 213 ++- plugins/obs-youtube/youtube-config.hpp | 14 +- plugins/obs-youtube/youtube-oauth.cpp | 1180 +++++++++++++++++ plugins/obs-youtube/youtube-oauth.hpp | 249 ++++ plugins/obs-youtube/youtube-service.cpp | 121 ++ plugins/obs-youtube/youtube-service.hpp | 27 +- 17 files changed, 3584 insertions(+), 386 deletions(-) create mode 120000 plugins/obs-youtube/clickable-label.hpp create mode 100644 plugins/obs-youtube/cmake/feature-oauth.cmake create mode 120000 plugins/obs-youtube/lineedit-autoresize.cpp create mode 120000 plugins/obs-youtube/lineedit-autoresize.hpp create mode 100644 plugins/obs-youtube/youtube-api.hpp create mode 100644 plugins/obs-youtube/youtube-chat.cpp create mode 100644 plugins/obs-youtube/youtube-chat.hpp create mode 100644 plugins/obs-youtube/youtube-oauth.cpp create mode 100644 plugins/obs-youtube/youtube-oauth.hpp diff --git a/plugins/obs-youtube/CMakeLists.txt b/plugins/obs-youtube/CMakeLists.txt index 75e2610d06ddae..50b6e88430a645 100644 --- a/plugins/obs-youtube/CMakeLists.txt +++ b/plugins/obs-youtube/CMakeLists.txt @@ -11,6 +11,8 @@ target_sources( target_link_libraries(obs-youtube PRIVATE OBS::libobs) +include(cmake/feature-oauth.cmake) + if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-youtube.rc) target_sources(obs-youtube PRIVATE obs-youtube.rc) diff --git a/plugins/obs-youtube/clickable-label.hpp b/plugins/obs-youtube/clickable-label.hpp new file mode 120000 index 00000000000000..c486b9cf70dd0c --- /dev/null +++ b/plugins/obs-youtube/clickable-label.hpp @@ -0,0 +1 @@ +../../UI/clickable-label.hpp \ No newline at end of file diff --git a/plugins/obs-youtube/cmake/feature-oauth.cmake b/plugins/obs-youtube/cmake/feature-oauth.cmake new file mode 100644 index 00000000000000..774e2ccd4f020b --- /dev/null +++ b/plugins/obs-youtube/cmake/feature-oauth.cmake @@ -0,0 +1,65 @@ +if(NOT DEFINED YOUTUBE_CLIENTID + OR "${YOUTUBE_CLIENTID}" STREQUAL "" + OR NOT DEFINED YOUTUBE_SECRET + OR "${YOUTUBE_SECRET}" STREQUAL "" + OR NOT DEFINED YOUTUBE_CLIENTID_HASH + OR "${YOUTUBE_CLIENTID_HASH}" STREQUAL "" + OR NOT DEFINED YOUTUBE_SECRET_HASH + OR "${YOUTUBE_SECRET_HASH}" STREQUAL "") + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable_feature(obs-youtube "YouTube OAuth connection") + endif() +else() + if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") + endif() + if(NOT TARGET OBS::oauth-local-redirect) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/local-redirect" + "${CMAKE_BINARY_DIR}/deps/oauth-service/local-redirect") + endif() + if(NOT TARGET OBS::oauth-service-base) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/service-base" + "${CMAKE_BINARY_DIR}/deps/oauth-service/service-base") + endif() + + find_package(CURL) + find_package(nlohmann_json) + find_qt(COMPONENTS Core Widgets) + + target_sources(obs-youtube PRIVATE forms/OBSYoutubeActions.ui) + + target_sources( + obs-youtube + PRIVATE # cmake-format: sortable + clickable-label.hpp + lineedit-autoresize.cpp + lineedit-autoresize.hpp + window-youtube-actions.cpp + window-youtube-actions.hpp + youtube-api.hpp + youtube-chat.cpp + youtube-chat.hpp + youtube-oauth.cpp + youtube-oauth.hpp) + + target_link_libraries(obs-youtube PRIVATE OBS::frontend-api OBS::obf OBS::oauth-service-base + OBS::oauth-local-redirect Qt::Core Qt::Widgets) + + target_compile_definitions( + obs-youtube + PRIVATE OAUTH_ENABLED YOUTUBE_CLIENTID="${YOUTUBE_CLIENTID}" YOUTUBE_CLIENTID_HASH=0x${YOUTUBE_CLIENTID_HASH} + YOUTUBE_SECRET="${YOUTUBE_SECRET}" YOUTUBE_SECRET_HASH=0x${YOUTUBE_SECRET_HASH}) + + set_target_properties( + obs-youtube + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON + AUTOUIC_SEARCH_PATHS forms) + + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_enable_feature(obs-youtube "YouTube OAuth connection") + else() + target_include_directories(obs-youtube PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) + endif() +endif() diff --git a/plugins/obs-youtube/data/locale/en-US.ini b/plugins/obs-youtube/data/locale/en-US.ini index 49a366e13d7e50..56bf27bf179e94 100644 --- a/plugins/obs-youtube/data/locale/en-US.ini +++ b/plugins/obs-youtube/data/locale/en-US.ini @@ -6,3 +6,112 @@ YouTube.Protocol="Protocol" YouTube.Server="Server" YouTube.StreamKey="Stream Key" YouTube.GetStreamKey="Get Stream Key" +YouTube.UseStreamKey="Use Stream Key" + +YouTube.Auth.Connect="Connect Account (recommended)" +YouTube.Auth.Disconnect="Disconnect Account" +YouTube.Auth.ConnectedAccount="Connected account" +YouTube.Auth.LockedProtocol="The protocol can not be changed while an account is connected" +YouTube.Auth.LoginDialog.Title="YouTube User Authorization" +YouTube.Auth.LoginDialog.Text="Please complete the authorization in your external browser.
If the external browser does not open, click the re-open button" +YouTube.Auth.LoginDialog.ReOpenURL="Re-open link" +YouTube.Auth.RedirectServer.Success="Authorization completed successfully.\nYou can now close this page." +YouTube.Auth.RedirectServer.Failure="The authorization process was not completed." +YouTube.Auth.LoginError.Title="Authentication Failure" +YouTube.Auth.LoginError.Text="Failed to authenticate:\n\n%1" +YouTube.Auth.LoginError.Text2="Failed to authenticate:\n\n%1: %2" +YouTube.Auth.SignOutDialog.Title="Disconnect Account?" +YouTube.Auth.SignOutDialog.Text="This change will apply immediately. Are you sure you want to disconnect your account?" +YouTube.Auth.ReLoginDialog.Title="YouTube Re-Login Required" +YouTube.Auth.ReLoginDialog.Text="%1 A re-login is required to keep the integration enabled. Proceed ?" +YouTube.Auth.ReLoginDialog.ScopeChange="The authentication requirements for YouTube have changed." +YouTube.Auth.ReLoginDialog.RefreshTokenFailed="The YouTube access token could not be refreshed." +YouTube.Auth.ReLoginDialog.ProfileDuplication="The YouTube service has been duplicated." + +YouTube.Chat.Dock="Chat" +YouTube.Chat.Input.Send="Send" +YouTube.Chat.Input.Placeholder="Enter message here..." +YouTube.Chat.Input.Sending="Sending..." +YouTube.Chat.Error.Title="Error while sending message" +YouTube.Chat.Error.Text="The message couldn't be sent: %1" + +YouTube.Actions.WindowTitle="YouTube Broadcast Setup - Channel: %1" +YouTube.Actions.CreateNewEvent="Create New Broadcast" +YouTube.Actions.ChooseEvent="Select Existing Broadcast" +YouTube.Actions.Title="Title*" +YouTube.Actions.MyBroadcast="My Broadcast" +YouTube.Actions.Description="Description" +YouTube.Actions.Privacy="Privacy*" +YouTube.Actions.Privacy.Private="Private" +YouTube.Actions.Privacy.Public="Public" +YouTube.Actions.Privacy.Unlisted="Unlisted" +YouTube.Actions.Category="Category" + +YouTube.Actions.Thumbnail="Thumbnail" +YouTube.Actions.Thumbnail.SelectFile="Select file..." +YouTube.Actions.Thumbnail.NoFileSelected="No file selected" +YouTube.Actions.Thumbnail.ClearFile="Clear" + +YouTube.Actions.MadeForKids="Is this video made for kids?*" +YouTube.Actions.MadeForKids.Yes="Yes, it's made for kids" +YouTube.Actions.MadeForKids.No="No, it's not made for kids" +YouTube.Actions.MadeForKids.Help="(?)" +YouTube.Actions.AdditionalSettings="Additional settings" +YouTube.Actions.Latency="Latency" +YouTube.Actions.Latency.Normal="Normal" +YouTube.Actions.Latency.Low="Low" +YouTube.Actions.Latency.UltraLow="Ultra low" +YouTube.Actions.EnableAutoStart="Enable Auto-start" +YouTube.Actions.EnableAutoStop="Enable Auto-stop" +YouTube.Actions.AutoStartStop.TT="Indicates whether this scheduled broadcast should start automatically" +YouTube.Actions.EnableDVR="Enable DVR" +YouTube.Actions.360Video="360 video" +YouTube.Actions.360Video.Help="(?)" +YouTube.Actions.ScheduleForLater="Schedule for later" +YouTube.Actions.RememberSettings="Remember these settings" + +YouTube.Actions.Create_Ready="Create broadcast" +YouTube.Actions.Create_GoLive="Create broadcast and start streaming" +YouTube.Actions.Choose_Ready="Select broadcast" +YouTube.Actions.Choose_GoLive="Select broadcast and start streaming" +YouTube.Actions.Create_Schedule="Schedule broadcast" +YouTube.Actions.Create_Schedule_Ready="Schedule and select broadcast" +YouTube.Actions.Dashboard="Open YouTube Studio" + +YouTube.Actions.Error.Title="Live broadcast creation error" +YouTube.Actions.Error.Text="YouTube access error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" +YouTube.Actions.Error.General="YouTube access error. Please check your network connection or your YouTube server access." +YouTube.Actions.Error.NoBroadcastCreated="Broadcast creation error '%1'.
A detailed error description can be found at https://developers.google.com/youtube/v3/live/docs/errors" +YouTube.Actions.Error.NoStreamCreated="No stream created. Please relink your account." +YouTube.Actions.Error.YouTubeApi="YouTube API Error. Please see the log file for more information." +YouTube.Actions.Error.BroadcastNotFound="The selected broadcast was not found." +YouTube.Actions.Error.FileMissing="Selected file does not exist." +YouTube.Actions.Error.FileOpeningFailed="Failed opening selected file." +YouTube.Actions.Error.FileTooLarge="Selected file is too large (Limit: 2 MiB)." +YouTube.Actions.Error.BroadcastTransitionFailed="Transitioning the broadcast failed: %1

If this error persists open the broadcast in YouTube Studio and try manually." +YouTube.Actions.Error.BroadcastTestStarting="Broadcast is transitioning to the test stage, this can take some time. Please try again in 10-30 seconds." + +YouTube.Actions.EventsLoading="Loading list of events..." +YouTube.Actions.EventCreated.Title="Event Created" +YouTube.Actions.EventCreated.Text="Event created successfully." + +YouTube.Actions.Stream="Stream" +YouTube.Actions.Stream.ScheduledFor="Scheduled for %1" +YouTube.Actions.Stream.Resume="Resume interrupted stream" +YouTube.Actions.Stream.YTStudio="Automatically created by YouTube Studio" + +YouTube.Actions.Notify.Title="YouTube" +YouTube.Actions.Notify.CreatingBroadcast="Creating a new Live Broadcast, please wait..." + +YouTube.Errors.NoChannels="No channel(s) available on selected account" +# YouTube API errors in format "YouTube.Errors." +YouTube.Errors.liveStreamingNotEnabled="Live streaming is not enabled on the selected YouTube channel.

See youtube.com/features for more information." +YouTube.Errors.livePermissionBlocked="Live streaming is unavailable on the selected YouTube Channel.
Please note that it may take up to 24 hours for live streaming to become available after enabling it in your channel settings.

See youtube.com/features for details." +YouTube.Errors.errorExecutingTransition="Transition failed due to a backend error. Please try again in a few seconds." +YouTube.Errors.errorStreamInactive="YouTube is not receiving data for your stream. Please check your configuration and try again." +YouTube.Errors.invalidTransition="The attempted transition was invalid. This may be due to the stream not having finished a previous transition. Please wait a few seconds and try again." +# Chat errors +YouTube.Errors.liveChatDisabled="Live chat is disabled on this stream." +YouTube.Errors.liveChatEnded="Live stream has ended." +YouTube.Errors.messageTextInvalid="The message text is not valid." +YouTube.Errors.rateLimitExceeded="You are sending messages too quickly." diff --git a/plugins/obs-youtube/lineedit-autoresize.cpp b/plugins/obs-youtube/lineedit-autoresize.cpp new file mode 120000 index 00000000000000..62ae685ede2d45 --- /dev/null +++ b/plugins/obs-youtube/lineedit-autoresize.cpp @@ -0,0 +1 @@ +../../UI/lineedit-autoresize.cpp \ No newline at end of file diff --git a/plugins/obs-youtube/lineedit-autoresize.hpp b/plugins/obs-youtube/lineedit-autoresize.hpp new file mode 120000 index 00000000000000..69ed432d224053 --- /dev/null +++ b/plugins/obs-youtube/lineedit-autoresize.hpp @@ -0,0 +1 @@ +../../UI/lineedit-autoresize.hpp \ No newline at end of file diff --git a/plugins/obs-youtube/window-youtube-actions.cpp b/plugins/obs-youtube/window-youtube-actions.cpp index 1f99ac02cf8b72..02089b67ae3ea5 100644 --- a/plugins/obs-youtube/window-youtube-actions.cpp +++ b/plugins/obs-youtube/window-youtube-actions.cpp @@ -1,9 +1,9 @@ -#include "window-basic-main.hpp" -#include "window-youtube-actions.hpp" +// SPDX-FileCopyrightText: 2021 Yuriy Chumak +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later -#include "obs-app.hpp" -#include "qt-wrappers.hpp" -#include "youtube-api-wrappers.hpp" +#include "window-youtube-actions.hpp" #include #include @@ -12,33 +12,58 @@ #include #include +#include +#include + const QString SchedulDateAndTimeFormat = "yyyy-MM-dd'T'hh:mm:ss'Z'"; const QString RepresentSchedulDateAndTimeFormat = "dddd, MMMM d, yyyy h:m"; -const QString IndexOfGamingCategory = "20"; -OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, +static QString GetTranslatedError(const RequestError &error) +{ + if (error.type != RequestErrorType::UNKNOWN_OR_CUSTOM || + error.error.empty()) + return QString(); + + std::string lookupString = "YouTube.Errors."; + lookupString += error.error; + + return QT_UTF8(obs_module_text(lookupString.c_str())); +} + +using namespace YouTubeApi; + +OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, + YouTubeApi::ServiceOAuth *auth, + OBSYoutubeActionsSettings *settings, bool broadcastReady) : QDialog(parent), ui(new Ui::OBSYoutubeActions), - apiYouTube(dynamic_cast(auth)), + apiYouTube(auth), workerThread(new WorkerThread(apiYouTube)), + settings(settings), broadcastReady(broadcastReady) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->setupUi(this); - - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Public"), - "public"); - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Unlisted"), - "unlisted"); - ui->privacyBox->addItem(QTStr("YouTube.Actions.Privacy.Private"), - "private"); - - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Normal"), - "normal"); - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.Low"), "low"); - ui->latencyBox->addItem(QTStr("YouTube.Actions.Latency.UltraLow"), - "ultraLow"); + ui->privacyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Privacy.Public")), + (int)LiveBroadcastPrivacyStatus::PUBLIC); + ui->privacyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Privacy.Unlisted")), + (int)LiveBroadcastPrivacyStatus::UNLISTED); + ui->privacyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Privacy.Private")), + (int)LiveBroadcastPrivacyStatus::PRIVATE); + + ui->latencyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Latency.Normal")), + (int)LiveBroadcastLatencyPreference::NORMAL); + ui->latencyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Latency.Low")), + (int)LiveBroadcastLatencyPreference::LOW); + ui->latencyBox->addItem( + QT_UTF8(obs_module_text("YouTube.Actions.Latency.UltraLow")), + (int)LiveBroadcastLatencyPreference::ULTRALOW); UpdateOkButtonStatus(); @@ -59,7 +84,8 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, [](const QString &) { QToolTip::showText( QCursor::pos(), - QTStr("YouTube.Actions.AutoStartStop.TT")); + obs_module_text( + "YouTube.Actions.AutoStartStop.TT")); }); connect(ui->help360Video, &QLabel::linkActivated, this, [](const QString &link) { QDesktopServices::openUrl(link); }); @@ -95,10 +121,11 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, ui->scheduledTime->setDateTime(QDateTime::currentDateTime()); auto thumbSelectionHandler = [&]() { - if (thumbnailFile.isEmpty()) { - QString filePath = OpenFile( + if (thumbnailFilePath.isEmpty()) { + QString filePath = QFileDialog::getOpenFileName( this, - QTStr("YouTube.Actions.Thumbnail.SelectFile"), + obs_module_text( + "YouTube.Actions.Thumbnail.SelectFile"), QStandardPaths::writableLocation( QStandardPaths::PicturesLocation), QString("Images (*.png *.jpg *.jpeg *.gif)")); @@ -108,16 +135,28 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, if (!tFile.exists()) { return ShowErrorDialog( this, - QTStr("YouTube.Actions.Error.FileMissing")); + obs_module_text( + "YouTube.Actions.Error.FileMissing")); } else if (tFile.size() > 2 * 1024 * 1024) { return ShowErrorDialog( this, - QTStr("YouTube.Actions.Error.FileTooLarge")); + obs_module_text( + "YouTube.Actions.Error.FileTooLarge")); + } + + QFile thumbFile(filePath); + if (!thumbFile.open(QFile::ReadOnly)) { + return ShowErrorDialog( + this, + obs_module_text( + "YouTube.Actions.Error.FileOpeningFailed")); } - thumbnailFile = filePath; - ui->selectedFileName->setText(thumbnailFile); - ui->selectFileButton->setText(QTStr( + thumbnailData = thumbFile.readAll(); + thumbnailFilePath = filePath; + ui->selectedFileName->setText( + thumbnailFilePath); + ui->selectFileButton->setText(obs_module_text( "YouTube.Actions.Thumbnail.ClearFile")); QImageReader imgReader(filePath); @@ -129,11 +168,12 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, Qt::SmoothTransformation)); } } else { - thumbnailFile.clear(); - ui->selectedFileName->setText(QTStr( + thumbnailData.clear(); + thumbnailFilePath.clear(); + ui->selectedFileName->setText(obs_module_text( "YouTube.Actions.Thumbnail.NoFileSelected")); - ui->selectFileButton->setText( - QTStr("YouTube.Actions.Thumbnail.SelectFile")); + ui->selectFileButton->setText(obs_module_text( + "YouTube.Actions.Thumbnail.SelectFile")); ui->thumbnailPreview->setPixmap( GetPlaceholder().pixmap(QSize(16, 16))); } @@ -150,27 +190,46 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, return; } - const char *name = config_get_string(OBSBasic::Get()->Config(), - "YouTube", "ChannelName"); - this->setWindowTitle(QTStr("YouTube.Actions.WindowTitle").arg(name)); + ChannelInfo info; + if (!apiYouTube->GetChannelInfo(info)) { + QString error = GetTranslatedError(apiYouTube->GetLastError()); + ShowErrorDialog( + parent, + error.isEmpty() + ? obs_module_text( + "YouTube.Actions.Error.General") + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.Text")) + .arg(error)); + Cancel(); + return; + }; + this->setWindowTitle( + QT_UTF8(obs_module_text("YouTube.Actions.WindowTitle")) + .arg(QString::fromStdString(info.title))); - QVector category_list; + std::vector category_list; if (!apiYouTube->GetVideoCategoriesList(category_list)) { + QString error = QString::fromStdString( + apiYouTube->GetLastError().message); ShowErrorDialog( parent, - apiYouTube->GetLastError().isEmpty() - ? QTStr("YouTube.Actions.Error.General") - : QTStr("YouTube.Actions.Error.Text") - .arg(apiYouTube->GetLastError())); + error.isEmpty() + ? obs_module_text( + "YouTube.Actions.Error.General") + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.Text")) + .arg(error)); Cancel(); return; } for (auto &category : category_list) { - ui->categoryBox->addItem(category.title, category.id); - if (category.id == IndexOfGamingCategory) { - ui->categoryBox->setCurrentText(category.title); - } + ui->categoryBox->addItem( + QString::fromStdString(category.snippet.title), + QString::fromStdString(category.id)); } + /* ID 20 is Gaming category */ + ui->categoryBox->setCurrentIndex(ui->categoryBox->findData("20")); connect(ui->okButton, &QPushButton::clicked, this, &OBSYoutubeActions::InitBroadcast); @@ -191,7 +250,7 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, loadingLabel->setAlignment(Qt::AlignHCenter); loadingLabel->setText( QString("%1") - .arg(QTStr("YouTube.Actions.EventsLoading"))); + .arg(obs_module_text("YouTube.Actions.EventsLoading"))); ui->scrollAreaWidgetContents->layout()->addWidget(loadingLabel); // Delete "loading..." label on completion @@ -202,31 +261,40 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, }); connect(workerThread, &WorkerThread::failed, this, [&]() { - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); + RequestError lastError = apiYouTube->GetLastError(); + + QString error = GetTranslatedError(lastError); - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = QTStr("YouTube.Actions.Error.Text") - .arg(last_error); + if (error.isEmpty()) + error = QString::fromStdString(lastError.message); - ShowErrorDialog(this, last_error); + ShowErrorDialog( + this, + error.isEmpty() + ? QT_UTF8(obs_module_text( + "YouTube.Actions.Error.YouTubeApi")) + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.Text")) + .arg(error)); QDialog::reject(); }); connect(workerThread, &WorkerThread::new_item, this, [&](const QString &title, const QString &dateTimeString, - const QString &broadcast, const QString &status, - bool astart, bool astop) { + const QString &broadcast, + const LiveBroadcastLifeCycleStatus &status, bool astart, + bool astop) { ClickableLabel *label = new ClickableLabel(); label->setTextFormat(Qt::RichText); - if (status == "live" || status == "testing") { + if (status == LiveBroadcastLifeCycleStatus::LIVE || + status == LiveBroadcastLifeCycleStatus::TESTING) { // Resumable stream label->setText( QString("%1
%2") .arg(title, - QTStr("YouTube.Actions.Stream.Resume"))); + obs_module_text( + "YouTube.Actions.Stream.Resume"))); } else if (dateTimeString.isEmpty()) { // The broadcast created by YouTube Studio has no start time. @@ -235,12 +303,14 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, label->setText( QString("%1
%2") .arg(title, - QTStr("YouTube.Actions.Stream.YTStudio"))); + obs_module_text( + "YouTube.Actions.Stream.YTStudio"))); } else { label->setText( QString("%1
%2") .arg(title, - QTStr("YouTube.Actions.Stream.ScheduledFor") + QT_UTF8(obs_module_text( + "YouTube.Actions.Stream.ScheduledFor")) .arg(dateTimeString))); } @@ -279,16 +349,14 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, }); workerThread->start(); - OBSBasic *main = OBSBasic::Get(); - bool rememberSettings = config_get_bool(main->basicConfig, "YouTube", - "RememberSettings"); - if (rememberSettings) + if (settings->rememberSettings) LoadSettings(); // Switch to events page and select readied broadcast once loaded if (broadcastReady) { ui->tabWidget->setCurrentIndex(1); - selectedBroadcast = apiYouTube->GetBroadcastId(); + selectedBroadcast = + QString::fromStdString(apiYouTube->GetBroadcastId()); } #ifdef __APPLE__ @@ -301,7 +369,7 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, void OBSYoutubeActions::showEvent(QShowEvent *event) { QDialog::showEvent(event); - if (thumbnailFile.isEmpty()) + if (thumbnailFilePath.isEmpty()) ui->thumbnailPreview->setPixmap( GetPlaceholder().pixmap(QSize(16, 16))); } @@ -318,91 +386,68 @@ void WorkerThread::run() { if (!pending) return; - json11::Json broadcasts; - for (QString broadcastStatus : {"active", "upcoming"}) { - if (!apiYouTube->GetBroadcastsList(broadcasts, "", - broadcastStatus)) { + for (LiveBroadcastListStatus broadcastStatus : + {LiveBroadcastListStatus::ACTIVE, + LiveBroadcastListStatus::UPCOMMING}) { + + std::vector broadcasts; + if (!apiYouTube->GetLiveBroadcastsList(broadcastStatus, + broadcasts)) { emit failed(); return; } - while (pending) { - auto items = broadcasts["items"].array_items(); - for (auto item : items) { - QString status = QString::fromStdString( - item["status"]["lifeCycleStatus"] - .string_value()); - - if (status == "live" || status == "testing") { - // Check that the attached liveStream is offline (reconnectable) - QString stream_id = QString::fromStdString( - item["contentDetails"] - ["boundStreamId"] - .string_value()); - json11::Json stream; - if (!apiYouTube->FindStream(stream_id, - stream)) - continue; - if (stream["status"]["streamStatus"] == - "active") - continue; - } - - QString title = QString::fromStdString( - item["snippet"]["title"].string_value()); - QString scheduledStartTime = - QString::fromStdString( - item["snippet"] - ["scheduledStartTime"] - .string_value()); - QString broadcast = QString::fromStdString( - item["id"].string_value()); - - // Treat already started streams as autostart for UI purposes - bool astart = - status == "live" || - item["contentDetails"]["enableAutoStart"] - .bool_value(); - bool astop = - item["contentDetails"]["enableAutoStop"] - .bool_value(); - - QDateTime utcDTime = QDateTime::fromString( - scheduledStartTime, - SchedulDateAndTimeFormat); - // DateTime parser means that input datetime is a local, so we need to move it - QDateTime dateTime = utcDTime.addSecs( - utcDTime.offsetFromUtc()); - - QString dateTimeString = QLocale().toString( - dateTime, - QString("%1 %2").arg( - QLocale().dateFormat( - QLocale::LongFormat), - QLocale().timeFormat( - QLocale::ShortFormat))); - - emit new_item(title, dateTimeString, broadcast, - status, astart, astop); + for (LiveBroadcast item : broadcasts) { + LiveBroadcastLifeCycleStatus status = + item.status.lifeCycleStatus; + + if ((status == LiveBroadcastLifeCycleStatus::LIVE || + status == LiveBroadcastLifeCycleStatus::TESTING) && + item.contentDetails.boundStreamId.has_value()) { + // Check that the attached liveStream is offline (reconnectable) + LiveStream stream; + if (!apiYouTube->FindLiveStream( + item.contentDetails.boundStreamId + .value(), + stream)) + continue; + if (stream.status.streamStatus == + LiveStreamStatusEnum::ACTIVE) + continue; } - auto nextPageToken = - broadcasts["nextPageToken"].string_value(); - if (nextPageToken.empty() || items.empty()) - break; - else { - if (!pending) - return; - if (!apiYouTube->GetBroadcastsList( - broadcasts, - QString::fromStdString( - nextPageToken), - broadcastStatus)) { - emit failed(); - return; - } - } + QString title = + QString::fromStdString(item.snippet.title); + QString scheduledStartTime = QString::fromStdString( + item.snippet.scheduledStartTime.value_or("")); + QString broadcast = QString::fromStdString(item.id); + + // Treat already started streams as autostart for UI purposes + bool astart = + status == LiveBroadcastLifeCycleStatus::LIVE || + item.contentDetails.enableAutoStart; + bool astop = item.contentDetails.enableAutoStop; + + QDateTime utcDTime = QDateTime::fromString( + scheduledStartTime, SchedulDateAndTimeFormat); + // DateTime parser means that input datetime is a local, so we need to move it + QDateTime dateTime = + utcDTime.addSecs(utcDTime.offsetFromUtc()); + + QString dateTimeString = QLocale().toString( + dateTime, + QString("%1 %2").arg( + QLocale().dateFormat( + QLocale::LongFormat), + QLocale().timeFormat( + QLocale::ShortFormat))); + + emit new_item(title, dateTimeString, broadcast, status, + astart, astop); + + if (!pending) + return; } } @@ -422,92 +467,113 @@ void OBSYoutubeActions::UpdateOkButtonStatus() ui->saveButton->setEnabled(enable); if (ui->checkScheduledLater->checkState() == Qt::Checked) { - ui->okButton->setText( - QTStr("YouTube.Actions.Create_Schedule")); - ui->saveButton->setText( - QTStr("YouTube.Actions.Create_Schedule_Ready")); + ui->okButton->setText(QT_UTF8(obs_module_text( + "YouTube.Actions.Create_Schedule"))); + ui->saveButton->setText(QT_UTF8(obs_module_text( + "YouTube.Actions.Create_Schedule_Ready"))); } else { - ui->okButton->setText( - QTStr("YouTube.Actions.Create_GoLive")); - ui->saveButton->setText( - QTStr("YouTube.Actions.Create_Ready")); + ui->okButton->setText(QT_UTF8(obs_module_text( + "YouTube.Actions.Create_GoLive"))); + ui->saveButton->setText(QT_UTF8(obs_module_text( + "YouTube.Actions.Create_Ready"))); } ui->pushButton->setVisible(false); } else { enable = !selectedBroadcast.isEmpty(); ui->okButton->setEnabled(enable); ui->saveButton->setEnabled(enable); - ui->okButton->setText(QTStr("YouTube.Actions.Choose_GoLive")); - ui->saveButton->setText(QTStr("YouTube.Actions.Choose_Ready")); + ui->okButton->setText(QT_UTF8( + obs_module_text("YouTube.Actions.Choose_GoLive"))); + ui->saveButton->setText(QT_UTF8( + obs_module_text("YouTube.Actions.Choose_Ready"))); ui->pushButton->setVisible(true); } } -bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, - StreamDescription &stream, - bool stream_later, +bool OBSYoutubeActions::CreateEventAction(LiveStream &stream, bool stream_later, bool ready_broadcast) { - YoutubeApiWrappers *apiYouTube = api; - BroadcastDescription broadcast = {}; - UiToBroadcast(broadcast); + LiveBroadcast broadcast = UiToBroadcast(); if (stream_later) { // DateTime parser means that input datetime is a local, so we need to move it auto dateTime = ui->scheduledTime->dateTime(); auto utcDTime = dateTime.addSecs(-dateTime.offsetFromUtc()); - broadcast.schedul_date_time = - utcDTime.toString(SchedulDateAndTimeFormat); + broadcast.snippet.scheduledStartTime = + utcDTime.toString(SchedulDateAndTimeFormat) + .toStdString(); } else { // stream now is always autostart/autostop - broadcast.auto_start = true; - broadcast.auto_stop = true; - broadcast.schedul_date_time = - QDateTime::currentDateTimeUtc().toString( - SchedulDateAndTimeFormat); + broadcast.contentDetails.enableAutoStart = true; + broadcast.contentDetails.enableAutoStop = true; + broadcast.snippet.scheduledStartTime = + QDateTime::currentDateTimeUtc() + .toString(SchedulDateAndTimeFormat) + .toStdString(); } - autostart = broadcast.auto_start; - autostop = broadcast.auto_stop; + autostart = broadcast.contentDetails.enableAutoStart; + autostop = broadcast.contentDetails.enableAutoStop; blog(LOG_DEBUG, "Scheduled date and time: %s", - broadcast.schedul_date_time.toStdString().c_str()); - if (!apiYouTube->InsertBroadcast(broadcast)) { + broadcast.snippet.scheduledStartTime.value().c_str()); + if (!apiYouTube->InsertLiveBroadcast(broadcast)) { blog(LOG_DEBUG, "No broadcast created."); return false; } - if (!apiYouTube->SetVideoCategory(broadcast.id, broadcast.title, - broadcast.description, - broadcast.category.id)) { + Video video; + video.id = broadcast.id; + video.snippet.title = broadcast.snippet.title; + video.snippet.description = broadcast.snippet.description; + video.snippet.categoryId = + ui->categoryBox->currentData().toString().toStdString(); + if (!apiYouTube->UpdateVideo(video)) { blog(LOG_DEBUG, "No category set."); return false; } - if (!thumbnailFile.isEmpty()) { + if (!thumbnailFilePath.isEmpty()) { blog(LOG_INFO, "Uploading thumbnail file \"%s\"...", - thumbnailFile.toStdString().c_str()); - if (!apiYouTube->SetVideoThumbnail(broadcast.id, - thumbnailFile)) { + thumbnailFilePath.toStdString().c_str()); + + const QString mime = + QMimeDatabase().mimeTypeForData(thumbnailData).name(); + + if (!apiYouTube->SetThumbnail(broadcast.id, QT_TO_UTF8(mime), + thumbnailData.constData(), + thumbnailData.size())) { blog(LOG_DEBUG, "No thumbnail set."); return false; } } if (!stream_later || ready_broadcast) { - stream = {"", "", "OBS Studio Video Stream"}; - if (!apiYouTube->InsertStream(stream)) { + stream.snippet.title = "OBS Studio Video Stream"; + stream.cdn.ingestionType = apiYouTube->GetIngestionType(); + stream.cdn.resolution = + LiveStreamCdnResolution::RESOLUTION_VARIABLE; + stream.cdn.frameRate = + LiveStreamCdnFrameRate::FRAMERATE_VARIABLE; + LiveStreamContentDetails contentDetails; + contentDetails.isReusable = false; + stream.contentDetails = contentDetails; + if (!apiYouTube->InsertLiveStream(stream)) { blog(LOG_DEBUG, "No stream created."); return false; } - json11::Json json; - if (!apiYouTube->BindStream(broadcast.id, stream.id, json)) { + + if (!apiYouTube->BindLiveBroadcast(broadcast, stream.id)) { blog(LOG_DEBUG, "No stream binded."); return false; } - if (broadcast.privacy != "private") { + apiYouTube->SetBroadcastId(broadcast.id); + + if (broadcast.status.privacyStatus != + LiveBroadcastPrivacyStatus::PRIVATE && + broadcast.snippet.liveChatId.has_value()) { const std::string apiLiveChatId = - json["snippet"]["liveChatId"].string_value(); - apiYouTube->SetChatId(broadcast.id, apiLiveChatId); + broadcast.snippet.liveChatId.value(); + apiYouTube->SetChatIds(broadcast.id, apiLiveChatId); } else { apiYouTube->ResetChat(); } @@ -516,57 +582,65 @@ bool OBSYoutubeActions::CreateEventAction(YoutubeApiWrappers *api, return true; } -bool OBSYoutubeActions::ChooseAnEventAction(YoutubeApiWrappers *api, - StreamDescription &stream) +bool OBSYoutubeActions::ChooseAnEventAction(LiveStream &stream) { - YoutubeApiWrappers *apiYouTube = api; - - json11::Json json; - if (!apiYouTube->FindBroadcast(selectedBroadcast, json)) { + LiveBroadcast broadcast; + if (!apiYouTube->FindLiveBroadcast(selectedBroadcast.toStdString(), + broadcast)) { blog(LOG_DEBUG, "No broadcast found."); return false; } std::string boundStreamId = - json["items"] - .array_items()[0]["contentDetails"]["boundStreamId"] - .string_value(); - std::string broadcastPrivacy = - json["items"] - .array_items()[0]["status"]["privacyStatus"] - .string_value(); - std::string apiLiveChatId = - json["items"] - .array_items()[0]["snippet"]["liveChatId"] - .string_value(); - - stream.id = boundStreamId.c_str(); - if (!stream.id.isEmpty() && apiYouTube->FindStream(stream.id, json)) { - auto item = json["items"].array_items()[0]; - auto streamName = item["cdn"]["ingestionInfo"]["streamName"] - .string_value(); - auto title = item["snippet"]["title"].string_value(); - - stream.name = streamName.c_str(); - stream.title = title.c_str(); - api->SetBroadcastId(selectedBroadcast); - } else { - stream = {"", "", "OBS Studio Video Stream"}; - if (!apiYouTube->InsertStream(stream)) { + broadcast.contentDetails.boundStreamId.value_or(""); + bool bindStream = true; + if (!boundStreamId.empty() && + apiYouTube->FindLiveStream(boundStreamId, stream)) { + + if (stream.cdn.ingestionType != + apiYouTube->GetIngestionType()) { + blog(LOG_WARNING, + "Missmatch ingestion types, binding a new stream required"); + if (!apiYouTube->BindLiveBroadcast(broadcast)) { + blog(LOG_DEBUG, "Could not unbind stream"); + return false; + } + } else { + bindStream = false; + } + } + + if (bindStream) { + stream.snippet.title = "OBS Studio Video Stream"; + stream.cdn.ingestionType = apiYouTube->GetIngestionType(); + stream.cdn.resolution = + LiveStreamCdnResolution::RESOLUTION_VARIABLE; + stream.cdn.frameRate = + LiveStreamCdnFrameRate::FRAMERATE_VARIABLE; + LiveStreamContentDetails contentDetails; + contentDetails.isReusable = false; + stream.contentDetails = contentDetails; + if (!apiYouTube->InsertLiveStream(stream)) { blog(LOG_DEBUG, "No stream created."); return false; } - if (!apiYouTube->BindStream(selectedBroadcast, stream.id, - json)) { + if (!apiYouTube->BindLiveBroadcast(broadcast, stream.id)) { blog(LOG_DEBUG, "No stream binded."); return false; } } - if (broadcastPrivacy != "private") - apiYouTube->SetChatId(selectedBroadcast, apiLiveChatId); - else + apiYouTube->SetBroadcastId(broadcast.id); + + if (broadcast.status.privacyStatus != + LiveBroadcastPrivacyStatus::PRIVATE && + broadcast.snippet.liveChatId.has_value()) { + const std::string apiLiveChatId = + broadcast.snippet.liveChatId.value(); + apiYouTube->SetChatIds(broadcast.id, apiLiveChatId); + } else { apiYouTube->ResetChat(); + } return true; } @@ -575,7 +649,7 @@ void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text) { QMessageBox dlg(parent); dlg.setWindowFlags(dlg.windowFlags() & ~Qt::WindowCloseButtonHint); - dlg.setWindowTitle(QTStr("YouTube.Actions.Error.Title")); + dlg.setWindowTitle(obs_module_text("YouTube.Actions.Error.Title")); dlg.setText(text); dlg.setTextFormat(Qt::RichText); dlg.setIcon(QMessageBox::Warning); @@ -585,27 +659,27 @@ void OBSYoutubeActions::ShowErrorDialog(QWidget *parent, QString text) void OBSYoutubeActions::InitBroadcast() { - StreamDescription stream; + LiveStream stream; QMessageBox msgBox(this); msgBox.setWindowFlags(msgBox.windowFlags() & ~Qt::WindowCloseButtonHint); - msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title")); - msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast")); + msgBox.setWindowTitle(obs_module_text("YouTube.Actions.Notify.Title")); + msgBox.setText( + obs_module_text("YouTube.Actions.Notify.CreatingBroadcast")); msgBox.setStandardButtons(QMessageBox::StandardButtons()); bool success = false; auto action = [&]() { if (ui->tabWidget->currentIndex() == 0) { success = this->CreateEventAction( - apiYouTube, stream, - ui->checkScheduledLater->isChecked()); + stream, ui->checkScheduledLater->isChecked()); } else { - success = this->ChooseAnEventAction(apiYouTube, stream); + success = this->ChooseAnEventAction(stream); }; QMetaObject::invokeMethod(&msgBox, "accept", Qt::QueuedConnection); }; - QScopedPointer thread(CreateQThread(action)); + QScopedPointer thread(new QuickThread(action)); thread->start(); msgBox.exec(); thread->wait(); @@ -615,9 +689,9 @@ void OBSYoutubeActions::InitBroadcast() // Stream later usecase. if (ui->checkScheduledLater->isChecked()) { QMessageBox msg(this); - msg.setWindowTitle(QTStr( + msg.setWindowTitle(obs_module_text( "YouTube.Actions.EventCreated.Title")); - msg.setText(QTStr( + msg.setText(obs_module_text( "YouTube.Actions.EventCreated.Text")); msg.setStandardButtons(QMessageBox::Ok); msg.exec(); @@ -626,198 +700,201 @@ void OBSYoutubeActions::InitBroadcast() } else { // Stream now usecase. blog(LOG_DEBUG, "New valid stream: %s", - QT_TO_UTF8(stream.name)); - emit ok(QT_TO_UTF8(stream.id), - QT_TO_UTF8(stream.name), true, true, - true); + stream.cdn.ingestionInfo.streamName + .c_str()); + apiYouTube->SetNewStream( + stream.id, + stream.cdn.ingestionInfo.streamName, + true, true, true); Accept(); } } else { // Stream to precreated broadcast usecase. - emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name), + apiYouTube->SetNewStream( + stream.id, stream.cdn.ingestionInfo.streamName, autostart, autostop, true); Accept(); } } else { // Fail. - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.NoBroadcastCreated") - .arg(last_error); - - ShowErrorDialog(this, last_error); + RequestError lastError = apiYouTube->GetLastError(); + QString error = GetTranslatedError(lastError); + ShowErrorDialog( + this, + error.isEmpty() + ? obs_module_text( + "YouTube.Actions.Error.YouTubeApi") + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.NoBroadcastCreated")) + .arg(error)); } } void OBSYoutubeActions::ReadyBroadcast() { - StreamDescription stream; + LiveStream stream; QMessageBox msgBox(this); msgBox.setWindowFlags(msgBox.windowFlags() & ~Qt::WindowCloseButtonHint); - msgBox.setWindowTitle(QTStr("YouTube.Actions.Notify.Title")); - msgBox.setText(QTStr("YouTube.Actions.Notify.CreatingBroadcast")); + msgBox.setWindowTitle(obs_module_text("YouTube.Actions.Notify.Title")); + msgBox.setText( + obs_module_text("YouTube.Actions.Notify.CreatingBroadcast")); msgBox.setStandardButtons(QMessageBox::StandardButtons()); bool success = false; auto action = [&]() { if (ui->tabWidget->currentIndex() == 0) { success = this->CreateEventAction( - apiYouTube, stream, - ui->checkScheduledLater->isChecked(), true); + stream, ui->checkScheduledLater->isChecked(), + true); } else { - success = this->ChooseAnEventAction(apiYouTube, stream); + success = this->ChooseAnEventAction(stream); }; QMetaObject::invokeMethod(&msgBox, "accept", Qt::QueuedConnection); }; - QScopedPointer thread(CreateQThread(action)); + QScopedPointer thread(new QuickThread(action)); thread->start(); msgBox.exec(); thread->wait(); if (success) { - emit ok(QT_TO_UTF8(stream.id), QT_TO_UTF8(stream.name), - autostart, autostop, false); + apiYouTube->SetNewStream(stream.id, + stream.cdn.ingestionInfo.streamName, + autostart, autostop, false); Accept(); } else { // Fail. - auto last_error = apiYouTube->GetLastError(); - if (last_error.isEmpty()) - last_error = QTStr("YouTube.Actions.Error.YouTubeApi"); - if (!apiYouTube->GetTranslatedError(last_error)) - last_error = - QTStr("YouTube.Actions.Error.NoBroadcastCreated") - .arg(last_error); - - ShowErrorDialog(this, last_error); + RequestError lastError = apiYouTube->GetLastError(); + QString error = GetTranslatedError(lastError); + ShowErrorDialog( + this, + error.isEmpty() + ? obs_module_text( + "YouTube.Actions.Error.YouTubeApi") + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.NoBroadcastCreated")) + .arg(error)); } } -void OBSYoutubeActions::UiToBroadcast(BroadcastDescription &broadcast) +LiveBroadcast OBSYoutubeActions::UiToBroadcast() { - broadcast.title = ui->title->text(); + LiveBroadcast broadcast; + + broadcast.snippet.title = ui->title->text().toStdString(); // ToDo: UI warning rather than silent truncation - broadcast.description = ui->description->toPlainText().left(5000); - broadcast.privacy = ui->privacyBox->currentData().toString(); - broadcast.category.title = ui->categoryBox->currentText(); - broadcast.category.id = ui->categoryBox->currentData().toString(); - broadcast.made_for_kids = ui->yesMakeForKids->isChecked(); - broadcast.latency = ui->latencyBox->currentData().toString(); - broadcast.auto_start = ui->checkAutoStart->isChecked(); - broadcast.auto_stop = ui->checkAutoStop->isChecked(); - broadcast.dvr = ui->checkDVR->isChecked(); - broadcast.schedul_for_later = ui->checkScheduledLater->isChecked(); - broadcast.projection = ui->check360Video->isChecked() ? "360" - : "rectangular"; - - if (ui->checkRememberSettings->isChecked()) - SaveSettings(broadcast); + broadcast.snippet.description = + ui->description->toPlainText().left(5000).toStdString(); + broadcast.status.privacyStatus = + (LiveBroadcastPrivacyStatus)ui->privacyBox->currentData() + .toInt(); + broadcast.status.selfDeclaredMadeForKids = + ui->yesMakeForKids->isChecked(); + broadcast.contentDetails.latencyPreference = + (LiveBroadcastLatencyPreference)ui->latencyBox->currentData() + .toInt(); + broadcast.contentDetails.enableAutoStart = + ui->checkAutoStart->isChecked(); + broadcast.contentDetails.enableAutoStop = + ui->checkAutoStop->isChecked(); + broadcast.contentDetails.enableDvr = ui->checkDVR->isChecked(); + broadcast.contentDetails.projection = + ui->check360Video->isChecked() + ? LiveBroadcastProjection::THREE_HUNDRED_SIXTY + : LiveBroadcastProjection::RECTANGULAR; + + SaveSettings(); + + return broadcast; } -void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast) +void OBSYoutubeActions::SaveSettings() { - OBSBasic *main = OBSBasic::Get(); - - config_set_string(main->basicConfig, "YouTube", "Title", - QT_TO_UTF8(broadcast.title)); - config_set_string(main->basicConfig, "YouTube", "Description", - QT_TO_UTF8(broadcast.description)); - config_set_string(main->basicConfig, "YouTube", "Privacy", - QT_TO_UTF8(broadcast.privacy)); - config_set_string(main->basicConfig, "YouTube", "CategoryID", - QT_TO_UTF8(broadcast.category.id)); - config_set_string(main->basicConfig, "YouTube", "Latency", - QT_TO_UTF8(broadcast.latency)); - config_set_bool(main->basicConfig, "YouTube", "MadeForKids", - broadcast.made_for_kids); - config_set_bool(main->basicConfig, "YouTube", "AutoStart", - broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "AutoStop", - broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr); - config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater", - broadcast.schedul_for_later); - config_set_string(main->basicConfig, "YouTube", "Projection", - QT_TO_UTF8(broadcast.projection)); - config_set_string(main->basicConfig, "YouTube", "ThumbnailFile", - QT_TO_UTF8(thumbnailFile)); - config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true); + settings->rememberSettings = ui->checkRememberSettings->isChecked(); + + if (!settings->rememberSettings) + return; + + settings->title = ui->title->text().toStdString(); + // ToDo: UI warning rather than silent truncation + settings->description = + ui->description->toPlainText().left(5000).toStdString(); + settings->privacy = + (LiveBroadcastPrivacyStatus)ui->privacyBox->currentData() + .toInt(); + settings->categoryId = + ui->categoryBox->currentData().toString().toStdString(); + settings->madeForKids = ui->yesMakeForKids->isChecked(); + settings->latency = + (LiveBroadcastLatencyPreference)ui->latencyBox->currentData() + .toInt(); + settings->autoStart = ui->checkAutoStart->isChecked(); + settings->autoStop = ui->checkAutoStop->isChecked(); + settings->dvr = ui->checkDVR->isChecked(); + settings->scheduleForLater = ui->checkScheduledLater->isChecked(); + settings->projection = + ui->check360Video->isChecked() + ? LiveBroadcastProjection::THREE_HUNDRED_SIXTY + : LiveBroadcastProjection::RECTANGULAR; + settings->thumbnailFile = thumbnailFilePath.toStdString(); } void OBSYoutubeActions::LoadSettings() { - OBSBasic *main = OBSBasic::Get(); + ui->checkRememberSettings->setChecked(settings->rememberSettings); - const char *title = - config_get_string(main->basicConfig, "YouTube", "Title"); - ui->title->setText(QT_UTF8(title)); + ui->title->setText(QString::fromStdString(settings->title)); - const char *desc = - config_get_string(main->basicConfig, "YouTube", "Description"); - ui->description->setPlainText(QT_UTF8(desc)); + ui->description->setPlainText( + QString::fromStdString(settings->description)); - const char *priv = - config_get_string(main->basicConfig, "YouTube", "Privacy"); - int index = ui->privacyBox->findData(priv); + int index = ui->privacyBox->findData((int)settings->privacy); ui->privacyBox->setCurrentIndex(index); - const char *catID = - config_get_string(main->basicConfig, "YouTube", "CategoryID"); - index = ui->categoryBox->findData(catID); + index = ui->categoryBox->findData( + QString::fromStdString(settings->categoryId)); ui->categoryBox->setCurrentIndex(index); - const char *latency = - config_get_string(main->basicConfig, "YouTube", "Latency"); - index = ui->latencyBox->findData(latency); + index = ui->latencyBox->findData((int)settings->latency); ui->latencyBox->setCurrentIndex(index); - bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR"); - ui->checkDVR->setChecked(dvr); + ui->checkDVR->setChecked(settings->dvr); - bool forKids = - config_get_bool(main->basicConfig, "YouTube", "MadeForKids"); - if (forKids) + if (settings->madeForKids) ui->yesMakeForKids->setChecked(true); else ui->notMakeForKids->setChecked(true); - bool schedLater = config_get_bool(main->basicConfig, "YouTube", - "ScheduleForLater"); - ui->checkScheduledLater->setChecked(schedLater); - - bool autoStart = - config_get_bool(main->basicConfig, "YouTube", "AutoStart"); - ui->checkAutoStart->setChecked(autoStart); - - bool autoStop = - config_get_bool(main->basicConfig, "YouTube", "AutoStop"); - ui->checkAutoStop->setChecked(autoStop); - - const char *projection = - config_get_string(main->basicConfig, "YouTube", "Projection"); - if (projection && *projection) { - if (strcmp(projection, "360") == 0) - ui->check360Video->setChecked(true); - else - ui->check360Video->setChecked(false); - } + ui->checkScheduledLater->setChecked(settings->scheduleForLater); + + ui->checkAutoStart->setChecked(settings->autoStart); + + ui->checkAutoStop->setChecked(settings->autoStop); + + ui->check360Video->setChecked( + settings->projection == + LiveBroadcastProjection::THREE_HUNDRED_SIXTY); + + const char *thumbFile = settings->thumbnailFile.c_str(); - const char *thumbFile = config_get_string(main->basicConfig, "YouTube", - "ThumbnailFile"); if (thumbFile && *thumbFile) { QFileInfo tFile(thumbFile); // Re-check validity before setting path again if (tFile.exists() && tFile.size() <= 2 * 1024 * 1024) { - thumbnailFile = tFile.absoluteFilePath(); - ui->selectedFileName->setText(thumbnailFile); - ui->selectFileButton->setText( - QTStr("YouTube.Actions.Thumbnail.ClearFile")); - QImageReader imgReader(thumbnailFile); + QFile thumbnailFile(thumbFile); + if (!thumbnailFile.open(QFile::ReadOnly)) { + return; + } + + thumbnailData = thumbnailFile.readAll(); + thumbnailFilePath = tFile.absoluteFilePath(); + ui->selectedFileName->setText(thumbnailFilePath); + ui->selectFileButton->setText(obs_module_text( + "YouTube.Actions.Thumbnail.ClearFile")); + + QImageReader imgReader(thumbnailFilePath); imgReader.setAutoTransform(true); const QImage newImage = imgReader.read(); ui->thumbnailPreview->setPixmap( @@ -830,22 +907,26 @@ void OBSYoutubeActions::LoadSettings() void OBSYoutubeActions::OpenYouTubeDashboard() { - ChannelDescription channel; - if (!apiYouTube->GetChannelDescription(channel)) { + ChannelInfo channel; + if (!apiYouTube->GetChannelInfo(channel)) { blog(LOG_DEBUG, "Could not get channel description."); + RequestError lastError = apiYouTube->GetLastError(); ShowErrorDialog( this, - apiYouTube->GetLastError().isEmpty() - ? QTStr("YouTube.Actions.Error.General") - : QTStr("YouTube.Actions.Error.Text") - .arg(apiYouTube->GetLastError())); + lastError.message.empty() + ? obs_module_text( + "YouTube.Actions.Error.General") + : QT_UTF8(obs_module_text( + "YouTube.Actions.Error.Text")) + .arg(QString::fromStdString( + lastError.message))); return; } //https://studio.youtube.com/channel/UCA9bSfH3KL186kyiUsvi3IA/videos/live?filter=%5B%5D&sort=%7B%22columnType%22%3A%22date%22%2C%22sortOrder%22%3A%22DESCENDING%22%7D QString uri = QString("https://studio.youtube.com/channel/%1/videos/live?filter=[]&sort={\"columnType\"%3A\"date\"%2C\"sortOrder\"%3A\"DESCENDING\"}") - .arg(channel.id); + .arg(QString::fromStdString(channel.id)); QDesktopServices::openUrl(uri); } diff --git a/plugins/obs-youtube/window-youtube-actions.hpp b/plugins/obs-youtube/window-youtube-actions.hpp index 18cbf0b8a0be3b..c7c864cefa54ec 100644 --- a/plugins/obs-youtube/window-youtube-actions.hpp +++ b/plugins/obs-youtube/window-youtube-actions.hpp @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Yuriy Chumak +// +// SPDX-License-Identifier: GPL-2.0-or-later + #pragma once #include @@ -5,17 +9,19 @@ #include #include "ui_OBSYoutubeActions.h" -#include "youtube-api-wrappers.hpp" +#include "youtube-oauth.hpp" class WorkerThread : public QThread { Q_OBJECT public: - WorkerThread(YoutubeApiWrappers *api) : QThread(), apiYouTube(api) {} + WorkerThread(YouTubeApi::ServiceOAuth *api) : QThread(), apiYouTube(api) + { + } void stop() { pending = false; } protected: - YoutubeApiWrappers *apiYouTube; + YouTubeApi::ServiceOAuth *apiYouTube; bool pending = true; public slots: @@ -23,7 +29,8 @@ public slots: signals: void ready(); void new_item(const QString &title, const QString &dateTimeString, - const QString &broadcast, const QString &status, + const QString &broadcast, + const YouTubeApi::LiveBroadcastLifeCycleStatus &status, bool astart, bool astop); void failed(); }; @@ -35,6 +42,8 @@ class OBSYoutubeActions : public QDialog { std::unique_ptr ui; + OBSYoutubeActionsSettings *settings; + signals: void ok(const QString &id, const QString &key, bool autostart, bool autostop, bool start_now); @@ -43,16 +52,16 @@ class OBSYoutubeActions : public QDialog { void showEvent(QShowEvent *event) override; void UpdateOkButtonStatus(); - bool CreateEventAction(YoutubeApiWrappers *api, - StreamDescription &stream, bool stream_later, - bool ready_broadcast = false); - bool ChooseAnEventAction(YoutubeApiWrappers *api, - StreamDescription &stream); + bool CreateEventAction(YouTubeApi::LiveStream &stream, + bool stream_later, bool ready_broadcast = false); + bool ChooseAnEventAction(YouTubeApi::LiveStream &stream); void ShowErrorDialog(QWidget *parent, QString text); public: - explicit OBSYoutubeActions(QWidget *parent, Auth *auth, + explicit OBSYoutubeActions(QWidget *parent, + YouTubeApi::ServiceOAuth *auth, + OBSYoutubeActionsSettings *settings, bool broadcastReady); virtual ~OBSYoutubeActions() override; @@ -61,11 +70,11 @@ class OBSYoutubeActions : public QDialog { private: void InitBroadcast(); void ReadyBroadcast(); - void UiToBroadcast(BroadcastDescription &broadcast); + YouTubeApi::LiveBroadcast UiToBroadcast(); void OpenYouTubeDashboard(); void Cancel(); void Accept(); - void SaveSettings(BroadcastDescription &broadcast); + void SaveSettings(); void LoadSettings(); QIcon GetPlaceholder() { return thumbPlaceholder; } @@ -74,9 +83,10 @@ class OBSYoutubeActions : public QDialog { QString selectedBroadcast; bool autostart, autostop; bool valid = false; - YoutubeApiWrappers *apiYouTube; + YouTubeApi::ServiceOAuth *apiYouTube; WorkerThread *workerThread = nullptr; bool broadcastReady = false; - QString thumbnailFile; + QString thumbnailFilePath; + QByteArray thumbnailData; QIcon thumbPlaceholder; }; diff --git a/plugins/obs-youtube/youtube-api.hpp b/plugins/obs-youtube/youtube-api.hpp new file mode 100644 index 00000000000000..667d2f2fa4ad4b --- /dev/null +++ b/plugins/obs-youtube/youtube-api.hpp @@ -0,0 +1,923 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace YouTubeApi { +using nlohmann::json; + +#ifndef NLOHMANN_OPTIONAL_YouTubeApi_HELPER +#define NLOHMANN_OPTIONAL_YouTubeApi_HELPER +template +inline std::optional get_stack_optional(const json &j, const char *property) +{ + auto it = j.find(property); + if (it != j.end() && !it->is_null()) { + return j.at(property).get>(); + } + return std::optional(); +} +#endif + +/* NOTE: Heavily stripped out structure of the object */ +struct ErrorVerbose { + std::string reason; +}; + +struct Error { + int64_t code; + std::string message; + std::vector errors; +}; + +struct ErrorResponse { + Error error; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct ChannelSnippet { + std::string title; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct Channel { + std::string id; + /* NOTE: "snippet" should be optional but since we will always ask for + * this part. So put as not optional. */ + ChannelSnippet snippet; +}; + +/* NOTE: Heavily stripped out structure of the response */ +struct ChannelListSnippetMineResponse { + std::vector items; +}; + +struct VideoCategorySnippet { + std::string channelId; + std::string title; + bool assignable; +}; + +/* NOTE: Stripped out structure of the object */ +struct VideoCategory { + std::string id; + VideoCategorySnippet snippet; +}; + +/* NOTE: Heavily stripped out structure of the response */ +struct VideoCategoryListResponse { + std::vector items; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveBroadcastSnippet { + std::string title; + std::string description; + std::optional scheduledStartTime; + std::optional scheduledEndTime; + std::optional liveChatId; +}; + +enum class LiveBroadcastLifeCycleStatus : int { + COMPLETE, + CREATED, + LIVE, + LIVE_STARTING, + READY, + REVOKED, + TEST_STARTING, + TESTING, +}; + +enum class LiveBroadcastPrivacyStatus : int { + PRIVATE, + PUBLIC, + UNLISTED, +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveBroadcastStatus { + LiveBroadcastLifeCycleStatus lifeCycleStatus; + LiveBroadcastPrivacyStatus privacyStatus; + bool selfDeclaredMadeForKids; +}; + +struct LiveBroadcastMonitorStream { + bool enableMonitorStream; + std::optional broadcastStreamDelayMs; +}; + +enum class LiveBroadcastClosedCaptionsType : int { + DISABLED, + HTTP_POST, + EMBEDDED, +}; + +enum class LiveBroadcastProjection : int { + RECTANGULAR, + THREE_HUNDRED_SIXTY, +}; + +enum class LiveBroadcastLatencyPreference : int { + NORMAL, + LOW, + ULTRALOW, +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveBroadcastContentDetails { + std::optional boundStreamId; + LiveBroadcastMonitorStream monitorStream; + std::optional enableEmbed; + bool enableDvr; + std::optional recordFromStart; + std::optional closedCaptionsType; + LiveBroadcastProjection projection; + LiveBroadcastLatencyPreference latencyPreference; + bool enableAutoStart; + bool enableAutoStop; +}; + +/* NOTE: We always request all of those */ +struct LiveBroadcast { + std::string id; + LiveBroadcastSnippet snippet; + LiveBroadcastStatus status; + LiveBroadcastContentDetails contentDetails; +}; + +/* NOTE: Stripped out structure of the response */ +struct LiveBroadcastListResponse { + std::optional nextPageToken; + std::vector items; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveStreamSnippet { + std::string title; +}; + +enum class LiveStreamCdnIngestionType : int { + DASH, + HLS, + RTMP, +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveStreamCdnIngestionInfo { + std::string streamName; +}; + +enum class LiveStreamCdnResolution : int { + RESOLUTION_VARIABLE, + RESOLUTION_240P, + RESOLUTION_360P, + RESOLUTION_480P, + RESOLUTION_720P, + RESOLUTION_1080P, + RESOLUTION_1440P, + RESOLUTION_2160P, +}; + +enum class LiveStreamCdnFrameRate : int { + FRAMERATE_VARIABLE, + FRAMERATE_30FPS, + FRAMERATE_60FPS, +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveStreamCdn { + LiveStreamCdnIngestionType ingestionType; + LiveStreamCdnIngestionInfo ingestionInfo; + LiveStreamCdnResolution resolution; + LiveStreamCdnFrameRate frameRate; +}; + +enum class LiveStreamStatusEnum : int { + ACTIVE, + CREATED, + /* Blame MSVC to not allow ERROR */ + ERR0R, + INACTIVE, + READY, +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveStreamStatus { + LiveStreamStatusEnum streamStatus; +}; + +/* NOTE: Stripped out structure of the object */ +struct LiveStreamContentDetails { + bool isReusable; +}; + +/* NOTE: We always request all of those */ +struct LiveStream { + std::string id; + LiveStreamSnippet snippet; + LiveStreamCdn cdn; + LiveStreamStatus status; + std::optional contentDetails; +}; + +/* NOTE: Stripped out structure of the response */ +struct LiveStreamListResponse { + std::vector items; +}; + +/* NOTE: Stripped out structure of the object */ +struct VideoSnippet { + std::string title; + std::string description; + std::string categoryId; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct Video { + std::string id; + VideoSnippet snippet; +}; + +enum class LiveChatTextMessageType : int { + CHAT_ENDED_EVENT, + MESSAGE_DELETED_EVENT, + SPONSOR_ONLY_MODE_ENDED_EVENT, + SPONSOR_ONLY_MODE_STARTED_EVENT, + NEW_SPONSOR_EVENT, + MEMBER_MILESTONE_CHAT_EVENT, + SUPER_CHAT_EVENT, + SUPER_STICKER_EVENT, + TEXT_MESSAGE_EVENT, + TOMBSTONE, + USER_BANNED_EVENT, + MEMBERSHIP_GIFTING_EVENT, + GIFT_MEMBERSHIP_RECEIVED_EVENT, +}; + +/* NOTE: Stripped out structure of the object */ +struct LiveChatTextMessageDetails { + std::string messageText; +}; + +/* NOTE: Heavily stripped out structure of the object */ +struct LiveChatMessageSnippet { + LiveChatTextMessageType type; + std::string liveChatId; + LiveChatTextMessageDetails textMessageDetails; +}; + +/* NOTE: Stripped out structure of the object */ +struct LiveChatMessage { + LiveChatMessageSnippet snippet; +}; + +inline void from_json(const json &j, ErrorVerbose &s) +{ + s.reason = j.at("reason").get(); +} + +inline void from_json(const json &j, Error &s) +{ + s.code = j.at("code").get(); + s.message = j.at("message").get(); + s.errors = j.at("errors").get>(); +} + +inline void from_json(const json &j, ErrorResponse &s) +{ + s.error = j.at("error").get(); +} + +inline void from_json(const json &j, ChannelSnippet &s) +{ + s.title = j.at("title").get(); +} + +inline void from_json(const json &j, Channel &s) +{ + s.id = j.at("id").get(); + s.snippet = j.at("snippet").get(); +} + +inline void from_json(const json &j, ChannelListSnippetMineResponse &s) +{ + s.items = j.at("items").get>(); +} + +inline void from_json(const json &j, VideoCategorySnippet &s) +{ + s.channelId = j.at("channelId").get(); + s.title = j.at("title").get(); + s.assignable = j.at("assignable").get(); +} + +inline void from_json(const json &j, VideoCategory &s) +{ + s.id = j.at("id").get(); + s.snippet = j.at("snippet").get(); +} + +inline void from_json(const json &j, VideoCategoryListResponse &s) +{ + s.items = j.at("items").get>(); +} + +inline void from_json(const json &j, LiveBroadcastSnippet &s) +{ + s.title = j.at("title").get(); + s.description = j.at("description").get(); + s.scheduledStartTime = + get_stack_optional(j, "scheduledStartTime"); + s.scheduledEndTime = + get_stack_optional(j, "scheduledEndTime"); + s.liveChatId = get_stack_optional(j, "liveChatId"); +} + +inline void to_json(json &j, const LiveBroadcastSnippet &s) +{ + j = json::object(); + j["title"] = s.title; + j["description"] = s.description; + j["scheduledStartTime"] = s.scheduledStartTime; + j["scheduledEndTime"] = s.scheduledEndTime; +} + +inline void from_json(const json &j, LiveBroadcastLifeCycleStatus &e) +{ + if (j == "complete") + e = LiveBroadcastLifeCycleStatus::COMPLETE; + else if (j == "created") + e = LiveBroadcastLifeCycleStatus::CREATED; + else if (j == "live") + e = LiveBroadcastLifeCycleStatus::LIVE; + else if (j == "liveStarting") + e = LiveBroadcastLifeCycleStatus::LIVE_STARTING; + else if (j == "ready") + e = LiveBroadcastLifeCycleStatus::READY; + else if (j == "revoked") + e = LiveBroadcastLifeCycleStatus::REVOKED; + else if (j == "testStarting") + e = LiveBroadcastLifeCycleStatus::TEST_STARTING; + else if (j == "testing") + e = LiveBroadcastLifeCycleStatus::TESTING; + else { + throw std::runtime_error("Unknown \"lifeCycleStatus\" value"); + } +} + +inline void from_json(const json &j, LiveBroadcastPrivacyStatus &e) +{ + if (j == "private") + e = LiveBroadcastPrivacyStatus::PRIVATE; + else if (j == "public") + e = LiveBroadcastPrivacyStatus::PUBLIC; + else if (j == "unlisted") + e = LiveBroadcastPrivacyStatus::UNLISTED; + else { + throw std::runtime_error("Unknown \"privacyStatus\" value"); + } +} + +inline void to_json(json &j, const LiveBroadcastPrivacyStatus &e) +{ + switch (e) { + case LiveBroadcastPrivacyStatus::PRIVATE: + j = "private"; + break; + case LiveBroadcastPrivacyStatus::PUBLIC: + j = "public"; + break; + case LiveBroadcastPrivacyStatus::UNLISTED: + j = "unlisted"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveBroadcastStatus &s) +{ + s.lifeCycleStatus = + j.at("lifeCycleStatus").get(); + s.privacyStatus = + j.at("privacyStatus").get(); + s.selfDeclaredMadeForKids = j.at("selfDeclaredMadeForKids").get(); +} + +inline void to_json(json &j, const LiveBroadcastStatus &s) +{ + j = json::object(); + j["privacyStatus"] = s.privacyStatus; + j["selfDeclaredMadeForKids"] = s.selfDeclaredMadeForKids; +} + +inline void from_json(const json &j, LiveBroadcastMonitorStream &s) +{ + s.enableMonitorStream = j.at("enableMonitorStream").get(); + s.broadcastStreamDelayMs = + get_stack_optional(j, "broadcastStreamDelayMs"); +} + +inline void to_json(json &j, const LiveBroadcastMonitorStream &s) +{ + j = json::object(); + j["enableMonitorStream"] = s.enableMonitorStream; + j["broadcastStreamDelayMs"] = s.broadcastStreamDelayMs; +} + +inline void from_json(const json &j, LiveBroadcastLatencyPreference &e) +{ + if (j == "normal") + e = LiveBroadcastLatencyPreference::NORMAL; + else if (j == "low") + e = LiveBroadcastLatencyPreference::LOW; + else if (j == "ultraLow") + e = LiveBroadcastLatencyPreference::ULTRALOW; + else { + throw std::runtime_error("Unknown \"latencyPreference\" value"); + } +} + +inline void to_json(json &j, const LiveBroadcastLatencyPreference &e) +{ + switch (e) { + case LiveBroadcastLatencyPreference::NORMAL: + j = "normal"; + break; + case LiveBroadcastLatencyPreference::LOW: + j = "low"; + break; + case LiveBroadcastLatencyPreference::ULTRALOW: + j = "ultraLow"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveBroadcastClosedCaptionsType &e) +{ + if (j == "closedCaptionsDisabled") + e = LiveBroadcastClosedCaptionsType::DISABLED; + else if (j == "closedCaptionsHttpPost") + e = LiveBroadcastClosedCaptionsType::HTTP_POST; + else if (j == "closedCaptionsEmbedded") + e = LiveBroadcastClosedCaptionsType::EMBEDDED; + else { + throw std::runtime_error( + "Unknown \"closedCaptionsType\" value"); + } +} + +inline void to_json(json &j, const LiveBroadcastClosedCaptionsType &e) +{ + switch (e) { + case LiveBroadcastClosedCaptionsType::DISABLED: + j = "closedCaptionsDisabled"; + break; + case LiveBroadcastClosedCaptionsType::HTTP_POST: + j = "closedCaptionsHttpPost"; + break; + case LiveBroadcastClosedCaptionsType::EMBEDDED: + j = "closedCaptionsEmbedded"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveBroadcastProjection &e) +{ + if (j == "rectangular") + e = LiveBroadcastProjection::RECTANGULAR; + else if (j == "360") + e = LiveBroadcastProjection::THREE_HUNDRED_SIXTY; + else { + throw std::runtime_error("Unknown \"latencyPreference\" value"); + } +} + +inline void to_json(json &j, const LiveBroadcastProjection &e) +{ + switch (e) { + case LiveBroadcastProjection::RECTANGULAR: + j = "rectangular"; + break; + case LiveBroadcastProjection::THREE_HUNDRED_SIXTY: + j = "360"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveBroadcastContentDetails &s) +{ + s.boundStreamId = get_stack_optional(j, "boundStreamId"); + s.monitorStream = + j.at("monitorStream").get(); + s.enableEmbed = get_stack_optional(j, "enableEmbed"); + s.enableDvr = j.at("enableDvr").get(); + s.recordFromStart = get_stack_optional(j, "recordFromStart"); + s.closedCaptionsType = + get_stack_optional( + j, "closedCaptionsType"); + s.projection = j.at("projection").get(); + s.latencyPreference = + j.at("latencyPreference").get(); + s.enableAutoStart = j.at("enableAutoStart").get(); + s.enableAutoStop = j.at("enableAutoStop").get(); +} + +inline void to_json(json &j, const LiveBroadcastContentDetails &s) +{ + j = json::object(); + j["monitorStream"] = s.monitorStream; + j["enableEmbed"] = s.enableEmbed; + j["enableDvr"] = s.enableDvr; + j["recordFromStart"] = s.recordFromStart; + j["closedCaptionsType"] = s.closedCaptionsType; + j["projection"] = s.projection; + j["latencyPreference"] = s.latencyPreference; + j["enableAutoStart"] = s.enableAutoStart; + j["enableAutoStop"] = s.enableAutoStop; +} + +inline void from_json(const json &j, LiveBroadcast &s) +{ + s.id = j.at("id").get(); + s.snippet = j.at("snippet").get(); + s.status = j.at("status").get(); + s.contentDetails = + j.at("contentDetails").get(); +} + +inline void to_json(json &j, const LiveBroadcast &s) +{ + j = json::object(); + j["id"] = s.id; + j["snippet"] = s.snippet; + j["status"] = s.status; + j["contentDetails"] = s.contentDetails; +} + +inline void from_json(const json &j, LiveBroadcastListResponse &s) +{ + s.nextPageToken = get_stack_optional(j, "nextPageToken"); + s.items = j.at("items").get>(); +} + +inline void from_json(const json &j, LiveStreamSnippet &s) +{ + s.title = j.at("title").get(); +} + +inline void to_json(json &j, const LiveStreamSnippet &s) +{ + j = json::object(); + j["title"] = s.title; +} + +inline void from_json(const json &j, LiveStreamCdnIngestionType &e) +{ + if (j == "dash") + e = LiveStreamCdnIngestionType::DASH; + else if (j == "hls") + e = LiveStreamCdnIngestionType::HLS; + else if (j == "rtmp") + e = LiveStreamCdnIngestionType::RTMP; + else { + throw std::runtime_error("Unknown \"ingestionType\" value"); + } +} + +inline void to_json(json &j, const LiveStreamCdnIngestionType &e) +{ + switch (e) { + case LiveStreamCdnIngestionType::DASH: + j = "dash"; + break; + case LiveStreamCdnIngestionType::HLS: + j = "hls"; + break; + case LiveStreamCdnIngestionType::RTMP: + j = "rtmp"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveStreamCdnIngestionInfo &s) +{ + s.streamName = j.at("streamName").get(); +} + +inline void from_json(const json &j, LiveStreamCdnResolution &e) +{ + if (j == "variable") + e = LiveStreamCdnResolution::RESOLUTION_VARIABLE; + else if (j == "240p") + e = LiveStreamCdnResolution::RESOLUTION_240P; + else if (j == "360p") + e = LiveStreamCdnResolution::RESOLUTION_360P; + else if (j == "480p") + e = LiveStreamCdnResolution::RESOLUTION_480P; + else if (j == "720p") + e = LiveStreamCdnResolution::RESOLUTION_720P; + else if (j == "1080p") + e = LiveStreamCdnResolution::RESOLUTION_1080P; + else if (j == "1440p") + e = LiveStreamCdnResolution::RESOLUTION_1440P; + else if (j == "2160p") + e = LiveStreamCdnResolution::RESOLUTION_2160P; + else { + throw std::runtime_error("Unknown \"resolution\" value"); + } +} + +inline void to_json(json &j, const LiveStreamCdnResolution &e) +{ + switch (e) { + case LiveStreamCdnResolution::RESOLUTION_VARIABLE: + j = "variable"; + break; + case LiveStreamCdnResolution::RESOLUTION_240P: + j = "240p"; + break; + case LiveStreamCdnResolution::RESOLUTION_360P: + j = "360p"; + break; + case LiveStreamCdnResolution::RESOLUTION_480P: + j = "480p"; + break; + case LiveStreamCdnResolution::RESOLUTION_720P: + j = "720p"; + break; + case LiveStreamCdnResolution::RESOLUTION_1080P: + j = "1080p"; + break; + case LiveStreamCdnResolution::RESOLUTION_1440P: + j = "1440p"; + break; + case LiveStreamCdnResolution::RESOLUTION_2160P: + j = "2160p"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveStreamCdnFrameRate &e) +{ + if (j == "variable") + e = LiveStreamCdnFrameRate::FRAMERATE_VARIABLE; + else if (j == "30fps") + e = LiveStreamCdnFrameRate::FRAMERATE_30FPS; + else if (j == "60fps") + e = LiveStreamCdnFrameRate::FRAMERATE_60FPS; + else { + throw std::runtime_error("Unknown \"frameRate\" value"); + } +} + +inline void to_json(json &j, const LiveStreamCdnFrameRate &e) +{ + switch (e) { + case LiveStreamCdnFrameRate::FRAMERATE_VARIABLE: + j = "variable"; + break; + case LiveStreamCdnFrameRate::FRAMERATE_30FPS: + j = "30fps"; + break; + case LiveStreamCdnFrameRate::FRAMERATE_60FPS: + j = "60fps"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveStreamCdn &s) +{ + s.ingestionType = + j.at("ingestionType").get(); + s.ingestionInfo = + j.at("ingestionInfo").get(); + s.resolution = j.at("resolution").get(); + s.frameRate = j.at("frameRate").get(); +} + +inline void to_json(json &j, const LiveStreamCdn &s) +{ + j = json::object(); + j["ingestionType"] = s.ingestionType; + j["resolution"] = s.resolution; + j["frameRate"] = s.frameRate; +} + +inline void from_json(const json &j, LiveStreamStatusEnum &e) +{ + if (j == "active") + e = LiveStreamStatusEnum::ACTIVE; + else if (j == "created") + e = LiveStreamStatusEnum::CREATED; + else if (j == "error") + e = LiveStreamStatusEnum::ERR0R; + else if (j == "inactive") + e = LiveStreamStatusEnum::INACTIVE; + else if (j == "ready") + e = LiveStreamStatusEnum::READY; + else { + throw std::runtime_error("Unknown \"streamStatus\" value"); + } +} + +inline void from_json(const json &j, LiveStreamStatus &s) +{ + s.streamStatus = j.at("streamStatus").get(); +} + +inline void from_json(const json &j, LiveStreamContentDetails &s) +{ + s.isReusable = j.at("isReusable").get(); +} + +inline void to_json(json &j, const LiveStreamContentDetails &s) +{ + j = json::object(); + j["isReusable"] = s.isReusable; +} + +inline void from_json(const json &j, LiveStream &s) +{ + s.id = j.at("id").get(); + s.snippet = j.at("snippet").get(); + s.cdn = j.at("cdn").get(); + s.status = j.at("status").get(); + s.contentDetails = get_stack_optional( + j, "contentDetails"); +} + +inline void to_json(json &j, const LiveStream &s) +{ + j = json::object(); + j["snippet"] = s.snippet; + j["cdn"] = s.cdn; + j["contentDetails"] = s.contentDetails; +} + +inline void from_json(const json &j, LiveStreamListResponse &s) +{ + s.items = j.at("items").get>(); +} + +inline void from_json(const json &j, VideoSnippet &s) +{ + s.title = j.at("title").get(); + s.description = j.at("description").get(); + s.categoryId = j.at("categoryId").get(); +} + +inline void to_json(json &j, const VideoSnippet &s) +{ + j = json::object(); + j["title"] = s.title; + j["description"] = s.description; + j["categoryId"] = s.categoryId; +} + +inline void from_json(const json &j, Video &s) +{ + s.id = j.at("id").get(); + s.snippet = j.at("snippet").get(); +} + +inline void to_json(json &j, const Video &s) +{ + j = json::object(); + j["id"] = s.id; + j["snippet"] = s.snippet; +} + +inline void from_json(const json &j, LiveChatTextMessageType &e) +{ + if (j == "chatEndedEvent") + e = LiveChatTextMessageType::CHAT_ENDED_EVENT; + else if (j == "messageDeletedEvent") + e = LiveChatTextMessageType::MESSAGE_DELETED_EVENT; + else if (j == "sponsorOnlyModeEndedEvent") + e = LiveChatTextMessageType::SPONSOR_ONLY_MODE_ENDED_EVENT; + else if (j == "sponsorOnlyModeStartedEvent") + e = LiveChatTextMessageType::SPONSOR_ONLY_MODE_STARTED_EVENT; + else if (j == "newSponsorEvent") + e = LiveChatTextMessageType::NEW_SPONSOR_EVENT; + else if (j == "memberMilestoneChatEvent") + e = LiveChatTextMessageType::MEMBER_MILESTONE_CHAT_EVENT; + else if (j == "superChatEvent") + e = LiveChatTextMessageType::SUPER_CHAT_EVENT; + else if (j == "superStickerEvent") + e = LiveChatTextMessageType::SUPER_STICKER_EVENT; + else if (j == "textMessageEvent") + e = LiveChatTextMessageType::TEXT_MESSAGE_EVENT; + else if (j == "tombstone") + e = LiveChatTextMessageType::TOMBSTONE; + else if (j == "userBannedEvent") + e = LiveChatTextMessageType::USER_BANNED_EVENT; + else if (j == "membershipGiftingEvent") + e = LiveChatTextMessageType::MEMBERSHIP_GIFTING_EVENT; + else if (j == "giftMembershipReceivedEvent") + e = LiveChatTextMessageType::GIFT_MEMBERSHIP_RECEIVED_EVENT; + else { + throw std::runtime_error("Unknown \"type\" value"); + } +} +inline void to_json(json &j, const LiveChatTextMessageType &e) +{ + switch (e) { + case LiveChatTextMessageType::CHAT_ENDED_EVENT: + j = "chatEndedEvent"; + break; + case LiveChatTextMessageType::MESSAGE_DELETED_EVENT: + j = "messageDeletedEvent"; + break; + case LiveChatTextMessageType::SPONSOR_ONLY_MODE_ENDED_EVENT: + j = "sponsorOnlyModeEndedEvent"; + break; + case LiveChatTextMessageType::SPONSOR_ONLY_MODE_STARTED_EVENT: + j = "sponsorOnlyModeStartedEvent"; + break; + case LiveChatTextMessageType::NEW_SPONSOR_EVENT: + j = "newSponsorEvent"; + break; + case LiveChatTextMessageType::MEMBER_MILESTONE_CHAT_EVENT: + j = "memberMilestoneChatEvent"; + break; + case LiveChatTextMessageType::SUPER_CHAT_EVENT: + j = "superChatEvent"; + break; + case LiveChatTextMessageType::SUPER_STICKER_EVENT: + j = "superStickerEvent"; + break; + case LiveChatTextMessageType::TEXT_MESSAGE_EVENT: + j = "textMessageEvent"; + break; + case LiveChatTextMessageType::TOMBSTONE: + j = "tombstone"; + break; + case LiveChatTextMessageType::USER_BANNED_EVENT: + j = "userBannedEvent"; + break; + case LiveChatTextMessageType::MEMBERSHIP_GIFTING_EVENT: + j = "membershipGiftingEvent"; + break; + case LiveChatTextMessageType::GIFT_MEMBERSHIP_RECEIVED_EVENT: + j = "giftMembershipReceivedEvent"; + break; + default: + throw std::runtime_error("This should not happen"); + } +} + +inline void from_json(const json &j, LiveChatTextMessageDetails &s) +{ + s.messageText = j.at("messageText").get(); +} + +inline void to_json(json &j, const LiveChatTextMessageDetails &s) +{ + j = json::object(); + j["messageText"] = s.messageText; +} + +inline void from_json(const json &j, LiveChatMessageSnippet &s) +{ + s.type = j.at("type").get(); + s.liveChatId = j.at("liveChatId").get(); + s.textMessageDetails = j.at("textMessageDetails"); +} + +inline void to_json(json &j, const LiveChatMessageSnippet &s) +{ + j = json::object(); + j["type"] = s.type; + j["liveChatId"] = s.liveChatId; + j["textMessageDetails"] = s.textMessageDetails; +} + +inline void from_json(const json &j, LiveChatMessage &s) +{ + s.snippet = j.at("snippet").get(); +} + +inline void to_json(json &j, const LiveChatMessage &s) +{ + j = json::object(); + j["snippet"] = s.snippet; +} + +} diff --git a/plugins/obs-youtube/youtube-chat.cpp b/plugins/obs-youtube/youtube-chat.cpp new file mode 100644 index 00000000000000..b133e794cee0ad --- /dev/null +++ b/plugins/obs-youtube/youtube-chat.cpp @@ -0,0 +1,182 @@ +#include "youtube-chat.hpp" + +#include +#include + +#include +#include +#include +#include + +constexpr const char *CHAT_PLACEHOLDER_URL = + "https://obsproject.com/placeholders/youtube-chat"; +constexpr const char *CHAT_POPOUT_URL = + "https://www.youtube.com/live_chat?is_popout=1&dark_theme=1&v="; + +constexpr const char *CHAT_SCRIPT = "\ +const obsCSS = document.createElement('style');\ +obsCSS.innerHTML = \"#panel-pages.yt-live-chat-renderer {display: none;}\ +yt-live-chat-viewer-engagement-message-renderer {display: none;}\";\ +document.querySelector('head').appendChild(obsCSS);"; + +static QString GetTranslatedError(const RequestError &error) +{ + if (error.type != RequestErrorType::UNKNOWN_OR_CUSTOM || + error.error.empty()) + return QString::fromStdString(error.message); + + std::string lookupString = "YouTube.Errors."; + lookupString += error.error; + + return QT_UTF8(obs_module_text(lookupString.c_str())); +} + +YoutubeChat::YoutubeChat(YouTubeApi::ServiceOAuth *api) + : apiYouTube(api), + url(CHAT_PLACEHOLDER_URL), + QWidget() +{ + + resize(300, 600); + setMinimumSize(200, 300); + + lineEdit = new LineEditAutoResize(); + lineEdit->setVisible(false); + lineEdit->setMaxLength(200); + lineEdit->setPlaceholderText( + obs_module_text("YouTube.Chat.Input.Placeholder")); + sendButton = + new QPushButton(obs_module_text("YouTube.Chat.Input.Send")); + sendButton->setVisible(false); + + chatLayout = new QHBoxLayout(); + chatLayout->setContentsMargins(0, 0, 0, 0); + chatLayout->addWidget(lineEdit, 1); + chatLayout->addWidget(sendButton); + + QVBoxLayout *layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + layout->addLayout(chatLayout); + + setLayout(layout); + + connect(lineEdit, SIGNAL(returnPressed()), this, + SLOT(SendChatMessage())); + connect(sendButton, SIGNAL(pressed()), this, SLOT(SendChatMessage())); +} + +YoutubeChat::~YoutubeChat() +{ + if (!sendingMessage.isNull() && sendingMessage->isRunning()) + sendingMessage->wait(); +} + +void YoutubeChat::SetChatIds(const std::string &broadcastId, + const std::string &chatId_) +{ + url = CHAT_POPOUT_URL; + url += broadcastId; + chatId = chatId_; + + if (!cefWidget.isNull()) + QMetaObject::invokeMethod(this, "UpdateCefWidget", + Qt::QueuedConnection); + + QMetaObject::invokeMethod(this, "EnableChatInput", Qt::QueuedConnection, + Q_ARG(bool, true)); +} + +void YoutubeChat::ResetIds() +{ + url = CHAT_PLACEHOLDER_URL; + chatId.clear(); + + if (!cefWidget.isNull()) + QMetaObject::invokeMethod(this, "UpdateCefWidget", + Qt::QueuedConnection); + + QMetaObject::invokeMethod(this, "EnableChatInput", Qt::QueuedConnection, + Q_ARG(bool, false)); +} + +void YoutubeChat::showEvent(QShowEvent *event) +{ + UpdateCefWidget(); + + QWidget::showEvent(event); +} + +void YoutubeChat::hideEvent(QHideEvent *event) +{ + cefWidget.reset(nullptr); + QWidget::hideEvent(event); +} + +void YoutubeChat::SendChatMessage() +{ + if (lineEdit->text().isEmpty()) + return; + + this->lineEdit->setEnabled(false); + this->sendButton->setEnabled(false); + + sendingMessage.reset(new QuickThread([&]() { + YouTubeApi::LiveChatMessage chatMessage; + chatMessage.snippet.type = + YouTubeApi::LiveChatTextMessageType::TEXT_MESSAGE_EVENT; + chatMessage.snippet.liveChatId = chatId; + chatMessage.snippet.textMessageDetails.messageText = + lineEdit->text().toStdString(); + + lineEdit->setText(""); + lineEdit->setPlaceholderText( + obs_module_text("YouTube.Chat.Input.Sending")); + + if (apiYouTube->InsertLiveChatMessage(chatMessage)) { + os_sleep_ms(3000); + } else { + QString error = + GetTranslatedError(apiYouTube->GetLastError()); + QMetaObject::invokeMethod( + this, "ShowErrorMessage", Qt::QueuedConnection, + Q_ARG(const QString &, error)); + } + lineEdit->setPlaceholderText( + obs_module_text("YouTube.Chat.Input.Placeholder")); + })); + + connect(sendingMessage.get(), &QThread::finished, this, [=]() { + this->lineEdit->setEnabled(true); + this->sendButton->setEnabled(true); + }); + + sendingMessage->start(); +} + +void YoutubeChat::ShowErrorMessage(const QString &error) +{ + QMessageBox::warning( + this, obs_module_text("YouTube.Chat.Error.Title"), + QString::fromUtf8(obs_module_text("YouTube.Chat.Error.Text"), + -1) + .arg(error)); +} + +void YoutubeChat::UpdateCefWidget() +{ + obs_frontend_browser_params params = {0}; + + params.url = url.c_str(); + params.startup_script = CHAT_SCRIPT; + + cefWidget.reset((QWidget *)obs_frontend_get_browser_widget(¶ms)); + + QVBoxLayout *layout = (QVBoxLayout *)this->layout(); + layout->insertWidget(0, cefWidget.get(), 1); +} + +void YoutubeChat::EnableChatInput(bool enable) +{ + lineEdit->setVisible(enable); + sendButton->setVisible(enable); +} diff --git a/plugins/obs-youtube/youtube-chat.hpp b/plugins/obs-youtube/youtube-chat.hpp new file mode 100644 index 00000000000000..e3eb866d9e2c50 --- /dev/null +++ b/plugins/obs-youtube/youtube-chat.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "lineedit-autoresize.hpp" +#include "youtube-oauth.hpp" + +class YoutubeChat : public QWidget { + Q_OBJECT + + YouTubeApi::ServiceOAuth *apiYouTube; + QScopedPointer cefWidget; + + LineEditAutoResize *lineEdit; + QPushButton *sendButton; + QHBoxLayout *chatLayout; + + std::string url; + std::string chatId; + + QScopedPointer sendingMessage; + +public: + YoutubeChat(YouTubeApi::ServiceOAuth *api); + ~YoutubeChat(); + void SetChatIds(const std::string &broadcastId, + const std::string &chatId); + void ResetIds(); + + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; +private slots: + void SendChatMessage(); + void ShowErrorMessage(const QString &error); + void UpdateCefWidget(); + void EnableChatInput(bool enable); +}; diff --git a/plugins/obs-youtube/youtube-config.cpp b/plugins/obs-youtube/youtube-config.cpp index bb30d97e374524..26277dc1f7de88 100644 --- a/plugins/obs-youtube/youtube-config.cpp +++ b/plugins/obs-youtube/youtube-config.cpp @@ -4,6 +4,10 @@ #include "youtube-config.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + struct YouTubeIngest { const char *rtmp; const char *rtmps; @@ -25,8 +29,24 @@ constexpr YouTubeIngest BACKUP_INGESTS = { "rtmps://b.rtmps.youtube.com:443/live2?backup=1", "https://b.upload.youtube.com/http_upload_hls?cid={stream_key}©=1&file=out.m3u8"}; -YouTubeConfig::YouTubeConfig(obs_data_t *settings, obs_service_t * /* self */) +YouTubeConfig::YouTubeConfig(obs_data_t *settings, obs_service_t *self) +#ifdef OAUTH_ENABLED + : serviceObj(self), + typeData(reinterpret_cast( + obs_service_get_type_data(self))) +#endif { +#ifdef OAUTH_ENABLED + if (!obs_data_has_user_value(settings, "uuid")) { + BPtr newUuid = os_generate_uuid(); + obs_data_set_string(settings, "uuid", newUuid); + } + + uuid = obs_data_get_string(settings, "uuid"); +#else + UNUSED_PARAMETER(self); +#endif + Update(settings); } @@ -35,13 +55,44 @@ void YouTubeConfig::Update(obs_data_t *settings) protocol = obs_data_get_string(settings, "protocol"); serverUrl = obs_data_get_string(settings, "server"); +#ifdef OAUTH_ENABLED + std::string newUuid = obs_data_get_string(settings, "uuid"); + + if (newUuid != uuid) { + if (oauth) { + typeData->ReleaseOAuth(uuid, serviceObj); + oauth = nullptr; + } + + uuid = newUuid; + } + if (!oauth) + oauth = typeData->GetOAuth(uuid, serviceObj); + + oauth->SetIngestionType( + protocol == "HLS" + ? YouTubeApi::LiveStreamCdnIngestionType::HLS + : YouTubeApi::LiveStreamCdnIngestionType::RTMP); + + if (!oauth->Connected()) { + streamKey = obs_data_get_string(settings, "stream_key"); + } +#else streamKey = obs_data_get_string(settings, "stream_key"); +#endif +} + +YouTubeConfig::~YouTubeConfig() +{ +#ifdef OAUTH_ENABLED + typeData->ReleaseOAuth(uuid, serviceObj); +#endif } void YouTubeConfig::InfoGetDefault(obs_data_t *settings) { - obs_data_set_default_string(settings, "protocol", "RTMPS"); - obs_data_set_default_string(settings, "server", PRIMARY_INGESTS.rtmps); + obs_data_set_string(settings, "protocol", "RTMPS"); + obs_data_set_string(settings, "server", PRIMARY_INGESTS.rtmps); } const char *YouTubeConfig::ConnectInfo(uint32_t type) @@ -51,9 +102,19 @@ const char *YouTubeConfig::ConnectInfo(uint32_t type) if (!serverUrl.empty()) return serverUrl.c_str(); break; - case OBS_SERVICE_CONNECT_INFO_STREAM_ID: + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: +#ifdef OAUTH_ENABLED + if (oauth->Connected()) { + const char *streamKey = oauth->GetStreamKey(); + if (streamKey) + return streamKey; + } else if (!streamKey.empty()) { + return streamKey.c_str(); + } +#else if (!streamKey.empty()) return streamKey.c_str(); +#endif break; default: break; @@ -64,8 +125,16 @@ const char *YouTubeConfig::ConnectInfo(uint32_t type) bool YouTubeConfig::CanTryToConnect() { +#ifdef OAUTH_ENABLED + if (serverUrl.empty()) + return false; + + return oauth->Connected() ? !!oauth->GetStreamKey() + : !streamKey.empty(); +#else if (serverUrl.empty() || streamKey.empty()) return false; +#endif return true; } @@ -73,7 +142,6 @@ bool YouTubeConfig::CanTryToConnect() static bool ModifiedProtocolCb(obs_properties_t *props, obs_property_t *, obs_data_t *settings) { - blog(LOG_DEBUG, "ModifiedProtocolCb"); std::string protocol = obs_data_get_string(settings, "protocol"); obs_property_t *p = obs_properties_get(props, "server"); @@ -103,15 +171,112 @@ static bool ModifiedProtocolCb(obs_properties_t *props, obs_property_t *, return true; } +#ifdef OAUTH_ENABLED + +static inline void AddChannelName(obs_properties_t *props, + YouTubeApi::ServiceOAuth *oauth) +{ + YouTubeApi::ChannelInfo info; + if (oauth->GetChannelInfo(info)) { + std::string channelTitle = ""; + channelTitle += info.title; + channelTitle += ""; + obs_property_t *p = + obs_properties_get(props, "connected_account"); + obs_property_set_long_description(p, channelTitle.c_str()); + obs_property_set_visible(p, true); + } +} + +static bool ConnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + YouTubeApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->Login()) + return false; + + AddChannelName(props, oauth); + + obs_property_set_visible(obs_properties_get(props, "connect"), false); + obs_property_set_visible(obs_properties_get(props, "disconnect"), true); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_enabled(obs_properties_get(props, "protocol"), false); + + return true; +} + +static bool DisconnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + YouTubeApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->SignOut()) + return false; + + obs_property_set_visible(obs_properties_get(props, "connect"), true); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + true); + obs_property_set_visible(obs_properties_get(props, "connected_account"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_enabled(obs_properties_get(props, "protocol"), true); + + return true; +} + +static bool UseStreamKeyCb(obs_properties_t *props, obs_property_t *, void *) +{ + obs_property_set_visible(obs_properties_get(props, "connect"), true); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "connected_account"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), true); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + true); + + return true; +} +#endif + obs_properties_t *YouTubeConfig::GetProperties() { obs_properties_t *ppts = obs_properties_create(); obs_property_t *p; +#ifdef OAUTH_ENABLED + bool connected = oauth->Connected(); + p = obs_properties_add_text( + ppts, "connected_account", + obs_module_text("YouTube.Auth.ConnectedAccount"), + OBS_TEXT_INFO); + obs_property_set_visible(p, false); + if (connected) + AddChannelName(ppts, oauth); +#endif + p = obs_properties_add_list(ppts, "protocol", obs_module_text("YouTube.Protocol"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); +#ifdef OAUTH_ENABLED + obs_property_set_long_description( + p, obs_module_text("YouTube.Auth.LockedProtocol")); + obs_property_set_enabled(p, !connected); +#endif if (obs_is_output_protocol_registered("RTMPS")) obs_property_list_add_string(p, "RTMPS", "RTMPS"); @@ -134,9 +299,12 @@ obs_properties_t *YouTubeConfig::GetProperties() obs_module_text("YouTube.Server"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); - obs_properties_add_text(ppts, "stream_key", - obs_module_text("YouTube.StreamKey"), - OBS_TEXT_PASSWORD); + p = obs_properties_add_text(ppts, "stream_key", + obs_module_text("YouTube.StreamKey"), + OBS_TEXT_PASSWORD); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); +#endif p = obs_properties_add_button(ppts, "get_stream_key", obs_module_text("YouTube.GetStreamKey"), @@ -144,5 +312,34 @@ obs_properties_t *YouTubeConfig::GetProperties() obs_property_button_set_type(p, OBS_BUTTON_URL); obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); + + p = obs_properties_add_button2(ppts, "connect", + obs_module_text("YouTube.Auth.Connect"), + ConnectCb, oauth); + obs_property_set_visible(p, !connected); + + p = obs_properties_add_button2( + ppts, "disconnect", obs_module_text("YouTube.Auth.Disconnect"), + DisconnectCb, oauth); + obs_property_set_visible(p, connected); + + p = obs_properties_add_button(ppts, "use_stream_key", + obs_module_text("YouTube.UseStreamKey"), + UseStreamKeyCb); + obs_property_set_visible(p, streamKey.empty() && !connected); + + obs_properties_add_text( + ppts, "legal_links", + "
" + "YouTube Terms of Service
" + "" + "Google Privacy Policy
" + "" + "Google Third-Party Permissions", + OBS_TEXT_INFO); +#endif + return ppts; } diff --git a/plugins/obs-youtube/youtube-config.hpp b/plugins/obs-youtube/youtube-config.hpp index de41698d0eb050..0731463c3e1be5 100644 --- a/plugins/obs-youtube/youtube-config.hpp +++ b/plugins/obs-youtube/youtube-config.hpp @@ -7,7 +7,19 @@ #include #include +#ifdef OAUTH_ENABLED +#include "youtube-service.hpp" +#endif + class YouTubeConfig { +#ifdef OAUTH_ENABLED + obs_service_t *serviceObj; + YouTubeService *typeData; + + std::string uuid; + YouTubeApi::ServiceOAuth *oauth = nullptr; +#endif + std::string protocol; std::string serverUrl; @@ -15,7 +27,7 @@ class YouTubeConfig { public: YouTubeConfig(obs_data_t *settings, obs_service_t *self); - inline ~YouTubeConfig(){}; + ~YouTubeConfig(); void Update(obs_data_t *settings); diff --git a/plugins/obs-youtube/youtube-oauth.cpp b/plugins/obs-youtube/youtube-oauth.cpp new file mode 100644 index 00000000000000..b3163aab843e56 --- /dev/null +++ b/plugins/obs-youtube/youtube-oauth.cpp @@ -0,0 +1,1180 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "youtube-oauth.hpp" + +#include + +#include +#include +#include + +#include "window-youtube-actions.hpp" +#include "youtube-chat.hpp" + +constexpr const char *AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; +constexpr const char *TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token"; +constexpr const char *API_URL = "https://www.googleapis.com/youtube/v3"; +constexpr const char *LIVE_BROADCASTS_API_ENDPOINT = "/liveBroadcasts"; +constexpr const char *LIVE_STREAMS_API_ENDPOINT = "/liveStreams"; + +constexpr const char *CLIENT_ID = (const char *)YOUTUBE_CLIENTID; +constexpr uint64_t CLIENT_ID_HASH = YOUTUBE_CLIENTID_HASH; + +constexpr const char *CLIENT_SECRET = (const char *)YOUTUBE_SECRET; +constexpr uint64_t CLIENT_SECRET_HASH = YOUTUBE_SECRET_HASH; + +constexpr const char *SCOPE = "https://www.googleapis.com/auth/youtube"; +constexpr int64_t SCOPE_VERSION = 1; + +constexpr const char *CHAT_DOCK_NAME = "ytChat"; + +namespace YouTubeApi { + +ServiceOAuth::ServiceOAuth() : OAuth::ServiceBase() +{ + broadcastFlow.priv = this; + broadcastFlow.flags = + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_START | + OBS_BROADCAST_FLOW_ALLOW_DIFFERED_BROADCAST_STOP; + broadcastFlow.get_broadcast_state = GetBroadcastState; + broadcastFlow.get_broadcast_start_type = GetBroadcastStartType; + broadcastFlow.get_broadcast_stop_type = GetBroadcastStopType; + broadcastFlow.manage_broadcast = ManageBroadcast; + broadcastFlow.stopped_streaming = StoppedStreaming; + broadcastFlow.differed_start_broadcast = DifferedStartBroadcast; + broadcastFlow.is_broadcast_stream_active = IsBroadcastStreamActive; + broadcastFlow.differed_stop_broadcast = DifferedStopBroadcast; + broadcastFlow.get_last_error = GetBroadcastsLastError; + + broadcastState = OBS_BROADCAST_NONE; + broadcastStartType = OBS_BROADCAST_START_WITH_STREAM; + broadcastStopType = OBS_BROADCAST_STOP_WITH_STREAM; +} + +std::string ServiceOAuth::UserAgent() +{ + std::string userAgent("obs-youtube "); + userAgent += obs_get_version_string(); + + return userAgent; +} + +const char *ServiceOAuth::TokenUrl() +{ + return TOKEN_URL; +} + +std::string ServiceOAuth::ClientId() +{ + std::string clientId = CLIENT_ID; + deobfuscate_str(&clientId[0], CLIENT_ID_HASH); + + return clientId; +} + +std::string ServiceOAuth::ClientSecret() +{ + std::string clientSecret = CLIENT_SECRET; + deobfuscate_str(&clientSecret[0], CLIENT_SECRET_HASH); + + return clientSecret; +} + +int64_t ServiceOAuth::ScopeVersion() +{ + return SCOPE_VERSION; +} + +static inline void WarningBox(const QString &title, const QString &text, + QWidget *parent) +{ + QMessageBox warn(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *ok = warn.addButton(QMessageBox::Ok); + ok->setText(ok->tr("Ok")); + warn.exec(); +} + +static int LoginConfirmation(const OAuth::LoginReason &reason, QWidget *parent) +{ + const char *reasonText = nullptr; + switch (reason) { + case OAuth::LoginReason::CONNECT: + return QMessageBox::Ok; + case OAuth::LoginReason::SCOPE_CHANGE: + reasonText = obs_module_text( + "YouTube.Auth.ReLoginDialog.ScopeChange"); + break; + case OAuth::LoginReason::REFRESH_TOKEN_FAILED: + reasonText = obs_module_text( + "YouTube.Auth.ReLoginDialog.RefreshTokenFailed"); + break; + case OAuth::LoginReason::PROFILE_DUPLICATION: + reasonText = obs_module_text( + "YouTube.Auth.ReLoginDialog.ProfileDuplication"); + break; + } + + QString title = QString::fromUtf8( + obs_module_text("YouTube.Auth.ReLoginDialog.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("YouTube.Auth.ReLoginDialog.Text")) + .arg(reasonText); + + QMessageBox ques(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + return ques.exec(); +} + +bool ServiceOAuth::LoginInternal(const OAuth::LoginReason &reason, + std::string &code, std::string &redirectUri) +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + if (reason != OAuth::LoginReason::CONNECT && + LoginConfirmation(reason, parent) != QMessageBox::Ok) { + return false; + } + + LocalRedirect login(AUTH_URL, ClientId().c_str(), SCOPE, true, parent); + + if (login.exec() != QDialog::Accepted) { + QString error = login.GetLastError(); + + if (!error.isEmpty()) { + blog(LOG_ERROR, "[%s] [%s]: %s", PluginLogName(), + __FUNCTION__, error.toUtf8().constData()); + + QString title = QString::fromUtf8( + obs_module_text( + "YouTube.Auth.LoginError.Title"), + -1); + QString text = + QString::fromUtf8( + obs_module_text( + "YouTube.Auth.LoginError.Text"), + -1) + .arg(error); + + WarningBox(title, text, parent); + } + + return false; + } + + code = login.GetCode().toStdString(); + redirectUri = login.GetRedirectUri().toStdString(); + + return true; +} + +bool ServiceOAuth::SignOutInternal() +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + QString title = QString::fromUtf8( + obs_module_text("YouTube.Auth.SignOutDialog.Title"), -1); + QString text = QString::fromUtf8( + obs_module_text("YouTube.Auth.SignOutDialog.Text"), -1); + + QMessageBox ques(QMessageBox::Question, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + return ques.exec() == QMessageBox::Yes; +} + +void ServiceOAuth::SetSettings(obs_data_t *data) +{ + uiSettings.rememberSettings = obs_data_get_bool(data, "remember"); + + if (!uiSettings.rememberSettings) + return; + + uiSettings.title = obs_data_get_string(data, "title"); + uiSettings.description = obs_data_get_string(data, "description"); + uiSettings.privacy = + (YouTubeApi::LiveBroadcastPrivacyStatus)obs_data_get_int( + data, "privacy"); + uiSettings.categoryId = obs_data_get_string(data, "category_id"); + uiSettings.latency = + (YouTubeApi::LiveBroadcastLatencyPreference)obs_data_get_int( + data, "latency"); + uiSettings.madeForKids = obs_data_get_bool(data, "made_for_kids"); + uiSettings.autoStart = obs_data_get_bool(data, "auto_start"); + uiSettings.autoStop = obs_data_get_bool(data, "auto_stop"); + uiSettings.dvr = obs_data_get_bool(data, "dvr"); + uiSettings.scheduleForLater = + obs_data_get_bool(data, "schedule_for_later"); + uiSettings.projection = + (YouTubeApi::LiveBroadcastProjection)obs_data_get_int( + data, "projection"); + uiSettings.thumbnailFile = obs_data_get_string(data, "thumbnail_file"); +} + +obs_data_t *ServiceOAuth::GetSettingsData() +{ + OBSData data = obs_data_create(); + + obs_data_set_bool(data, "remember", uiSettings.rememberSettings); + + if (!uiSettings.rememberSettings) + return data; + + obs_data_set_string(data, "title", uiSettings.title.c_str()); + obs_data_set_string(data, "description", + uiSettings.description.c_str()); + obs_data_set_int(data, "privacy", (int)uiSettings.privacy); + obs_data_set_string(data, "category_id", uiSettings.categoryId.c_str()); + obs_data_set_int(data, "latency", (int)uiSettings.latency); + obs_data_set_bool(data, "made_for_kids", uiSettings.madeForKids); + obs_data_set_bool(data, "auto_start", uiSettings.autoStart); + obs_data_set_bool(data, "auto_stop", uiSettings.autoStop); + obs_data_set_bool(data, "dvr", uiSettings.dvr); + obs_data_set_bool(data, "schedule_for_later", + uiSettings.scheduleForLater); + obs_data_set_int(data, "projection", (int)uiSettings.projection); + obs_data_set_string(data, "thumbnail_file", + uiSettings.thumbnailFile.c_str()); + + return data; +} + +void ServiceOAuth::LoadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + if (chat) { + blog(LOG_ERROR, "[%s][%s] The chat was not unloaded", + PluginLogName(), __FUNCTION__); + return; + } + + chat = new YoutubeChat(this); + obs_frontend_add_dock_by_id(CHAT_DOCK_NAME, + obs_module_text("YouTube.Chat.Dock"), chat); +} + +void ServiceOAuth::UnloadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + obs_frontend_remove_dock(CHAT_DOCK_NAME); + + chat = nullptr; +} + +void ServiceOAuth::AddBondedServiceFrontend(obs_service_t *service) +{ + obs_frontend_add_broadcast_flow(service, &broadcastFlow); +} + +void ServiceOAuth::RemoveBondedServiceFrontend(obs_service_t *service) +{ + obs_frontend_remove_broadcast_flow(service); +} + +void ServiceOAuth::LoginError(RequestError &error) +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + QString title = QString::fromUtf8( + obs_module_text("YouTube.Auth.LoginError.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("YouTube.Auth.LoginError.Text2"), -1) + .arg(QString::fromStdString(error.message)) + .arg(QString::fromStdString(error.error)); + + WarningBox(title, text, parent); +} + +obs_broadcast_state ServiceOAuth::GetBroadcastState(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + return self->broadcastState; +} + +obs_broadcast_start ServiceOAuth::GetBroadcastStartType(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + return self->broadcastStartType; +} + +obs_broadcast_stop ServiceOAuth::GetBroadcastStopType(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + return self->broadcastStopType; +} + +void ServiceOAuth::ManageBroadcast(void *priv) +{ + obs_frontend_push_ui_translation(obs_module_get_string); + ServiceOAuth *self = reinterpret_cast(priv); + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + OBSYoutubeActions dialog(parent, self, &self->uiSettings, false); + dialog.exec(); + obs_frontend_pop_ui_translation(); +} + +void ServiceOAuth::StoppedStreaming(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + if (self->broadcastState != OBS_BROADCAST_ACTIVE && + self->broadcastStartType == + OBS_BROADCAST_START_DIFFER_FROM_STREAM) { + self->broadcastState = OBS_BROADCAST_NONE; + } + + if (self->broadcastStopType == OBS_BROADCAST_STOP_WITH_STREAM) { + self->broadcastState = OBS_BROADCAST_NONE; + } +} + +static std::string GetTranslatedError(const RequestError &error) +{ + if (error.type != RequestErrorType::UNKNOWN_OR_CUSTOM || + error.error.empty()) + return error.message; + + std::string lookupString = "YouTube.Errors."; + lookupString += error.error; + + return obs_module_text(lookupString.c_str()); +} + +void ServiceOAuth::DifferedStartBroadcast(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + LiveBroadcast broadcast; + if (!self->FindLiveBroadcast(self->broadcastId, broadcast)) { + self->broadcastLastError = self->lastError.message; + return; + } + + // Broadcast is already (going to be) live + if (broadcast.status.lifeCycleStatus == + LiveBroadcastLifeCycleStatus::LIVE || + broadcast.status.lifeCycleStatus == + LiveBroadcastLifeCycleStatus::LIVE_STARTING) { + self->broadcastState = OBS_BROADCAST_ACTIVE; + return; + } + + if (broadcast.status.lifeCycleStatus == + LiveBroadcastLifeCycleStatus::TEST_STARTING) { + self->broadcastLastError = obs_module_text( + "YouTube.Actions.Error.BroadcastTestStarting"); + return; + } + + // Only reset if broadcast has monitoring enabled and is not already in "testing" mode + if (broadcast.status.lifeCycleStatus != + LiveBroadcastLifeCycleStatus::TESTING && + broadcast.contentDetails.monitorStream.enableMonitorStream) { + broadcast.contentDetails.monitorStream.enableMonitorStream = + false; + if (!self->UpdateLiveBroadcast(broadcast)) { + self->broadcastLastError = + GetTranslatedError(self->lastError); + return; + } + } + + if (self->TransitionLiveBroadcast( + self->broadcastId, LiveBroadcastTransitionStatus::LIVE)) { + self->broadcastState = OBS_BROADCAST_ACTIVE; + return; + } + + self->broadcastLastError = GetTranslatedError(self->lastError); + // Return a success if the command failed, but was redundant (broadcast already live) + if (self->lastError.error == "redundantTransition") + self->broadcastState = OBS_BROADCAST_ACTIVE; +} + +obs_broadcast_stream_state ServiceOAuth::IsBroadcastStreamActive(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + LiveStream stream; + if (!self->FindLiveStream(self->liveStreamId, stream)) + return OBS_BROADCAST_STREAM_FAILURE; + + if (stream.status.streamStatus == LiveStreamStatusEnum::ACTIVE) + return OBS_BROADCAST_STREAM_ACTIVE; + + return OBS_BROADCAST_STREAM_INACTIVE; +} + +bool ServiceOAuth::DifferedStopBroadcast(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + self->broadcastState = OBS_BROADCAST_NONE; + if (self->TransitionLiveBroadcast( + self->broadcastId, LiveBroadcastTransitionStatus::COMPLETE)) + return true; + + self->broadcastLastError = GetTranslatedError(self->lastError); + // Return a success if the command failed, but was redundant (broadcast already stopped) + return self->lastError.error == "redundantTransition"; +} + +const char *ServiceOAuth::GetBroadcastsLastError(void *priv) +{ + ServiceOAuth *self = reinterpret_cast(priv); + + if (self->broadcastLastError.empty()) + return nullptr; + + return self->broadcastLastError.c_str(); +} + +void ServiceOAuth::SetNewStream(const std::string &id, const std::string &key, + bool autoStart, bool autoStop, bool startNow) +{ + liveStreamId = id; + streamKey = key; + + broadcastState = autoStart ? OBS_BROADCAST_ACTIVE + : OBS_BROADCAST_INACTIVE; + + if (autoStart) { + broadcastStartType = + startNow ? OBS_BROADCAST_START_WITH_STREAM_NOW + : OBS_BROADCAST_START_WITH_STREAM; + } else { + broadcastStartType = OBS_BROADCAST_START_DIFFER_FROM_STREAM; + } + broadcastStopType = autoStop ? OBS_BROADCAST_STOP_WITH_STREAM + : OBS_BROADCAST_STOP_DIFFER_FROM_STREAM; +} + +void ServiceOAuth::SetChatIds(const std::string &broadcastId, + const std::string &chatId) +{ + if (!chat) + return; + + chat->SetChatIds(broadcastId, chatId); +} + +void ServiceOAuth::ResetChat() +{ + if (!chat) + return; + + chat->ResetIds(); +} + +static inline int Timeout(int dataSize = 0) +{ + return 5 + dataSize / 125000; +} + +bool ServiceOAuth::WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + std::string error; + CURLcode curlCode = + GetRemoteFile(UserAgent().c_str(), url, str, error, + responseCode, contentType, request_type, postData, + {"Authorization: Bearer " + AccessToken()}, + nullptr, Timeout(postDataSize), postDataSize); + + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + lastError = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + error); + return false; + } + + if (*responseCode == 200) + return true; + + try { + ErrorResponse errorResponse = nlohmann::json::parse(str); + lastError = RequestError(errorResponse.error.message, + errorResponse.error.errors[0].reason); + + } catch (nlohmann::json::exception &e) { + lastError = RequestError( + RequestErrorType::ERROR_JSON_PARSING_FAILED, e.what()); + } + + return false; +} + +bool ServiceOAuth::TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + long responseCode = 0; + if (WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize)) { + return true; + } + + if (lastError.type == RequestErrorType::CURL_REQUEST_FAILED) { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.message.c_str(), lastError.error.c_str()); + } else if (lastError.type == + RequestErrorType::ERROR_JSON_PARSING_FAILED) { + blog(LOG_ERROR, "[%s][%s] HTTP status: %ld\n%s", + PluginLogName(), funcName, responseCode, str.c_str()); + } else { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.error.c_str(), lastError.message.c_str()); + } + + if (responseCode != 401) + return false; + + lastError = RequestError(); + if (!RefreshAccessToken(lastError)) + return false; + + return WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize); +} + +void ServiceOAuth::CheckIfSuccessRequestIsError(const char *funcName, + const std::string &output) +{ + try { + ErrorResponse errorResponse = nlohmann::json::parse(output); + lastError = RequestError(errorResponse.error.message, + errorResponse.error.errors[0].reason); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.message.c_str(), lastError.error.c_str()); + + } catch (nlohmann::json::exception &e) { + RequestError error(RequestErrorType::ERROR_JSON_PARSING_FAILED, + e.what()); + blog(LOG_DEBUG, "[%s][%s] %s: %s", PluginLogName(), funcName, + error.message.c_str(), error.error.c_str()); + } +} + +bool ServiceOAuth::GetChannelInfo(ChannelInfo &info) +{ + if (!(channelInfo.id.empty() || channelInfo.title.empty())) { + info = channelInfo; + return true; + } + + std::string url = API_URL; + url += "/channels?part=snippet&mine=true"; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, "[%s][%s] Failed to retrieve channel info", + PluginLogName(), __FUNCTION__); + return false; + } + + ChannelListSnippetMineResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.items.empty()) { + lastError = + RequestError("No channel found", "NoChannels"); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + channelInfo = ChannelInfo(response.items[0].id, + response.items[0].snippet.title); + info = channelInfo; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::GetVideoCategoriesList(std::vector &list, + bool forceUS) +{ + if (!videoCategoryList.empty()) { + list = videoCategoryList; + return true; + } + + std::string url = API_URL; + std::string locale; + if (forceUS) { + locale = "en_US"; + } else { + locale = obs_get_locale(); + locale.replace(locale.find("-"), 1, "_"); + } + /* + * All OBS locale regions aside from "US" are missing category id 29 + * ("Nonprofits & Activism"), but it is still available to channels + * set to those regions via the YouTube Studio website. + * To work around this inconsistency with the API all locales will + * use the "US" region and only set the language part for localisation. + * It is worth noting that none of the regions available on YouTube + * feature any category not also available to the "US" region. + */ + url += "/videoCategories?part=snippet®ionCode=US&hl=" + locale; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, + "[%s][%s] Failed to retrieve video category list with %s locale", + PluginLogName(), __FUNCTION__, locale.c_str()); + + return forceUS ? false : GetVideoCategoriesList(list, true); + } + + VideoCategoryListResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.items.empty()) { + lastError = RequestError("No categories found", ""); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + videoCategoryList = response.items; + list = videoCategoryList; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::GetLiveBroadcastsList(const LiveBroadcastListStatus &status, + std::vector &list) +{ + std::string statusStr; + switch (status) { + case LiveBroadcastListStatus::ACTIVE: + statusStr = "active"; + break; + case LiveBroadcastListStatus::ALL: + statusStr = "all"; + break; + case LiveBroadcastListStatus::COMPLETED: + statusStr = "completed"; + break; + case LiveBroadcastListStatus::UPCOMMING: + statusStr = "upcoming"; + break; + } + + std::string baseUrl = API_URL; + baseUrl += LIVE_BROADCASTS_API_ENDPOINT; + /* 50 is the biggest accepted value for 'maxResults' */ + baseUrl += + "?part=snippet,contentDetails,status&broadcastType=all&maxResults=50&broadcastStatus=" + + statusStr; + + std::string output; + lastError = RequestError(); + + bool hasPage = true; + std::string url = baseUrl; + while (hasPage) { + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, + "[%s][%s] Failed to retrieve live broadcast list with status \"%s\"", + PluginLogName(), __FUNCTION__, statusStr.c_str()); + + return false; + } + + LiveBroadcastListResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.items.empty()) { + lastError = RequestError( + "No live broadcast found with status \"" + + statusStr + "\"", + ""); + blog(LOG_DEBUG, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return true; + } + + std::copy(response.items.begin(), response.items.end(), + std::back_inserter(list)); + + hasPage = response.nextPageToken.has_value(); + if (!hasPage) + return true; + + url = baseUrl; + url += "&pageToken=" + response.nextPageToken.value(); + + } catch (nlohmann::json::exception &e) { + lastError = RequestError( + RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + break; + } + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::FindLiveStream(const std::string &id, LiveStream &stream) +{ + std::string url = API_URL; + url += LIVE_STREAMS_API_ENDPOINT; + url += "?part=id,snippet,cdn,status&maxResults=1&id=" + id; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, "[%s][%s] Failed to retrieve channel info", + PluginLogName(), __FUNCTION__); + return false; + } + + LiveStreamListResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.items.empty()) { + lastError = RequestError("No stream found", ""); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + stream = response.items[0]; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::InsertLiveBroadcast(LiveBroadcast &broadcast) +{ + std::string url = API_URL; + url += LIVE_BROADCASTS_API_ENDPOINT; + url += "?part=snippet,status,contentDetails"; + + nlohmann::json json; + try { + json = broadcast; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + return false; + } + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "", json.dump().c_str())) { + blog(LOG_ERROR, "[%s][%s] Failed to insert broadcast", + PluginLogName(), __FUNCTION__); + return false; + } + + try { + broadcast = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::UpdateVideo(Video &video) +{ + std::string url = API_URL; + url += "/videos?part=snippet"; + + nlohmann::json json; + try { + json = video; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + return false; + } + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "PUT", json.dump().c_str())) { + blog(LOG_ERROR, "[%s][%s] Failed to update video info", + PluginLogName(), __FUNCTION__); + return false; + } + + try { + video = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::SetThumbnail(const std::string &id, const char *mimeType, + const char *thumbnailData, int thumbnailSize) +{ + std::string url = + "https://www.googleapis.com/upload/youtube/v3/thumbnails/set?videoId=" + + id; + + std::string output; + lastError = RequestError(); + if (TryGetRemoteFile(__FUNCTION__, url.c_str(), output, mimeType, + "POST", thumbnailData, thumbnailSize)) { + return true; + } + + blog(LOG_ERROR, "[%s][%s] Failed to upload thumbnail", PluginLogName(), + __FUNCTION__); + + return false; +} + +bool ServiceOAuth::InsertLiveStream(LiveStream &stream) +{ + std::string url = API_URL; + url += LIVE_STREAMS_API_ENDPOINT; + url += "?part=snippet,cdn,status,contentDetails"; + + nlohmann::json json; + try { + json = stream; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + return false; + } + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "", json.dump().c_str())) { + blog(LOG_ERROR, "[%s][%s] Failed to insert stream", + PluginLogName(), __FUNCTION__); + return false; + } + + try { + stream = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::BindLiveBroadcast(LiveBroadcast &broadcast, + const std::string &streamId) +{ + std::string url = API_URL; + url += LIVE_BROADCASTS_API_ENDPOINT; + url += "/bind?id=" + broadcast.id; + if (!streamId.empty()) + url += "&streamId=" + streamId; + url += "&part=id,snippet,contentDetails,status"; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "", "{}")) { + blog(LOG_ERROR, "[%s][%s] Failed to %s broadcast", + PluginLogName(), __FUNCTION__, + streamId.empty() ? "unbind" : "bind"); + return false; + } + + try { + broadcast = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::FindLiveBroadcast(const std::string &id, + LiveBroadcast &broadcast) +{ + std::string url = API_URL; + url += LIVE_BROADCASTS_API_ENDPOINT; + url += "?part=id,snippet,contentDetails,status&broadcastType=all&maxResults=1&id=" + + id; + + std::string output; + lastError = RequestError(); + + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, + "[%s][%s] Failed to retrieve live broadcast list", + PluginLogName(), __FUNCTION__); + + return false; + } + + LiveBroadcastListResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.items.empty()) { + lastError = RequestError("No live broadcast found", ""); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + broadcast = response.items[0]; + return true; + + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::UpdateLiveBroadcast(LiveBroadcast &broadcast) +{ + std::string url = API_URL; + url += LIVE_BROADCASTS_API_ENDPOINT; + url += "?part=id,snippet,contentDetails,status"; + + nlohmann::json json; + try { + json = broadcast; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + return false; + } + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "PUT", json.dump().c_str())) { + blog(LOG_ERROR, "[%s][%s] Failed to update live broadcast", + PluginLogName(), __FUNCTION__); + return false; + } + + try { + broadcast = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +bool ServiceOAuth::TransitionLiveBroadcast( + const std::string &id, const LiveBroadcastTransitionStatus &status) +{ + std::string url = API_URL; + url += LIVE_BROADCASTS_API_ENDPOINT; + url += "/transition?id=" + id + "&broadcastStatus="; + + switch (status) { + case LiveBroadcastTransitionStatus::COMPLETE: + url += "complete"; + break; + case LiveBroadcastTransitionStatus::LIVE: + url += "live"; + break; + case LiveBroadcastTransitionStatus::TESTING: + url += "testing"; + break; + } + + url += "&part=status"; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "POST", "{}")) { + blog(LOG_ERROR, "[%s][%s] Failed to update live broadcast", + PluginLogName(), __FUNCTION__); + return false; + } + + return true; +} + +bool ServiceOAuth::InsertLiveChatMessage(LiveChatMessage &message) +{ + std::string url = API_URL; + url += "/liveChat/messages?part=snippet"; + + nlohmann::json json; + try { + json = message; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + return false; + } + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "POST", + json.dump().c_str())) { + blog(LOG_ERROR, "[%s][%s] Failed to send message", + PluginLogName(), __FUNCTION__); + return false; + } + + try { + message = nlohmann::json::parse(output); + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + /* Check if it is an error response */ + CheckIfSuccessRequestIsError(__FUNCTION__, output); + + return false; +} + +} diff --git a/plugins/obs-youtube/youtube-oauth.hpp b/plugins/obs-youtube/youtube-oauth.hpp new file mode 100644 index 00000000000000..152c9851725a63 --- /dev/null +++ b/plugins/obs-youtube/youtube-oauth.hpp @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "youtube-api.hpp" + +#define QT_UTF8(str) QString::fromUtf8(str, -1) +#define QT_TO_UTF8(str) str.toUtf8().constData() + +class QuickThread : public QThread { +public: + explicit inline QuickThread(std::function func_) : func(func_) + { + } + +private: + virtual void run() override { func(); } + + std::function func; +}; + +struct OBSYoutubeActionsSettings { + bool rememberSettings = false; + std::string title; + std::string description; + YouTubeApi::LiveBroadcastPrivacyStatus privacy; + std::string categoryId; + YouTubeApi::LiveBroadcastLatencyPreference latency; + bool madeForKids; + bool autoStart; + bool autoStop; + bool dvr; + bool scheduleForLater; + YouTubeApi::LiveBroadcastProjection projection; + std::string thumbnailFile; +}; + +class YoutubeChat; +namespace YouTubeApi { + +class LocalRedirect : public OAuth::LocalRedirect { + Q_OBJECT + + inline QString DialogTitle() override + { + return obs_module_text("YouTube.Auth.LoginDialog.Title"); + } + inline QString DialogText() override + { + return obs_module_text("YouTube.Auth.LoginDialog.Text"); + } + inline QString ReOpenUrlButtonText() override + { + return obs_module_text("YouTube.Auth.LoginDialog.ReOpenURL"); + } + + inline QString ServerPageTitle() override { return "OBS YouTube"; } + inline QString ServerResponsePageLogoUrl() override + { + return "https://obsproject.com/assets/images/new_icon_small-r.png"; + } + inline QString ServerResponsePageLogoAlt() override { return "OBS"; } + inline QString ServerResponseSuccessText() override + { + return obs_module_text("YouTube.Auth.RedirectServer.Success"); + } + inline QString ServerResponseFailureText() override + { + return obs_module_text("YouTube.Auth.RedirectServer.Failure"); + } + + inline void DebugLog(const QString &info) override + { + blog(LOG_DEBUG, "[obs-youtube] [YouTubeLocalRedirect] %s", + info.toUtf8().constData()); + }; + +public: + LocalRedirect(const QString &baseAuthUrl, const QString &clientId, + const QString &scope, bool useState, QWidget *parent) + : OAuth::LocalRedirect(baseAuthUrl, clientId, scope, useState, + parent) + { + } + ~LocalRedirect() {} +}; + +struct ChannelInfo { + std::string id; + std::string title; + + ChannelInfo() {} + ChannelInfo(const std::string &id, const std::string &title) + : id(id), + title(title) + { + } +}; + +enum class LiveBroadcastListStatus : int { + ACTIVE, + ALL, + COMPLETED, + UPCOMMING, +}; + +enum class LiveBroadcastTransitionStatus : int { + COMPLETE, + LIVE, + TESTING, +}; + +class ServiceOAuth : public OAuth::ServiceBase { + LiveStreamCdnIngestionType ingestionType = + LiveStreamCdnIngestionType::RTMP; + + RequestError lastError; + + ChannelInfo channelInfo; + std::vector videoCategoryList; + OBSYoutubeActionsSettings uiSettings; + + std::string broadcastId; + + obs_frontend_broadcast_flow broadcastFlow = {0}; + std::string broadcastLastError; + + std::string liveStreamId; + std::string streamKey; + obs_broadcast_state broadcastState; + obs_broadcast_start broadcastStartType; + obs_broadcast_stop broadcastStopType; + + YoutubeChat *chat = nullptr; + + inline std::string UserAgent() override; + + inline const char *TokenUrl() override; + + inline std::string ClientId() override; + inline std::string ClientSecret() override; + + inline int64_t ScopeVersion() override; + + bool LoginInternal(const OAuth::LoginReason &reason, std::string &code, + std::string &redirectUri) override; + bool SignOutInternal() override; + + void SetSettings(obs_data_t *data) override; + obs_data_t *GetSettingsData() override; + + void LoadFrontendInternal() override; + void UnloadFrontendInternal() override; + + void AddBondedServiceFrontend(obs_service_t *service) override; + void RemoveBondedServiceFrontend(obs_service_t *service) override; + + const char *PluginLogName() override { return "obs-youtube"; }; + + void LoginError(RequestError &error) override; + + static obs_broadcast_state GetBroadcastState(void *priv); + static obs_broadcast_start GetBroadcastStartType(void *priv); + static obs_broadcast_stop GetBroadcastStopType(void *priv); + static void ManageBroadcast(void *priv); + + static void StoppedStreaming(void *priv); + + static void DifferedStartBroadcast(void *priv); + static obs_broadcast_stream_state IsBroadcastStreamActive(void *priv); + static bool DifferedStopBroadcast(void *priv); + static const char *GetBroadcastsLastError(void *priv); + + bool WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + + bool TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + + void CheckIfSuccessRequestIsError(const char *funcName, + const std::string &output); + +public: + ServiceOAuth(); + + void SetIngestionType(LiveStreamCdnIngestionType type) + { + ingestionType = type; + } + LiveStreamCdnIngestionType GetIngestionType() { return ingestionType; } + + void SetBroadcastId(const std::string &id) { broadcastId = id; } + std::string GetBroadcastId() { return broadcastId; } + + const char *GetStreamKey() + { + return streamKey.empty() ? nullptr : streamKey.c_str(); + }; + + void SetNewStream(const std::string &id, const std::string &key, + bool autoStart, bool autoStop, bool startNow); + + void SetChatIds(const std::string &broadcastId, + const std::string &chatId); + void ResetChat(); + + RequestError GetLastError() { return lastError; } + + bool GetChannelInfo(ChannelInfo &info); + bool GetVideoCategoriesList(std::vector &list, + bool forceUS = false); + bool GetLiveBroadcastsList(const LiveBroadcastListStatus &status, + std::vector &list); + bool FindLiveStream(const std::string &id, LiveStream &stream); + bool InsertLiveBroadcast(LiveBroadcast &broadcast); + bool UpdateVideo(Video &video); + bool SetThumbnail(const std::string &id, const char *mimeType, + const char *thumbnailData, int thumbnailSize); + bool InsertLiveStream(LiveStream &stream); + bool BindLiveBroadcast(LiveBroadcast &broadcast, + const std::string &streamId = {}); + bool FindLiveBroadcast(const std::string &id, LiveBroadcast &broadcast); + bool UpdateLiveBroadcast(LiveBroadcast &broadcast); + bool + TransitionLiveBroadcast(const std::string &id, + const LiveBroadcastTransitionStatus &status); + + bool InsertLiveChatMessage(LiveChatMessage &message); +}; + +} diff --git a/plugins/obs-youtube/youtube-service.cpp b/plugins/obs-youtube/youtube-service.cpp index 2db06891e2810e..1880762e4bb2fb 100644 --- a/plugins/obs-youtube/youtube-service.cpp +++ b/plugins/obs-youtube/youtube-service.cpp @@ -4,8 +4,14 @@ #include "youtube-service.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + #include "youtube-config.hpp" +constexpr const char *DATA_FILENAME = "obs-youtube.json"; + YouTubeService::YouTubeService() { info.type_data = this; @@ -31,6 +37,10 @@ YouTubeService::YouTubeService() info.get_properties = InfoGetProperties; obs_register_service(&info); + +#ifdef OAUTH_ENABLED + obs_frontend_add_event_callback(OBSEvent, this); +#endif } YouTubeService::~YouTubeService() {} @@ -39,3 +49,114 @@ void YouTubeService::Register() { new YouTubeService(); } + +#ifdef OAUTH_ENABLED +void YouTubeService::OBSEvent(obs_frontend_event event, void *priv) +{ + YouTubeService *self = reinterpret_cast(priv); + + switch (event) { + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + case OBS_FRONTEND_EVENT_FINISHED_LOADING: { + self->deferUiFunction = false; + break; + } + case OBS_FRONTEND_EVENT_PROFILE_CHANGING: + self->SaveOAuthsData(); + self->deferUiFunction = true; + self->data = nullptr; + break; + case OBS_FRONTEND_EVENT_EXIT: + self->SaveOAuthsData(); + obs_frontend_remove_event_callback(OBSEvent, priv); + return; + default: + break; + } + + if (self->oauths.empty()) + return; + + for (auto const &p : self->oauths) + p.second->OBSEvent(event); +} + +void YouTubeService::SaveOAuthsData() +{ + OBSDataAutoRelease saveData = obs_data_create(); + bool writeData = false; + for (auto const &[uuid, oauth] : oauths) { + OBSDataAutoRelease data = oauth->GetData(); + if (data == nullptr) + continue; + + obs_data_set_obj(saveData, uuid.c_str(), data); + writeData = true; + } + + if (!writeData) + return; + + BPtr profilePath = obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + if (!obs_data_save_json_pretty_safe(saveData, dataPath.c_str(), "tmp", + "bak")) + blog(LOG_ERROR, + "[obs-youtube][%s] Failed to save integrations data", + __FUNCTION__); +}; + +YouTubeApi::ServiceOAuth *YouTubeService::GetOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (data == nullptr) { + BPtr profilePath = + obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + data = obs_data_create_from_json_file_safe(dataPath.c_str(), + "bak"); + + if (!data) { + blog(LOG_ERROR, + "[obs-youtube][%s] Failed to open integrations data: %s", + __FUNCTION__, dataPath.c_str()); + return nullptr; + } + } + + if (oauths.count(uuid) == 0) { + OBSDataAutoRelease oauthData = + obs_data_get_obj(data, uuid.c_str()); + oauths.emplace(uuid, + std::make_shared()); + oauths[uuid]->Setup(oauthData, deferUiFunction); + oauthsRefCounter.emplace(uuid, 0); + oauths[uuid]->AddBondedService(service); + } + + oauthsRefCounter[uuid] += 1; + return oauths[uuid].get(); +} + +void YouTubeService::ReleaseOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (oauthsRefCounter.count(uuid) == 0) + return; + + oauths[uuid]->RemoveBondedService(service); + oauthsRefCounter[uuid] -= 1; + + if (oauthsRefCounter[uuid] != 0) + return; + + oauths.erase(uuid); + oauthsRefCounter.erase(uuid); +} +#endif diff --git a/plugins/obs-youtube/youtube-service.hpp b/plugins/obs-youtube/youtube-service.hpp index d4a2849ee80b92..9764481437189f 100644 --- a/plugins/obs-youtube/youtube-service.hpp +++ b/plugins/obs-youtube/youtube-service.hpp @@ -5,7 +5,14 @@ #pragma once #include -#include + +#ifdef OAUTH_ENABLED +#include +#include +#include + +#include "youtube-oauth.hpp" +#endif class YouTubeService { obs_service_info info = {0}; @@ -27,6 +34,18 @@ class YouTubeService { static obs_properties_t *InfoGetProperties(void *data); +#ifdef OAUTH_ENABLED + OBSDataAutoRelease data = nullptr; + + std::unordered_map> + oauths; + std::unordered_map oauthsRefCounter; + bool deferUiFunction = true; + + void SaveOAuthsData(); + static void OBSEvent(obs_frontend_event event, void *priv); +#endif public: YouTubeService(); ~YouTubeService(); @@ -36,4 +55,10 @@ class YouTubeService { void GetDefaults(obs_data_t *settings); obs_properties_t *GetProperties(); + +#ifdef OAUTH_ENABLED + YouTubeApi::ServiceOAuth *GetOAuth(const std::string &uuid, + obs_service_t *service); + void ReleaseOAuth(const std::string &uuid, obs_service_t *service); +#endif }; From 7291abc33f7b4efecd4e37d665c7a496e8f26378 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 22 Jun 2023 16:48:28 +0200 Subject: [PATCH 37/65] obs-youtube: Add bandwidth test support --- plugins/obs-youtube/cmake/feature-oauth.cmake | 32 ++++++++------ plugins/obs-youtube/youtube-config.cpp | 43 ++++++++++++++++++- plugins/obs-youtube/youtube-config.hpp | 9 ++++ plugins/obs-youtube/youtube-oauth.cpp | 2 +- plugins/obs-youtube/youtube-service-info.cpp | 25 +++++++++++ plugins/obs-youtube/youtube-service.cpp | 13 ++++-- plugins/obs-youtube/youtube-service.hpp | 10 ++--- 7 files changed, 108 insertions(+), 26 deletions(-) diff --git a/plugins/obs-youtube/cmake/feature-oauth.cmake b/plugins/obs-youtube/cmake/feature-oauth.cmake index 774e2ccd4f020b..c827abc2725918 100644 --- a/plugins/obs-youtube/cmake/feature-oauth.cmake +++ b/plugins/obs-youtube/cmake/feature-oauth.cmake @@ -1,15 +1,8 @@ -if(NOT DEFINED YOUTUBE_CLIENTID - OR "${YOUTUBE_CLIENTID}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET - OR "${YOUTUBE_SECRET}" STREQUAL "" - OR NOT DEFINED YOUTUBE_CLIENTID_HASH - OR "${YOUTUBE_CLIENTID_HASH}" STREQUAL "" - OR NOT DEFINED YOUTUBE_SECRET_HASH - OR "${YOUTUBE_SECRET_HASH}" STREQUAL "") - if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) - target_disable_feature(obs-youtube "YouTube OAuth connection") - endif() -else() +if(YOUTUBE_CLIENTID + AND YOUTUBE_SECRET + AND YOUTUBE_CLIENTID_HASH MATCHES "(0|[a-fA-F0-9]+)" + AND YOUTUBE_SECRET_HASH MATCHES "(0|[a-fA-F0-9]+)") + if(NOT TARGET OBS::obf) add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") endif() @@ -42,8 +35,15 @@ else() youtube-oauth.cpp youtube-oauth.hpp) - target_link_libraries(obs-youtube PRIVATE OBS::frontend-api OBS::obf OBS::oauth-service-base - OBS::oauth-local-redirect Qt::Core Qt::Widgets) + target_link_libraries( + obs-youtube + PRIVATE OBS::frontend-api + OBS::obf + OBS::oauth-service-base + OBS::oauth-local-redirect + nlohmann_json::nlohmann_json + Qt::Core + Qt::Widgets) target_compile_definitions( obs-youtube @@ -62,4 +62,8 @@ else() else() target_include_directories(obs-youtube PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) endif() +else() + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable_feature(obs-youtube "YouTube OAuth connection") + endif() endif() diff --git a/plugins/obs-youtube/youtube-config.cpp b/plugins/obs-youtube/youtube-config.cpp index 26277dc1f7de88..bb55afbfc1f8db 100644 --- a/plugins/obs-youtube/youtube-config.cpp +++ b/plugins/obs-youtube/youtube-config.cpp @@ -66,6 +66,7 @@ void YouTubeConfig::Update(obs_data_t *settings) uuid = newUuid; } + if (!oauth) oauth = typeData->GetOAuth(uuid, serviceObj); @@ -106,6 +107,41 @@ const char *YouTubeConfig::ConnectInfo(uint32_t type) #ifdef OAUTH_ENABLED if (oauth->Connected()) { const char *streamKey = oauth->GetStreamKey(); + + if (bandwidthTest) { + /* Create throwaway stream key for bandwidth test */ + bandwidthTestStream = {}; + bandwidthTestStream.snippet.title = + "OBS Studio Test Stream"; + bandwidthTestStream.cdn.ingestionType = + oauth->GetIngestionType(); + bandwidthTestStream.cdn.resolution = + YouTubeApi::LiveStreamCdnResolution:: + RESOLUTION_VARIABLE; + bandwidthTestStream.cdn.frameRate = + YouTubeApi::LiveStreamCdnFrameRate:: + FRAMERATE_VARIABLE; + YouTubeApi::LiveStreamContentDetails + contentDetails; + contentDetails.isReusable = false; + bandwidthTestStream.contentDetails = + contentDetails; + if (!oauth->InsertLiveStream( + bandwidthTestStream)) { + blog(LOG_ERROR, + "[obs-youtube][ConnectInfo] " + "Bandwidth test stream could no be created"); + return nullptr; + } + + if (bandwidthTestStream.cdn.ingestionInfo + .streamName.empty()) + return nullptr; + + return bandwidthTestStream.cdn.ingestionInfo + .streamName.c_str(); + } + if (streamKey) return streamKey; } else if (!streamKey.empty()) { @@ -129,8 +165,11 @@ bool YouTubeConfig::CanTryToConnect() if (serverUrl.empty()) return false; - return oauth->Connected() ? !!oauth->GetStreamKey() - : !streamKey.empty(); + if (oauth->Connected()) { + return bandwidthTest ? true : !!oauth->GetStreamKey(); + } else { + return !streamKey.empty(); + } #else if (serverUrl.empty() || streamKey.empty()) return false; diff --git a/plugins/obs-youtube/youtube-config.hpp b/plugins/obs-youtube/youtube-config.hpp index 0731463c3e1be5..50b48c1f3a660b 100644 --- a/plugins/obs-youtube/youtube-config.hpp +++ b/plugins/obs-youtube/youtube-config.hpp @@ -18,6 +18,9 @@ class YouTubeConfig { std::string uuid; YouTubeApi::ServiceOAuth *oauth = nullptr; + + bool bandwidthTest = false; + YouTubeApi::LiveStream bandwidthTestStream; #endif std::string protocol; @@ -39,5 +42,11 @@ class YouTubeConfig { bool CanTryToConnect(); +#ifdef OAUTH_ENABLED + bool CanBandwidthTest() { return oauth->Connected(); } + void EnableBandwidthTest(bool enabled) { bandwidthTest = enabled; } + bool BandwidthTestEnabled() { return bandwidthTest; } +#endif + obs_properties_t *GetProperties(); }; diff --git a/plugins/obs-youtube/youtube-oauth.cpp b/plugins/obs-youtube/youtube-oauth.cpp index b3163aab843e56..db52cb3b120665 100644 --- a/plugins/obs-youtube/youtube-oauth.cpp +++ b/plugins/obs-youtube/youtube-oauth.cpp @@ -151,7 +151,7 @@ bool ServiceOAuth::LoginInternal(const OAuth::LoginReason &reason, QString error = login.GetLastError(); if (!error.isEmpty()) { - blog(LOG_ERROR, "[%s] [%s]: %s", PluginLogName(), + blog(LOG_ERROR, "[%s][%s]: %s", PluginLogName(), __FUNCTION__, error.toUtf8().constData()); QString title = QString::fromUtf8( diff --git a/plugins/obs-youtube/youtube-service-info.cpp b/plugins/obs-youtube/youtube-service-info.cpp index cdd128800fbdb8..8de1cbf5f6c19b 100644 --- a/plugins/obs-youtube/youtube-service-info.cpp +++ b/plugins/obs-youtube/youtube-service-info.cpp @@ -73,3 +73,28 @@ obs_properties_t *YouTubeService::InfoGetProperties(void *data) return reinterpret_cast(data)->GetProperties(); return nullptr; } + +#ifdef OAUTH_ENABLED +bool YouTubeService::InfoCanBandwidthTest(void *data) +{ + if (data) + return reinterpret_cast(data) + ->CanBandwidthTest(); + return false; +} + +void YouTubeService::InfoEnableBandwidthTest(void *data, bool enabled) +{ + if (data) + return reinterpret_cast(data) + ->EnableBandwidthTest(enabled); +} + +bool YouTubeService::InfoBandwidthTestEnabled(void *data) +{ + if (data) + return reinterpret_cast(data) + ->BandwidthTestEnabled(); + return false; +} +#endif diff --git a/plugins/obs-youtube/youtube-service.cpp b/plugins/obs-youtube/youtube-service.cpp index 1880762e4bb2fb..9ce7c55842551f 100644 --- a/plugins/obs-youtube/youtube-service.cpp +++ b/plugins/obs-youtube/youtube-service.cpp @@ -10,7 +10,9 @@ #include "youtube-config.hpp" +#ifdef OAUTH_ENABLED constexpr const char *DATA_FILENAME = "obs-youtube.json"; +#endif YouTubeService::YouTubeService() { @@ -36,6 +38,12 @@ YouTubeService::YouTubeService() info.get_defaults = YouTubeConfig::InfoGetDefault; info.get_properties = InfoGetProperties; +#ifdef OAUTH_ENABLED + info.can_bandwidth_test = InfoCanBandwidthTest; + info.enable_bandwidth_test = InfoEnableBandwidthTest; + info.bandwidth_test_enabled = InfoBandwidthTestEnabled; +#endif + obs_register_service(&info); #ifdef OAUTH_ENABLED @@ -43,8 +51,6 @@ YouTubeService::YouTubeService() #endif } -YouTubeService::~YouTubeService() {} - void YouTubeService::Register() { new YouTubeService(); @@ -123,10 +129,9 @@ YouTubeApi::ServiceOAuth *YouTubeService::GetOAuth(const std::string &uuid, "bak"); if (!data) { - blog(LOG_ERROR, + blog(LOG_DEBUG, "[obs-youtube][%s] Failed to open integrations data: %s", __FUNCTION__, dataPath.c_str()); - return nullptr; } } diff --git a/plugins/obs-youtube/youtube-service.hpp b/plugins/obs-youtube/youtube-service.hpp index 9764481437189f..2534a0d210cd3c 100644 --- a/plugins/obs-youtube/youtube-service.hpp +++ b/plugins/obs-youtube/youtube-service.hpp @@ -45,17 +45,17 @@ class YouTubeService { void SaveOAuthsData(); static void OBSEvent(obs_frontend_event event, void *priv); + + static bool InfoCanBandwidthTest(void *data); + static void InfoEnableBandwidthTest(void *data, bool enabled); + static bool InfoBandwidthTestEnabled(void *data); #endif public: YouTubeService(); - ~YouTubeService(); + inline ~YouTubeService() {} static void Register(); - void GetDefaults(obs_data_t *settings); - - obs_properties_t *GetProperties(); - #ifdef OAUTH_ENABLED YouTubeApi::ServiceOAuth *GetOAuth(const std::string &uuid, obs_service_t *service); From 5210d07df271269c9b0841d6724aff120bbf87fe Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 23 Jun 2023 07:08:20 +0200 Subject: [PATCH 38/65] UI,docs: Ignore broadcast flow if bandwidth test enabled --- UI/data/locale/en-US.ini | 2 + UI/window-basic-main.cpp | 42 ++++++++++++++----- .../reference-frontend-broadcast-flow-api.rst | 2 + 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 7dc0876770d3ae..58a0bbd98d77d0 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -714,6 +714,8 @@ Basic.Main.AutoStopEnabled="(Auto Stop)" Basic.Main.BroadcastManualStartWarning.Title="Manual start required" Basic.Main.BroadcastManualStartWarning="Auto-start is disabled for this event, click \"Go Live\" to start your broadcast." Basic.Main.BroadcastEndWarning="You will not be able to reconnect.
Your stream will stop and you will no longer be live." +Basic.Main.ManageBroadcastBWTest.Title="Bandwidth Test Enabled" +Basic.Main.ManageBroadcastBWTest.Text="You have OBS configured in bandwidth test mode. Managing broadcast is not allowed under this mode, you will need to disable it in order manage your broadcast." Basic.Main.StoppingStreaming="Stopping Stream..." Basic.Main.ForceStopStreaming="Stop Streaming (discard delay)" Basic.Main.ShowContextBar="Show Source Toolbar" diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index cdc5b484b4de03..67d36230618b9d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -6703,7 +6703,9 @@ void OBSBasic::StartStreaming() if (disableOutputsRef) return; - if (serviceBroadcastFlow) { + /* Ignore broadcast flow if bandwidth test is enabled */ + bool bwtest = obs_service_bandwidth_test_enabled(service); + if (!bwtest && serviceBroadcastFlow) { if (serviceBroadcastFlow->BroadcastState() == OBS_BROADCAST_NONE) { ui->streamButton->setChecked(false); @@ -6752,7 +6754,8 @@ void OBSBasic::StartStreaming() return; } - if (serviceBroadcastFlow) { + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!bwtest && serviceBroadcastFlow) { ui->broadcastButton->setChecked(false); if (serviceBroadcastFlow->BroadcastStartType() == @@ -6779,7 +6782,8 @@ void OBSBasic::StartStreaming() if (replayBufferWhileStreaming) StartReplayBuffer(); - if (!serviceBroadcastFlow || + /* Ignore broadcast flow if bandwidth test is enabled */ + if (bwtest || !serviceBroadcastFlow || serviceBroadcastFlow->BroadcastStartType() != OBS_BROADCAST_START_DIFFER_FROM_STREAM) return; @@ -6814,6 +6818,13 @@ void OBSBasic::StartStreaming() void OBSBasic::ManageBroadcastButtonClicked() { + if (obs_service_bandwidth_test_enabled(service)) { + OBSMessageBox::warning( + this, QTStr("Basic.Main.ManageBroadcastBWTest.Title"), + QTStr("Basic.Main.ManageBroadcastBWTest.Text")); + return; + } + bool streamingActive = outputHandler->StreamingActive(); serviceBroadcastFlow->ManageBroadcast(streamingActive); @@ -7033,7 +7044,9 @@ void OBSBasic::StopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(streamingStopping); - if (serviceBroadcastFlow) + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow) serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7062,7 +7075,9 @@ void OBSBasic::ForceStopStreaming() if (outputHandler->StreamingActive()) outputHandler->StopStreaming(true); - if (serviceBroadcastFlow) + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow) serviceBroadcastFlow->StopStreaming(); OnDeactivate(); @@ -7149,7 +7164,9 @@ void OBSBasic::StreamingStart() sysTrayStream->setEnabled(true); } - if (serviceBroadcastFlow && + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow && serviceBroadcastFlow->BroadcastStartType() == OBS_BROADCAST_START_DIFFER_FROM_STREAM && !broadcastStreamCheckThread) { @@ -7273,7 +7290,9 @@ void OBSBasic::StreamingStop(int code, QString last_error) } // Reset broadcast button state/text - if (serviceBroadcastFlow && + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!obs_service_bandwidth_test_enabled(service) && + serviceBroadcastFlow && serviceBroadcastFlow->BroadcastState() != OBS_BROADCAST_ACTIVE) ResetBroadcastButtonState(); } @@ -7792,11 +7811,13 @@ void OBSBasic::OnVirtualCamStop(int) void OBSBasic::on_streamButton_clicked() { + bool bwtest = obs_service_bandwidth_test_enabled(service); if (outputHandler->StreamingActive()) { bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStoppingStream"); - if (isVisible() && serviceBroadcastFlow) { + /* Ignore broadcast flow if bandwidth test is enabled */ + if (isVisible() && !bwtest && serviceBroadcastFlow) { if (serviceBroadcastFlow->BroadcastStopType() == OBS_BROADCAST_STOP_WITH_STREAM) { QMessageBox::StandardButton button = @@ -7860,10 +7881,9 @@ void OBSBasic::on_streamButton_clicked() bool confirm = config_get_bool(GetGlobalConfig(), "BasicWindow", "WarnBeforeStartingStream"); - bool bwtest = obs_service_bandwidth_test_enabled(service); - // Disable confirmation if this is going to open broadcast setup - if (serviceBroadcastFlow && + /* Ignore broadcast flow if bandwidth test is enabled */ + if (!bwtest && serviceBroadcastFlow && serviceBroadcastFlow->BroadcastState() == OBS_BROADCAST_NONE) { confirm = false; diff --git a/docs/sphinx/reference-frontend-broadcast-flow-api.rst b/docs/sphinx/reference-frontend-broadcast-flow-api.rst index 713477efcbb606..5308031a4e3fbc 100644 --- a/docs/sphinx/reference-frontend-broadcast-flow-api.rst +++ b/docs/sphinx/reference-frontend-broadcast-flow-api.rst @@ -8,6 +8,8 @@ Services usually does not separate the concept of broadcast and stream, but some does. In this case features related to differed start/stop might need to be enabled. +NOTE: If the service has its bandwidth test enabled, the broadcast flow is ignored. + .. code:: cpp #include From 788880304956d88648118f25a9e08229ab183a44 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 23 Jun 2023 07:12:59 +0200 Subject: [PATCH 39/65] UI: Disable stream settings while an output is active --- UI/window-basic-settings-stream.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 51b33d47426b76..d0b9ccee9a30c2 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -86,6 +86,10 @@ void OBSBasicSettings::LoadStream1Settings() QMetaObject::invokeMethod(this, &OBSBasicSettings::UpdateResFPSLimits, Qt::QueuedConnection); + + if (obs_video_active()) { + ui->streamPage->setEnabled(false); + } } void OBSBasicSettings::SaveStream1Settings() From 3517aa4286e30181663010a68e3f30bd3a1b4407 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 24 Jun 2023 22:08:21 +0200 Subject: [PATCH 40/65] plugins: Add obs-twitch --- plugins/CMakeLists.txt | 2 + plugins/obs-twitch/CMakeLists.txt | 36 ++++ plugins/obs-twitch/cmake/macos/Info.plist.in | 28 +++ .../obs-twitch/cmake/windows/obs-module.rc.in | 24 +++ plugins/obs-twitch/data/locale/en-US.ini | 8 + plugins/obs-twitch/obs-twitch.cpp | 16 ++ plugins/obs-twitch/twitch-api.hpp | 35 ++++ plugins/obs-twitch/twitch-config.cpp | 162 ++++++++++++++++++ plugins/obs-twitch/twitch-config.hpp | 47 +++++ plugins/obs-twitch/twitch-service-info.cpp | 98 +++++++++++ plugins/obs-twitch/twitch-service.cpp | 110 ++++++++++++ plugins/obs-twitch/twitch-service.hpp | 44 +++++ 12 files changed, 610 insertions(+) create mode 100644 plugins/obs-twitch/CMakeLists.txt create mode 100644 plugins/obs-twitch/cmake/macos/Info.plist.in create mode 100644 plugins/obs-twitch/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-twitch/data/locale/en-US.ini create mode 100644 plugins/obs-twitch/obs-twitch.cpp create mode 100644 plugins/obs-twitch/twitch-api.hpp create mode 100644 plugins/obs-twitch/twitch-config.cpp create mode 100644 plugins/obs-twitch/twitch-config.hpp create mode 100644 plugins/obs-twitch/twitch-service-info.cpp create mode 100644 plugins/obs-twitch/twitch-service.cpp create mode 100644 plugins/obs-twitch/twitch-service.hpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7a58aee2ac9aa9..11a1dbc33fb4c3 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -75,6 +75,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) add_subdirectory(obs-text) endif() add_subdirectory(obs-transitions) + add_subdirectory(obs-twitch) if(OS_WINDOWS OR OS_MACOS OR OS_LINUX) @@ -204,3 +205,4 @@ add_subdirectory(obs-services) add_subdirectory(custom-services) add_subdirectory(orphaned-services) add_subdirectory(obs-youtube) +add_subdirectory(obs-twitch) diff --git a/plugins/obs-twitch/CMakeLists.txt b/plugins/obs-twitch/CMakeLists.txt new file mode 100644 index 00000000000000..3ffbca6a33262f --- /dev/null +++ b/plugins/obs-twitch/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(obs-twitch MODULE) +add_library(OBS::obs-twitch ALIAS obs-twitch) + +# Use the included GetRemoteFile function +if(NOT TARGET OBS::oauth) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/oauth" "${CMAKE_BINARY_DIR}/deps/oauth-service/oauth") +endif() + +find_package(nlohmann_json) + +target_sources( + obs-twitch + PRIVATE # cmake-format: sortable + obs-twitch.cpp + twitch-api.hpp + twitch-config.cpp + twitch-config.hpp + twitch-service-info.cpp + twitch-service.cpp + twitch-service.hpp) + +target_link_libraries(obs-twitch PRIVATE OBS::libobs OBS::oauth nlohmann_json::nlohmann_json) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-twitch.rc) + target_sources(obs-twitch PRIVATE obs-twitch.rc) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs(obs-twitch PROPERTIES FOLDER plugins PREFIX "") +else() + set_target_properties(obs-twitch PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(obs-twitch) +endif() diff --git a/plugins/obs-twitch/cmake/macos/Info.plist.in b/plugins/obs-twitch/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..36e161bcaf4bad --- /dev/null +++ b/plugins/obs-twitch/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-youtube + CFBundleIdentifier + com.obsproject.obs-twitch + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + obs-youtube + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/obs-twitch/cmake/windows/obs-module.rc.in b/plugins/obs-twitch/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..3cdf24efb46f52 --- /dev/null +++ b/plugins/obs-twitch/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS Twitch service" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-twitch" + VALUE "OriginalFilename", "obs-twitch" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-twitch/data/locale/en-US.ini b/plugins/obs-twitch/data/locale/en-US.ini new file mode 100644 index 00000000000000..65af478d2b3d19 --- /dev/null +++ b/plugins/obs-twitch/data/locale/en-US.ini @@ -0,0 +1,8 @@ +Twitch.Ingest.Auto="Auto (recommended)" +Twitch.Ingest.Default="Default" +Twitch.Protocol="Protocol" +Twitch.Server="Server" +Twitch.StreamKey="Stream Key" +Twitch.GetStreamKey="Get Stream Key" +Twitch.UseStreamKey="Use Stream Key" +Twitch.EnforceBandwidthTestMode="Enforce Bandwidth Test Mode" diff --git a/plugins/obs-twitch/obs-twitch.cpp b/plugins/obs-twitch/obs-twitch.cpp new file mode 100644 index 00000000000000..e02004bc8531a7 --- /dev/null +++ b/plugins/obs-twitch/obs-twitch.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "twitch-service.hpp" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-twitch", "en-US") + +bool obs_module_load(void) +{ + TwitchService::Register(); + return true; +} diff --git a/plugins/obs-twitch/twitch-api.hpp b/plugins/obs-twitch/twitch-api.hpp new file mode 100644 index 00000000000000..4ae1b9896015c6 --- /dev/null +++ b/plugins/obs-twitch/twitch-api.hpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace TwitchApi { +using nlohmann::json; + +/* NOTE: "Reserved for internal use" and "_id" values are stripped */ +struct Ingest { + std::string name; + std::string url_template; + std::string url_template_secure; +}; + +struct IngestResponse { + std::vector ingests; +}; + +inline void from_json(const json &j, Ingest &s) +{ + s.name = j.at("name").get(); + s.url_template = j.at("url_template").get(); + s.url_template_secure = j.at("url_template_secure").get(); +} + +inline void from_json(const json &j, IngestResponse &s) +{ + s.ingests = j.at("ingests").get>(); +} + +} diff --git a/plugins/obs-twitch/twitch-config.cpp b/plugins/obs-twitch/twitch-config.cpp new file mode 100644 index 00000000000000..aad05c4e1a7070 --- /dev/null +++ b/plugins/obs-twitch/twitch-config.cpp @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "twitch-config.hpp" + +constexpr const char *STREAM_KEY_LINK = + "https://dashboard.twitch.tv/settings/stream"; +constexpr const char *DEFAULT_RTMPS_INGEST = "rtmps://live.twitch.tv/app"; +constexpr const char *DEFAULT_RTMP_INGEST = "rtmp://live.twitch.tv/app"; + +constexpr const char *BANDWIDTH_TEST = "?bandwidthtest=true"; + +TwitchConfig::TwitchConfig(obs_data_t *settings, obs_service_t *self) + : typeData(reinterpret_cast( + obs_service_get_type_data(self))) +{ + + Update(settings); +} + +void TwitchConfig::Update(obs_data_t *settings) +{ + protocol = obs_data_get_string(settings, "protocol"); + + std::string server = obs_data_get_string(settings, "server"); + serverAuto = server == "auto"; + if (!serverAuto) + serverUrl = obs_data_get_string(settings, "server"); + + streamKey = obs_data_get_string(settings, "stream_key"); + bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; + + enforceBandwidthTest = obs_data_get_bool(settings, "enforce_bwtest"); +} + +TwitchConfig::~TwitchConfig() {} + +void TwitchConfig::InfoGetDefault(obs_data_t *settings) +{ + obs_data_set_string(settings, "protocol", "RTMPS"); + obs_data_set_string(settings, "server", "auto"); +} + +const char *TwitchConfig::ConnectInfo(uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + if (serverAuto) { + std::vector ingests = + typeData->GetIngests(true); + if (ingests.empty()) { + return protocol == "RTMPS" + ? DEFAULT_RTMPS_INGEST + : DEFAULT_RTMP_INGEST; + } + + serverUrl = protocol == "RTMPS" + ? ingests[0].url_template_secure + : ingests[0].url_template; + } + + if (!serverUrl.empty()) + return serverUrl.c_str(); + + break; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + if (BandwidthTestEnabled() ? !bandwidthTestStreamKey.empty() + : !streamKey.empty()) { + return BandwidthTestEnabled() + ? bandwidthTestStreamKey.c_str() + : streamKey.c_str(); + } + break; + default: + break; + } + + return nullptr; +} + +bool TwitchConfig::CanTryToConnect() +{ + if ((!serverAuto && serverUrl.empty()) || streamKey.empty()) + return false; + + return true; +} + +static bool ModifiedProtocolCb(void *priv, obs_properties_t *props, + obs_property_t *, obs_data_t *settings) +{ + TwitchService *typeData = reinterpret_cast(priv); + + std::string protocol = obs_data_get_string(settings, "protocol"); + obs_property_t *p = obs_properties_get(props, "server"); + + if (protocol.empty()) + return false; + + std::vector ingests = typeData->GetIngests(); + obs_property_list_clear(p); + + obs_property_list_add_string(p, obs_module_text("Twitch.Ingest.Auto"), + "auto"); + + if (!ingests.empty()) { + for (const TwitchApi::Ingest &ingest : ingests) { + obs_property_list_add_string( + p, ingest.name.c_str(), + protocol == "RTMPS" + ? ingest.url_template_secure.c_str() + : ingest.url_template.c_str()); + } + } + + obs_property_list_add_string(p, + obs_module_text("Twitch.Ingest.Default"), + protocol == "RTMPS" ? DEFAULT_RTMPS_INGEST + : DEFAULT_RTMP_INGEST); + + return true; +} + +obs_properties_t *TwitchConfig::GetProperties() +{ + obs_properties_t *ppts = obs_properties_create(); + obs_property_t *p; + + p = obs_properties_add_list(ppts, "protocol", + obs_module_text("Twitch.Protocol"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + if (obs_is_output_protocol_registered("RTMPS")) + obs_property_list_add_string(p, "RTMPS", "RTMPS"); + + if (obs_is_output_protocol_registered("RTMP")) + obs_property_list_add_string(p, "RTMP", "RTMP"); + + obs_property_set_modified_callback2(p, ModifiedProtocolCb, typeData); + + obs_properties_add_list(ppts, "server", + obs_module_text("Twitch.Server"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + p = obs_properties_add_text(ppts, "stream_key", + obs_module_text("Twitch.StreamKey"), + OBS_TEXT_PASSWORD); + + p = obs_properties_add_button(ppts, "get_stream_key", + obs_module_text("Twitch.GetStreamKey"), + nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); + + obs_properties_add_bool( + ppts, "enforce_bwtest", + obs_module_text("Twitch.EnforceBandwidthTestMode")); + + return ppts; +} diff --git a/plugins/obs-twitch/twitch-config.hpp b/plugins/obs-twitch/twitch-config.hpp new file mode 100644 index 00000000000000..bee8c34eaab286 --- /dev/null +++ b/plugins/obs-twitch/twitch-config.hpp @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "twitch-service.hpp" + +class TwitchConfig { + TwitchService *typeData; + + std::string protocol; + bool serverAuto; + std::string serverUrl; + + std::string streamKey; + std::string bandwidthTestStreamKey; + + bool bandwidthTest = false; + bool enforceBandwidthTest = false; + +public: + TwitchConfig(obs_data_t *settings, obs_service_t *self); + ~TwitchConfig(); + + void Update(obs_data_t *settings); + + static void InfoGetDefault(obs_data_t *settings); + + const char *Protocol() { return protocol.c_str(); }; + + const char *ConnectInfo(uint32_t type); + + bool CanTryToConnect(); + + bool CanBandwidthTest() { return true; } + void EnableBandwidthTest(bool enabled) { bandwidthTest = enabled; } + bool BandwidthTestEnabled() + { + return bandwidthTest || enforceBandwidthTest; + } + + obs_properties_t *GetProperties(); +}; diff --git a/plugins/obs-twitch/twitch-service-info.cpp b/plugins/obs-twitch/twitch-service-info.cpp new file mode 100644 index 00000000000000..d9fbb3bb2195b8 --- /dev/null +++ b/plugins/obs-twitch/twitch-service-info.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "twitch-service.hpp" + +#include "twitch-config.hpp" + +static const char *SUPPORTED_VIDEO_CODECS[] = {"h264", NULL}; + +void TwitchService::InfoFreeTypeData(void *typeData) +{ + if (typeData) + delete reinterpret_cast(typeData); +} + +const char *TwitchService::InfoGetName(void *) +{ + return "Twitch"; +} + +void *TwitchService::InfoCreate(obs_data_t *settings, obs_service_t *service) +{ + return reinterpret_cast(new TwitchConfig(settings, service)); + ; +} + +void TwitchService::InfoDestroy(void *data) +{ + if (data) + delete reinterpret_cast(data); +} + +void TwitchService::InfoUpdate(void *data, obs_data_t *settings) +{ + TwitchConfig *priv = reinterpret_cast(data); + if (priv) + priv->Update(settings); +} + +const char *TwitchService::InfoGetConnectInfo(void *data, uint32_t type) +{ + TwitchConfig *priv = reinterpret_cast(data); + if (priv) + return priv->ConnectInfo(type); + return nullptr; +} + +const char *TwitchService::InfoGetProtocol(void *data) +{ + TwitchConfig *priv = reinterpret_cast(data); + if (priv) + return priv->Protocol(); + return nullptr; +} + +const char **TwitchService::InfoGetSupportedVideoCodecs(void *) +{ + return SUPPORTED_VIDEO_CODECS; +} + +bool TwitchService::InfoCanTryToConnect(void *data) +{ + TwitchConfig *priv = reinterpret_cast(data); + if (priv) + return priv->CanTryToConnect(); + return false; +} + +obs_properties_t *TwitchService::InfoGetProperties(void *data) +{ + if (data) + return reinterpret_cast(data)->GetProperties(); + return nullptr; +} + +bool TwitchService::InfoCanBandwidthTest(void *data) +{ + if (data) + return reinterpret_cast(data) + ->CanBandwidthTest(); + return false; +} + +void TwitchService::InfoEnableBandwidthTest(void *data, bool enabled) +{ + if (data) + return reinterpret_cast(data) + ->EnableBandwidthTest(enabled); +} + +bool TwitchService::InfoBandwidthTestEnabled(void *data) +{ + if (data) + return reinterpret_cast(data) + ->BandwidthTestEnabled(); + return false; +} diff --git a/plugins/obs-twitch/twitch-service.cpp b/plugins/obs-twitch/twitch-service.cpp new file mode 100644 index 00000000000000..9fb70b6a871002 --- /dev/null +++ b/plugins/obs-twitch/twitch-service.cpp @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "twitch-service.hpp" + +#include "twitch-config.hpp" + +TwitchService::TwitchService() +{ + info.type_data = this; + info.free_type_data = InfoFreeTypeData; + + info.id = "twitch"; + info.supported_protocols = "RTMPS;RTMP"; + + info.get_name = InfoGetName; + info.create = InfoCreate; + info.destroy = InfoDestroy; + info.update = InfoUpdate; + + info.get_connect_info = InfoGetConnectInfo; + + info.get_protocol = InfoGetProtocol; + + info.get_supported_video_codecs = InfoGetSupportedVideoCodecs; + + info.can_try_to_connect = InfoCanTryToConnect; + + info.flags = 0; + + info.get_defaults = TwitchConfig::InfoGetDefault; + info.get_properties = InfoGetProperties; + + info.can_bandwidth_test = InfoCanBandwidthTest; + info.enable_bandwidth_test = InfoEnableBandwidthTest; + info.bandwidth_test_enabled = InfoBandwidthTestEnabled; + + obs_register_service(&info); +} + +void TwitchService::Register() +{ + new TwitchService(); +} + +std::vector TwitchService::GetIngests(bool refresh) +{ + if (!ingests.empty() && !refresh) + return ingests; + + std::string output; + std::string errorStr; + long responseCode = 0; + std::string userAgent("obs-twitch "); + userAgent += obs_get_version_string(); + + CURLcode curlCode = GetRemoteFile( + userAgent.c_str(), "https://ingest.twitch.tv/ingests", output, + errorStr, &responseCode, "application/x-www-form-urlencoded", + "", nullptr, std::vector(), nullptr, 5); + + RequestError error; + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + error = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + errorStr); + blog(LOG_ERROR, "[obs-twitch][%s] %s: %s", __FUNCTION__, + error.message.c_str(), error.error.c_str()); + return {}; + } + + TwitchApi::IngestResponse response; + switch (responseCode) { + case 200: + try { + response = nlohmann::json::parse(output); + ingests = response.ingests; + + for (TwitchApi::Ingest &ingest : ingests) { +#define REMOVE_STREAMKEY_PLACEHOLDER(url) \ + url = url.substr(0, url.find("/{stream_key}")) + + REMOVE_STREAMKEY_PLACEHOLDER( + ingest.url_template); + REMOVE_STREAMKEY_PLACEHOLDER( + ingest.url_template_secure); + +#undef REMOVE_STREAMKEY_PLACEHOLDER + } + + return ingests; + } catch (nlohmann::json::exception &e) { + error = RequestError( + RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[obs-twitch][%s] %s: %s", __FUNCTION__, + error.message.c_str(), error.error.c_str()); + } + break; + default: + error = RequestError( + RequestErrorType::UNMANAGED_HTTP_RESPONSE_CODE, + errorStr); + blog(LOG_ERROR, "[obs-twitch][%s] HTTP status: %ld", + __FUNCTION__, responseCode); + break; + } + + return {}; +} diff --git a/plugins/obs-twitch/twitch-service.hpp b/plugins/obs-twitch/twitch-service.hpp new file mode 100644 index 00000000000000..50c63022087961 --- /dev/null +++ b/plugins/obs-twitch/twitch-service.hpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "twitch-api.hpp" + +class TwitchService { + obs_service_info info = {0}; + + std::vector ingests; + + static void InfoFreeTypeData(void *typeData); + + static const char *InfoGetName(void *typeData); + static void *InfoCreate(obs_data_t *settings, obs_service_t *service); + static void InfoDestroy(void *data); + static void InfoUpdate(void *data, obs_data_t *settings); + + static const char *InfoGetConnectInfo(void *data, uint32_t type); + + static const char *InfoGetProtocol(void *data); + + static const char **InfoGetSupportedVideoCodecs(void *data); + + static bool InfoCanTryToConnect(void *data); + + static obs_properties_t *InfoGetProperties(void *data); + + static bool InfoCanBandwidthTest(void *data); + static void InfoEnableBandwidthTest(void *data, bool enabled); + static bool InfoBandwidthTestEnabled(void *data); + +public: + TwitchService(); + ~TwitchService() {} + + static void Register(); + + std::vector GetIngests(bool refresh = false); +}; From 3a17355f7ffe58ef6ffe2e00065d6f08ea2eaad8 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 15:06:07 +0200 Subject: [PATCH 41/65] UI: Remove integrated Twitch integration --- UI/CMakeLists.txt | 1 - UI/auth-twitch.cpp | 515 ---------------------------------- UI/auth-twitch.hpp | 45 --- UI/cmake/feature-twitch.cmake | 10 - UI/cmake/legacy.cmake | 17 -- UI/data/locale/en-US.ini | 4 - UI/window-basic-main.cpp | 6 +- 7 files changed, 1 insertion(+), 597 deletions(-) delete mode 100644 UI/auth-twitch.cpp delete mode 100644 UI/auth-twitch.hpp delete mode 100644 UI/cmake/feature-twitch.cmake diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index a4c54effb474f6..c60e3fad60eae8 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -55,7 +55,6 @@ if(NOT OAUTH_BASE_URL) mark_as_advanced(OAUTH_BASE_URL) # cmake-format: on endif() -include(cmake/feature-twitch.cmake) include(cmake/feature-restream.cmake) include(cmake/feature-sparkle.cmake) include(cmake/feature-whatsnew.cmake) diff --git a/UI/auth-twitch.cpp b/UI/auth-twitch.cpp deleted file mode 100644 index 28d7f5e1112b81..00000000000000 --- a/UI/auth-twitch.cpp +++ /dev/null @@ -1,515 +0,0 @@ -#include "auth-twitch.hpp" - -#include -#include -#include -#include -#include - -#include -#include - -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include - -#include "ui-config.h" -#include - -using namespace json11; - -/* ------------------------------------------------------------------------- */ - -#define TWITCH_AUTH_URL OAUTH_BASE_URL "v1/twitch/redirect" -#define TWITCH_TOKEN_URL OAUTH_BASE_URL "v1/twitch/token" - -#define TWITCH_SCOPE_VERSION 1 - -#define TWITCH_CHAT_DOCK_NAME "twitchChat" -#define TWITCH_INFO_DOCK_NAME "twitchInfo" -#define TWITCH_STATS_DOCK_NAME "twitchStats" -#define TWITCH_FEED_DOCK_NAME "twitchFeed" - -static Auth::Def twitchDef = {"Twitch", Auth::Type::OAuth_StreamKey}; - -/* ------------------------------------------------------------------------- */ - -TwitchAuth::TwitchAuth(const Def &d) : OAuthStreamKey(d) -{ - if (!cef) - return; - - cef->add_popup_whitelist_url( - "https://twitch.tv/popout/frankerfacez/chat?ffz-settings", - this); - - /* enables javascript-based popups. basically bttv popups */ - cef->add_popup_whitelist_url("about:blank#blocked", this); - - uiLoadTimer.setSingleShot(true); - uiLoadTimer.setInterval(500); - connect(&uiLoadTimer, &QTimer::timeout, this, - &TwitchAuth::TryLoadSecondaryUIPanes); -} - -TwitchAuth::~TwitchAuth() -{ - if (!uiLoaded) - return; - - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(TWITCH_CHAT_DOCK_NAME); - main->RemoveDockWidget(TWITCH_INFO_DOCK_NAME); - main->RemoveDockWidget(TWITCH_STATS_DOCK_NAME); - main->RemoveDockWidget(TWITCH_FEED_DOCK_NAME); -} - -bool TwitchAuth::MakeApiRequest(const char *path, Json &json_out) -{ - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - std::string url = "https://api.twitch.tv/helix/"; - url += std::string(path); - - std::vector headers; - headers.push_back(std::string("Client-ID: ") + client_id); - headers.push_back(std::string("Authorization: Bearer ") + token); - - std::string output; - std::string error; - long error_code = 0; - - bool success = false; - - auto func = [&]() { - success = GetRemoteFile(url.c_str(), output, error, &error_code, - "application/json", "", nullptr, - headers, nullptr, 5); - }; - - ExecThreadedWithoutBlocking( - func, QTStr("Auth.LoadingChannel.Title"), - QTStr("Auth.LoadingChannel.Text").arg(service())); - if (error_code == 403) { - OBSMessageBox::warning(OBSBasic::Get(), - Str("TwitchAuth.TwoFactorFail.Title"), - Str("TwitchAuth.TwoFactorFail.Text"), - true); - blog(LOG_WARNING, "%s: %s", __FUNCTION__, - "Got 403 from Twitch, user probably does not " - "have two-factor authentication enabled on " - "their account"); - return false; - } - - if (!success || output.empty()) - throw ErrorInfo("Failed to get text from remote", error); - - json_out = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - error = json_out["error"].string_value(); - if (!error.empty()) - throw ErrorInfo(error, json_out["message"].string_value()); - - return true; -} - -bool TwitchAuth::GetChannelInfo() -try { - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - if (!GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION)) - return false; - if (token.empty()) - return false; - if (!key_.empty()) - return true; - - Json json; - bool success = MakeApiRequest("users", json); - - if (!success) - return false; - - name = json["data"][0]["login"].string_value(); - - std::string path = "streams/key?broadcaster_id=" + - json["data"][0]["id"].string_value(); - success = MakeApiRequest(path.c_str(), json); - if (!success) - return false; - - key_ = json["data"][0]["stream_key"].string_value(); - - return true; -} catch (ErrorInfo info) { - QString title = QTStr("Auth.ChannelFailure.Title"); - QString text = QTStr("Auth.ChannelFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void TwitchAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "Name", name.c_str()); - config_set_string(main->Config(), service(), "UUID", uuid.c_str()); - - OAuthStreamKey::SaveInternal(); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool TwitchAuth::LoadInternal() -{ - if (!cef) - return false; - - OBSBasic *main = OBSBasic::Get(); - name = get_config_str(main, service(), "Name"); - uuid = get_config_str(main, service(), "UUID"); - - firstLoad = false; - return OAuthStreamKey::LoadInternal(); -} - -static const char *ffz_script = "\ -var ffz = document.createElement('script');\ -ffz.setAttribute('src','https://cdn.frankerfacez.com/script/script.min.js');\ -document.head.appendChild(ffz);"; - -static const char *bttv_script = "\ -localStorage.setItem('bttv_clickTwitchEmotes', true);\ -localStorage.setItem('bttv_darkenedMode', true);\ -localStorage.setItem('bttv_bttvGIFEmotes', true);\ -var bttv = document.createElement('script');\ -bttv.setAttribute('src','https://cdn.betterttv.net/betterttv.js');\ -document.head.appendChild(bttv);"; - -static const char *referrer_script1 = "\ -Object.defineProperty(document, 'referrer', {get : function() { return '"; -static const char *referrer_script2 = "'; }});"; - -void TwitchAuth::LoadUI() -{ - if (!cef) - return; - if (uiLoaded) - return; - if (!GetChannelInfo()) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - /* Twitch panels require a UUID, it does not actually need to be unique, - * and is generated client-side. - * It is only for preferences stored in the browser's local store. */ - if (uuid.empty()) { - QString qtUuid = QUuid::createUuid().toString(); - qtUuid.replace(QRegularExpression("[{}-]"), ""); - uuid = qtUuid.toStdString(); - } - - std::string moderation_tools_url; - moderation_tools_url = "https://www.twitch.tv/"; - moderation_tools_url += name; - moderation_tools_url += "/dashboard/settings/moderation?no-reload=true"; - - /* ----------------------------------- */ - - url = "https://www.twitch.tv/popout/"; - url += name; - url += "/chat"; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - BrowserDock *chat = new BrowserDock(); - chat->setObjectName(TWITCH_CHAT_DOCK_NAME); - chat->resize(300, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, url, panel_cookies); - chat->SetWidget(browser); - cef->add_force_popup_url(moderation_tools_url, chat); - - if (App()->IsThemeDark()) { - script = "localStorage.setItem('twilight.theme', 1);"; - } else { - script = "localStorage.setItem('twilight.theme', 0);"; - } - - const int twAddonChoice = - config_get_int(main->Config(), service(), "AddonChoice"); - if (twAddonChoice) { - if (twAddonChoice & 0x1) - script += bttv_script; - if (twAddonChoice & 0x2) - script += ffz_script; - } - - browser->setStartupScript(script); - - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - chat->setFloating(true); - chat->move(pos.x() + size.width() - chat->width() - 50, pos.y() + 50); - - if (firstLoad) { - chat->setVisible(true); - } else if (!config_has_user_value(main->Config(), "BasicWindow", - "DockState")) { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - - config_set_string(main->Config(), "BasicWindow", "DockState", - dockStateStr); - } - - TryLoadSecondaryUIPanes(); - - uiLoaded = true; -} - -void TwitchAuth::LoadSecondaryUIPanes() -{ - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - if (App()->IsThemeDark()) { - script = "localStorage.setItem('twilight.theme', 1);"; - } else { - script = "localStorage.setItem('twilight.theme', 0);"; - } - script += referrer_script1; - script += "https://www.twitch.tv/"; - script += name; - script += "/dashboard/live"; - script += referrer_script2; - - const int twAddonChoice = - config_get_int(main->Config(), service(), "AddonChoice"); - if (twAddonChoice) { - if (twAddonChoice & 0x1) - script += bttv_script; - if (twAddonChoice & 0x2) - script += ffz_script; - } - - /* ----------------------------------- */ - - url = "https://dashboard.twitch.tv/popout/u/"; - url += name; - url += "/stream-manager/edit-stream-info"; - - BrowserDock *info = new BrowserDock(); - info->setObjectName(TWITCH_INFO_DOCK_NAME); - info->resize(300, 650); - info->setMinimumSize(200, 300); - info->setWindowTitle(QTStr("Auth.StreamInfo")); - info->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(info, url, panel_cookies); - info->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(info, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://www.twitch.tv/popout/"; - url += name; - url += "/dashboard/live/stats"; - - BrowserDock *stats = new BrowserDock(); - stats->setObjectName(TWITCH_STATS_DOCK_NAME); - stats->resize(200, 250); - stats->setMinimumSize(200, 150); - stats->setWindowTitle(QTStr("TwitchAuth.Stats")); - stats->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(stats, url, panel_cookies); - stats->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(stats, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://dashboard.twitch.tv/popout/u/"; - url += name; - url += "/stream-manager/activity-feed"; - url += "?uuid=" + uuid; - - BrowserDock *feed = new BrowserDock(); - feed->setObjectName(TWITCH_FEED_DOCK_NAME); - feed->resize(300, 650); - feed->setMinimumSize(200, 300); - feed->setWindowTitle(QTStr("TwitchAuth.Feed")); - feed->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(feed, url, panel_cookies); - feed->SetWidget(browser); - browser->setStartupScript(script); - - main->AddDockWidget(feed, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - info->setFloating(true); - stats->setFloating(true); - feed->setFloating(true); - - QSize statSize = stats->frameSize(); - - info->move(pos.x() + 50, pos.y() + 50); - stats->move(pos.x() + size.width() / 2 - statSize.width() / 2, - pos.y() + size.height() / 2 - statSize.height() / 2); - feed->move(pos.x() + 100, pos.y() + 100); - - if (firstLoad) { - info->setVisible(true); - stats->setVisible(false); - feed->setVisible(false); - } else { - uint32_t lastVersion = config_get_int(App()->GlobalConfig(), - "General", "LastVersion"); - - if (lastVersion <= MAKE_SEMANTIC_VERSION(23, 0, 2)) { - feed->setVisible(false); - } - - const char *dockStateStr = config_get_string( - main->Config(), "BasicWindow", "DockState"); - QByteArray dockState = - QByteArray::fromBase64(QByteArray(dockStateStr)); - main->RestoreState(dockState); - } -} - -/* Twitch.tv has an OAuth for itself. If we try to load multiple panel pages - * at once before it's OAuth'ed itself, they will all try to perform the auth - * process at the same time, get their own request codes, and only the last - * code will be valid -- so one or more panels are guaranteed to fail. - * - * To solve this, we want to load just one panel first (the chat), and then all - * subsequent panels should only be loaded once we know that Twitch has auth'ed - * itself (if the cookie "auth-token" exists for twitch.tv). - * - * This is annoying to deal with. */ -void TwitchAuth::TryLoadSecondaryUIPanes() -{ - QPointer this_ = this; - - auto cb = [this_](bool found) { - if (!this_) { - return; - } - - if (!found) { - QMetaObject::invokeMethod(&this_->uiLoadTimer, "start"); - } else { - QMetaObject::invokeMethod(this_, - "LoadSecondaryUIPanes"); - } - }; - - panel_cookies->CheckForCookie("https://www.twitch.tv", "auth-token", - cb); -} - -bool TwitchAuth::RetryLogin() -{ - OAuthLogin login(OBSBasic::Get(), TWITCH_AUTH_URL, false); - if (login.exec() == QDialog::Rejected) { - return false; - } - - std::shared_ptr auth = - std::make_shared(twitchDef); - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - return GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()), true); -} - -std::shared_ptr TwitchAuth::Login(QWidget *parent, const std::string &) -{ - OAuthLogin login(parent, TWITCH_AUTH_URL, false); - if (login.exec() == QDialog::Rejected) { - return nullptr; - } - - std::shared_ptr auth = - std::make_shared(twitchDef); - - std::string client_id = TWITCH_CLIENTID; - deobfuscate_str(&client_id[0], TWITCH_HASH); - - if (!auth->GetToken(TWITCH_TOKEN_URL, client_id, TWITCH_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()))) { - return nullptr; - } - - if (auth->GetChannelInfo()) { - return auth; - } - - return nullptr; -} - -static std::shared_ptr CreateTwitchAuth() -{ - return std::make_shared(twitchDef); -} - -static void DeleteCookies() -{ - if (panel_cookies) - panel_cookies->DeleteCookies("twitch.tv", std::string()); -} - -void RegisterTwitchAuth() -{ -#if !defined(__APPLE__) && !defined(_WIN32) - if (QApplication::platformName().contains("wayland")) - return; -#endif - - OAuth::RegisterOAuth(twitchDef, CreateTwitchAuth, TwitchAuth::Login, - DeleteCookies); -} diff --git a/UI/auth-twitch.hpp b/UI/auth-twitch.hpp deleted file mode 100644 index 152b2a95730fd7..00000000000000 --- a/UI/auth-twitch.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include -#include "auth-oauth.hpp" - -class BrowserDock; - -class TwitchAuth : public OAuthStreamKey { - Q_OBJECT - - friend class TwitchLogin; - - bool uiLoaded = false; - - std::string name; - std::string uuid; - - virtual bool RetryLogin() override; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - bool MakeApiRequest(const char *path, json11::Json &json_out); - bool GetChannelInfo(); - - virtual void LoadUI() override; - -public: - TwitchAuth(const Def &d); - ~TwitchAuth(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service_name); - - QTimer uiLoadTimer; - -public slots: - void TryLoadSecondaryUIPanes(); - void LoadSecondaryUIPanes(); -}; diff --git a/UI/cmake/feature-twitch.cmake b/UI/cmake/feature-twitch.cmake deleted file mode 100644 index 099f4c8e07d131..00000000000000 --- a/UI/cmake/feature-twitch.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(TWITCH_CLIENTID - AND TWITCH_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-twitch.cpp auth-twitch.hpp) - target_enable_feature(obs-studio "Twitch API connection" TWITCH_ENABLED) -else() - target_disable_feature(obs-studio "Twitch API connection") - set(TWITCH_CLIENTID "") - set(TWITCH_HASH "0") -endif() diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 3cd183a92fb213..b548e4842f4989 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -24,18 +24,6 @@ set(OAUTH_BASE_URL mark_as_advanced(OAUTH_BASE_URL) -if(NOT DEFINED TWITCH_CLIENTID - OR "${TWITCH_CLIENTID}" STREQUAL "" - OR NOT DEFINED TWITCH_HASH - OR "${TWITCH_HASH}" STREQUAL "" - OR NOT TARGET OBS::browser-panels) - set(TWITCH_ENABLED OFF) - set(TWITCH_CLIENTID "") - set(TWITCH_HASH "0") -else() - set(TWITCH_ENABLED ON) -endif() - if(NOT DEFINED RESTREAM_CLIENTID OR "${RESTREAM_CLIENTID}" STREQUAL "" OR NOT DEFINED RESTREAM_HASH @@ -302,11 +290,6 @@ if(TARGET OBS::browser-panels) target_sources(obs PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp) - if(TWITCH_ENABLED) - target_compile_definitions(obs PRIVATE TWITCH_ENABLED) - target_sources(obs PRIVATE auth-twitch.cpp auth-twitch.hpp) - endif() - if(RESTREAM_ENABLED) target_compile_definitions(obs PRIVATE RESTREAM_ENABLED) target_sources(obs PRIVATE auth-restream.cpp auth-restream.hpp) diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 58a0bbd98d77d0..cd3bb36d87f12f 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -154,10 +154,6 @@ Auth.ChannelFailure.Title="Failed to load channel" Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3" Auth.Chat="Chat" Auth.StreamInfo="Stream Information" -TwitchAuth.Stats="Twitch Stats" -TwitchAuth.Feed="Twitch Activity Feed" -TwitchAuth.TwoFactorFail.Title="Could not query stream key" -TwitchAuth.TwoFactorFail.Text="OBS was unable to connect to your Twitch account. Please make sure two-factor authentication is set up in your Twitch security settings as this is required to stream." RestreamAuth.Channels="Restream Channels" # copy filters diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 67d36230618b9d..968203523a8f2d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -269,7 +269,6 @@ void setupDockAction(QDockWidget *dock) action->connect(action, &QAction::enabledChanged, neverDisable); } -extern void RegisterTwitchAuth(); extern void RegisterRestreamAuth(); OBSBasic::OBSBasic(QWidget *parent) @@ -279,10 +278,7 @@ OBSBasic::OBSBasic(QWidget *parent) { setAttribute(Qt::WA_NativeWindow); -#ifdef TWITCH_ENABLED - RegisterTwitchAuth(); -#endif -#ifdef RESTREAM_ENABLED +#if RESTREAM_ENABLED RegisterRestreamAuth(); #endif From c860751ad857083c453cd29402c113a56d3ba170 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 5 Jul 2023 12:42:54 +0200 Subject: [PATCH 42/65] oauth-service: Add OBS Browser login --- .../obs-browser-login/CMakeLists.txt | 15 +++ .../oauth-obs-browser-login.cpp | 100 ++++++++++++++++++ .../oauth-obs-browser-login.hpp | 44 ++++++++ 3 files changed, 159 insertions(+) create mode 100644 deps/oauth-service/obs-browser-login/CMakeLists.txt create mode 100644 deps/oauth-service/obs-browser-login/oauth-obs-browser-login.cpp create mode 100644 deps/oauth-service/obs-browser-login/oauth-obs-browser-login.hpp diff --git a/deps/oauth-service/obs-browser-login/CMakeLists.txt b/deps/oauth-service/obs-browser-login/CMakeLists.txt new file mode 100644 index 00000000000000..77e8db42c433b9 --- /dev/null +++ b/deps/oauth-service/obs-browser-login/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +find_qt(COMPONENTS Core Widgets) + +if(NOT TARGET OBS::oauth) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/oauth" "${CMAKE_BINARY_DIR}/deps/oauth-service/oauth") +endif() + +add_library(oauth-obs-browser-login INTERFACE) +add_library(OBS::oauth-obs-browser-login ALIAS oauth-obs-browser-login) + +target_sources(oauth-obs-browser-login INTERFACE oauth-obs-browser-login.cpp oauth-obs-browser-login.hpp) +target_include_directories(oauth-obs-browser-login INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(oauth-obs-browser-login INTERFACE OBS::libobs OBS::frontend-api OBS::oauth Qt::Core Qt::Widgets) diff --git a/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.cpp b/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.cpp new file mode 100644 index 00000000000000..4974ef4e04252e --- /dev/null +++ b/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "oauth-obs-browser-login.hpp" + +#include +#include +#include +#include + +#include +#include + +OAuth::OBSBrowserLogin::OBSBrowserLogin(const QString &baseUrl_, + const std::string &urlPath_, + const QStringList &popupWhitelistUrls_, + QWidget *parent) + : QDialog(parent), + baseUrl(baseUrl_), + url(baseUrl_.toStdString() + urlPath_) +{ + if (!popupWhitelistUrls_.empty()) { + QString popupWhitelistUrlsStr = popupWhitelistUrls_.join(";"); + popupWhitelistUrls = strlist_split( + popupWhitelistUrlsStr.toUtf8().constData(), ';', false); + } + + setWindowTitle("Auth"); + setMinimumSize(400, 400); + resize(700, 700); + + setWindowFlag(Qt::WindowContextHelpButtonHint, false); + + QPushButton *close = new QPushButton(tr("Cancel")); + connect(close, &QAbstractButton::clicked, this, &QDialog::reject); + + QHBoxLayout *bottomLayout = new QHBoxLayout(); + bottomLayout->addStretch(); + bottomLayout->addWidget(close); + bottomLayout->addStretch(); + + QVBoxLayout *topLayout = new QVBoxLayout(); + topLayout->addLayout(bottomLayout); + + setLayout(topLayout); +} + +void OAuth::OBSBrowserLogin::UrlChanged(const QString &url) +{ + std::string uri = "code="; + qsizetype code_idx = url.indexOf(uri.c_str()); + if (code_idx == -1) + return; + + if (!url.startsWith(baseUrl)) + return; + + code_idx += (int)uri.size(); + + qsizetype next_idx = url.indexOf("&", code_idx); + if (next_idx != -1) + code = url.mid(code_idx, next_idx - code_idx); + else + code = url.right(url.size() - code_idx); + + accept(); +} + +int OAuth::OBSBrowserLogin::exec() +{ + code.clear(); + lastError.clear(); + + obs_frontend_browser_params params = {0}; + params.url = url.c_str(); + params.popup_whitelist_urls = (const char **)popupWhitelistUrls.Get(); + params.enable_cookie = true; + + cefWidget.reset((QWidget *)obs_frontend_get_browser_widget(¶ms)); + if (cefWidget.isNull()) { + lastError = "No CEF widget generated"; + return QDialog::Rejected; + } + + /* Only method to connect QCefWidget signal without requiring obs-browser + * headers */ + connect(cefWidget.get(), SIGNAL(titleChanged(const QString &)), this, + SLOT(setWindowTitle(const QString &))); + connect(cefWidget.get(), SIGNAL(urlChanged(const QString &)), this, + SLOT(UrlChanged(const QString &))); + + QVBoxLayout *layout = (QVBoxLayout *)this->layout(); + layout->insertWidget(0, cefWidget.get()); + + int ret = QDialog::exec(); + + cefWidget.reset(); + return ret; +} diff --git a/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.hpp b/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.hpp new file mode 100644 index 00000000000000..817a1ba4d3bfcc --- /dev/null +++ b/deps/oauth-service/obs-browser-login/oauth-obs-browser-login.hpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +namespace OAuth { + +class OBSBrowserLogin : public QDialog { + Q_OBJECT + + QScopedPointer cefWidget; + + QString baseUrl; + std::string url; + + BPtr popupWhitelistUrls = nullptr; + + QString code; + + QString lastError; + +public: + OBSBrowserLogin(const QString &baseUrl, const std::string &urlPath, + const QStringList &popupWhitelistUrls, QWidget *parent); + ~OBSBrowserLogin() {} + + inline QString GetCode() { return code; } + + inline QString GetLastError() { return lastError; } + +private slots: + void UrlChanged(const QString &url); + +public slots: + int exec() override; +}; + +} From c5a82df309abc7713fc4b1c7fe7fc6073c7fca07 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 15:20:42 +0200 Subject: [PATCH 43/65] obs-twitch: Add account integration --- plugins/obs-twitch/CMakeLists.txt | 2 + plugins/obs-twitch/cmake/feature-oauth.cmake | 47 ++ plugins/obs-twitch/data/locale/en-US.ini | 25 + plugins/obs-twitch/twitch-api.hpp | 112 +++++ plugins/obs-twitch/twitch-browser-widget.cpp | 111 +++++ plugins/obs-twitch/twitch-browser-widget.hpp | 36 ++ plugins/obs-twitch/twitch-config.cpp | 205 +++++++- plugins/obs-twitch/twitch-config.hpp | 6 + plugins/obs-twitch/twitch-oauth.cpp | 471 +++++++++++++++++++ plugins/obs-twitch/twitch-oauth.hpp | 83 ++++ plugins/obs-twitch/twitch-service.cpp | 122 +++++ plugins/obs-twitch/twitch-service.hpp | 25 + 12 files changed, 1243 insertions(+), 2 deletions(-) create mode 100644 plugins/obs-twitch/cmake/feature-oauth.cmake create mode 100644 plugins/obs-twitch/twitch-browser-widget.cpp create mode 100644 plugins/obs-twitch/twitch-browser-widget.hpp create mode 100644 plugins/obs-twitch/twitch-oauth.cpp create mode 100644 plugins/obs-twitch/twitch-oauth.hpp diff --git a/plugins/obs-twitch/CMakeLists.txt b/plugins/obs-twitch/CMakeLists.txt index 3ffbca6a33262f..a04602e4ca4753 100644 --- a/plugins/obs-twitch/CMakeLists.txt +++ b/plugins/obs-twitch/CMakeLists.txt @@ -23,6 +23,8 @@ target_sources( target_link_libraries(obs-twitch PRIVATE OBS::libobs OBS::oauth nlohmann_json::nlohmann_json) +include(cmake/feature-oauth.cmake) + if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-twitch.rc) target_sources(obs-twitch PRIVATE obs-twitch.rc) diff --git a/plugins/obs-twitch/cmake/feature-oauth.cmake b/plugins/obs-twitch/cmake/feature-oauth.cmake new file mode 100644 index 00000000000000..d59df8a9a04dd1 --- /dev/null +++ b/plugins/obs-twitch/cmake/feature-oauth.cmake @@ -0,0 +1,47 @@ +if(TWITCH_CLIENTID AND TWITCH_HASH MATCHES "(0|[a-fA-F0-9]+)") + if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") + endif() + if(NOT TARGET OBS::oauth-obs-browser-login) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/obs-browser-login" + "${CMAKE_BINARY_DIR}/deps/oauth-service/obs-browser-login") + endif() + if(NOT TARGET OBS::oauth-service-base) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/service-base" + "${CMAKE_BINARY_DIR}/deps/oauth-service/service-base") + endif() + + if(NOT OAUTH_BASE_URL) + set(OAUTH_BASE_URL + "https://auth.obsproject.com/" + CACHE STRING "Default OAuth base URL") + + mark_as_advanced(OAUTH_BASE_URL) + endif() + + find_qt(COMPONENTS Core Widgets) + + target_sources( + obs-twitch PRIVATE # cmake-format: sortable + twitch-browser-widget.cpp twitch-browser-widget.hpp twitch-oauth.cpp twitch-oauth.hpp) + + target_link_libraries(obs-twitch PRIVATE OBS::obf OBS::oauth-service-base OBS::oauth-obs-browser-login Qt::Core + Qt::Widgets) + + target_compile_definitions(obs-twitch PRIVATE OAUTH_ENABLED OAUTH_BASE_URL="${OAUTH_BASE_URL}" + TWITCH_CLIENTID="${TWITCH_CLIENTID}" TWITCH_HASH=0x${TWITCH_HASH}) + + set_target_properties( + obs-twitch + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON) + + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_enable_feature(obs-twitch "Twitch OAuth connection") + endif() +else() + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable_feature(obs-twitch "Twitch OAuth connection") + endif() +endif() diff --git a/plugins/obs-twitch/data/locale/en-US.ini b/plugins/obs-twitch/data/locale/en-US.ini index 65af478d2b3d19..0c97ea77bb4474 100644 --- a/plugins/obs-twitch/data/locale/en-US.ini +++ b/plugins/obs-twitch/data/locale/en-US.ini @@ -6,3 +6,28 @@ Twitch.StreamKey="Stream Key" Twitch.GetStreamKey="Get Stream Key" Twitch.UseStreamKey="Use Stream Key" Twitch.EnforceBandwidthTestMode="Enforce Bandwidth Test Mode" + +Twitch.Auth.Connect="Connect Account (recommended)" +Twitch.Auth.Disconnect="Disconnect Account" +Twitch.Auth.ConnectedAccount="Connected account" +Twitch.Auth.LoginError.Title="Authentication Failure" +Twitch.Auth.LoginError.Text="Failed to authenticate:\n\n%1" +Twitch.Auth.LoginError.Text2="Failed to authenticate:\n\n%1: %2" +Twitch.Auth.SignOutDialog.Title="Disconnect Account?" +Twitch.Auth.SignOutDialog.Text="This change will apply immediately. Are you sure you want to disconnect your account?" +Twitch.Auth.ReLoginDialog.Title="Twitch Re-Login Required" +Twitch.Auth.ReLoginDialog.Text="%1 A re-login is required to keep the integration enabled. Proceed ?" +Twitch.Auth.ReLoginDialog.ScopeChange="The authentication requirements for Twitch have changed." +Twitch.Auth.ReLoginDialog.RefreshTokenFailed="The Twitch access token could not be refreshed." +Twitch.Auth.ReLoginDialog.ProfileDuplication="The Twitch service has been duplicated." + +Twitch.Addon="Chat Add-Ons" +Twitch.Addon.None="None" +Twitch.Addon.BTTV="BetterTTV" +Twitch.Addon.FFZ="FrankerFaceZ" +Twitch.Addon.Both="BetterTTV and FrankerFaceZ" + +Twitch.Dock.Chat="Chat" +Twitch.Dock.StreamInfo="Stream Information" +Twitch.Dock.Stats="Twitch Stats" +Twitch.Dock.Feed="Twitch Activity Feed" diff --git a/plugins/obs-twitch/twitch-api.hpp b/plugins/obs-twitch/twitch-api.hpp index 4ae1b9896015c6..836b544b5d22fe 100644 --- a/plugins/obs-twitch/twitch-api.hpp +++ b/plugins/obs-twitch/twitch-api.hpp @@ -9,6 +9,21 @@ namespace TwitchApi { using nlohmann::json; +#ifdef OAUTH_ENABLED +#ifndef NLOHMANN_OPTIONAL_TwitchApi_HELPER +#define NLOHMANN_OPTIONAL_TwitchApi_HELPER +template +inline std::optional get_stack_optional(const json &j, const char *property) +{ + auto it = j.find(property); + if (it != j.end() && !it->is_null()) { + return j.at(property).get>(); + } + return std::optional(); +} +#endif +#endif + /* NOTE: "Reserved for internal use" and "_id" values are stripped */ struct Ingest { std::string name; @@ -32,4 +47,101 @@ inline void from_json(const json &j, IngestResponse &s) s.ingests = j.at("ingests").get>(); } +#ifdef OAUTH_ENABLED +enum class UserType : int { + NORMAL, + STAFF, + GLOBAL_MOD, + ADMIN, +}; + +enum class BroadcasterType : int { + NORMAL, + PARTNER, + AFFILIATE, +}; + +struct User { + std::string id; + std::string login; + std::string displayName; + UserType type; + BroadcasterType broadcasterType; + std::string description; + std::string profileImageUrl; + std::string offlineImageUrl; + /* Available if "user:read:email" scope */ + std::optional email; + std::string createdAt; +}; + +struct UsersResponse { + std::vector data; +}; + +struct StreamKey { + std::string streamKey; +}; + +struct StreamKeyResponse { + std::vector data; +}; + +inline void from_json(const json &j, UserType &e) +{ + if (j == "") + e = UserType::NORMAL; + else if (j == "staff") + e = UserType::STAFF; + else if (j == "global_mod") + e = UserType::GLOBAL_MOD; + else if (j == "admin") + e = UserType::ADMIN; + else { + throw std::runtime_error("Unknown \"type\" value"); + } +} + +inline void from_json(const json &j, BroadcasterType &e) +{ + if (j == "") + e = BroadcasterType::NORMAL; + else if (j == "partner") + e = BroadcasterType::PARTNER; + else if (j == "affiliate") + e = BroadcasterType::AFFILIATE; + else { + throw std::runtime_error("Unknown \"broadcaster_type\" value"); + } +} + +inline void from_json(const json &j, User &s) +{ + s.id = j.at("id").get(); + s.login = j.at("login").get(); + s.displayName = j.at("display_name").get(); + s.type = j.at("type").get(); + s.broadcasterType = j.at("broadcaster_type").get(); + s.description = j.at("description").get(); + s.profileImageUrl = j.at("profile_image_url").get(); + s.offlineImageUrl = j.at("offline_image_url").get(); + s.email = get_stack_optional(j, "email"); + s.createdAt = j.at("created_at").get(); +} + +inline void from_json(const json &j, UsersResponse &s) +{ + s.data = j.at("data").get>(); +} + +inline void from_json(const json &j, StreamKey &s) +{ + s.streamKey = j.at("stream_key").get(); +} + +inline void from_json(const json &j, StreamKeyResponse &s) +{ + s.data = j.at("data").get>(); +} +#endif } diff --git a/plugins/obs-twitch/twitch-browser-widget.cpp b/plugins/obs-twitch/twitch-browser-widget.cpp new file mode 100644 index 00000000000000..58a0860d5cb0b1 --- /dev/null +++ b/plugins/obs-twitch/twitch-browser-widget.cpp @@ -0,0 +1,111 @@ +#include "twitch-browser-widget.hpp" + +#include + +#include +#include +#include +#include + +constexpr const char *FFZ_SCRIPT = "\ +var ffz = document.createElement('script');\ +ffz.setAttribute('src','https://cdn.frankerfacez.com/script/script.min.js');\ +document.head.appendChild(ffz);"; + +constexpr const char *BTTV_SCRIPT = "\ +localStorage.setItem('bttv_clickTwitchEmotes', true);\ +localStorage.setItem('bttv_darkenedMode', true);\ +localStorage.setItem('bttv_bttvGIFEmotes', true);\ +var bttv = document.createElement('script');\ +bttv.setAttribute('src','https://cdn.betterttv.net/betterttv.js');\ +document.head.appendChild(bttv);"; + +TwitchBrowserWidget::TwitchBrowserWidget(const Addon &addon_, + const QString &url_, + const QString &startupScriptBase_, + const QStringList &forcePopupUrl_, + const QStringList &popupWhitelistUrl_) + : addon(addon_), + url(url_.toStdString()), + startupScriptBase(startupScriptBase_.toStdString()), + QWidget() +{ + setMinimumSize(200, 300); + + QString urlList = forcePopupUrl_.join(";"); + forcePopupUrl = strlist_split(urlList.toUtf8().constData(), ';', false); + + urlList = popupWhitelistUrl_.join(";"); + popupWhitelistUrl = + strlist_split(urlList.toUtf8().constData(), ';', false); + + QVBoxLayout *layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + + setLayout(layout); +} + +void TwitchBrowserWidget::SetAddon(const Addon &newAddon) +{ + if (addon == newAddon) + return; + + addon = newAddon; + + if (!cefWidget.isNull()) + QMetaObject::invokeMethod(this, "UpdateCefWidget", + Qt::QueuedConnection); +} + +void TwitchBrowserWidget::showEvent(QShowEvent *event) +{ + UpdateCefWidget(); + + QWidget::showEvent(event); +} + +void TwitchBrowserWidget::hideEvent(QHideEvent *event) +{ + cefWidget.reset(nullptr); + QWidget::hideEvent(event); +} + +void TwitchBrowserWidget::UpdateCefWidget() +{ + obs_frontend_browser_params params = {0}; + + startupScript = obs_frontend_is_theme_dark() + ? "localStorage.setItem('twilight.theme', 1);" + : "localStorage.setItem('twilight.theme', 0);"; + startupScript += startupScriptBase; + + switch (addon) { + case NONE: + break; + case BOTH: + case FFZ: + startupScript += FFZ_SCRIPT; + if (addon != BOTH) + break; + [[fallthrough]]; + case BTTV: + startupScript += BTTV_SCRIPT; + break; + } + + params.url = url.c_str(); + params.startup_script = startupScript.c_str(); + params.enable_cookie = true; + + if (forcePopupUrl) + params.force_popup_urls = (const char **)forcePopupUrl.Get(); + if (popupWhitelistUrl) + params.popup_whitelist_urls = + (const char **)popupWhitelistUrl.Get(); + + cefWidget.reset((QWidget *)obs_frontend_get_browser_widget(¶ms)); + + QVBoxLayout *layout = (QVBoxLayout *)this->layout(); + layout->addWidget(cefWidget.get()); + cefWidget->setParent(this); +} diff --git a/plugins/obs-twitch/twitch-browser-widget.hpp b/plugins/obs-twitch/twitch-browser-widget.hpp new file mode 100644 index 00000000000000..591633f3fb4ea2 --- /dev/null +++ b/plugins/obs-twitch/twitch-browser-widget.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +class TwitchBrowserWidget : public QWidget { + Q_OBJECT + +public: + enum Addon : int { NONE = 0, FFZ = 0x1, BTTV = 0x2, BOTH = FFZ | BTTV }; + +private: + std::string url; + std::string startupScriptBase; + std::string startupScript; + BPtr forcePopupUrl = nullptr; + BPtr popupWhitelistUrl = nullptr; + + QScopedPointer cefWidget; + + Addon addon; + +public: + TwitchBrowserWidget(const Addon &addon, const QString &url, + const QString &startupScriptBase, + const QStringList &forcePopupUrl, + const QStringList &popupWhitelistUrl); + ~TwitchBrowserWidget() {} + void SetAddon(const Addon &newAddon); + + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; +private slots: + void UpdateCefWidget(); +}; diff --git a/plugins/obs-twitch/twitch-config.cpp b/plugins/obs-twitch/twitch-config.cpp index aad05c4e1a7070..1801e4bc7beb74 100644 --- a/plugins/obs-twitch/twitch-config.cpp +++ b/plugins/obs-twitch/twitch-config.cpp @@ -4,6 +4,10 @@ #include "twitch-config.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + constexpr const char *STREAM_KEY_LINK = "https://dashboard.twitch.tv/settings/stream"; constexpr const char *DEFAULT_RTMPS_INGEST = "rtmps://live.twitch.tv/app"; @@ -14,7 +18,20 @@ constexpr const char *BANDWIDTH_TEST = "?bandwidthtest=true"; TwitchConfig::TwitchConfig(obs_data_t *settings, obs_service_t *self) : typeData(reinterpret_cast( obs_service_get_type_data(self))) +#ifdef OAUTH_ENABLED + , + serviceObj(self) + +#endif { +#ifdef OAUTH_ENABLED + if (!obs_data_has_user_value(settings, "uuid")) { + BPtr newUuid = os_generate_uuid(); + obs_data_set_string(settings, "uuid", newUuid); + } + + uuid = obs_data_get_string(settings, "uuid"); +#endif Update(settings); } @@ -28,13 +45,39 @@ void TwitchConfig::Update(obs_data_t *settings) if (!serverAuto) serverUrl = obs_data_get_string(settings, "server"); +#ifdef OAUTH_ENABLED + std::string newUuid = obs_data_get_string(settings, "uuid"); + + if (newUuid != uuid) { + if (oauth) { + typeData->ReleaseOAuth(uuid, serviceObj); + oauth = nullptr; + } + + uuid = newUuid; + } + if (!oauth) + oauth = typeData->GetOAuth(uuid, serviceObj); + + if (!oauth->Connected()) { + streamKey = obs_data_get_string(settings, "stream_key"); + } + + oauth->SetAddon((TwitchBrowserWidget::Addon)obs_data_get_int( + settings, "chat_addon")); +#else streamKey = obs_data_get_string(settings, "stream_key"); bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; - +#endif enforceBandwidthTest = obs_data_get_bool(settings, "enforce_bwtest"); } -TwitchConfig::~TwitchConfig() {} +TwitchConfig::~TwitchConfig() +{ +#ifdef OAUTH_ENABLED + typeData->ReleaseOAuth(uuid, serviceObj); +#endif +} void TwitchConfig::InfoGetDefault(obs_data_t *settings) { @@ -65,6 +108,13 @@ const char *TwitchConfig::ConnectInfo(uint32_t type) break; case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: +#ifdef OAUTH_ENABLED + if (oauth->Connected() && streamKey.empty()) { + if (oauth->GetStreamKey(streamKey)) + bandwidthTestStreamKey = + streamKey + BANDWIDTH_TEST; + } +#endif if (BandwidthTestEnabled() ? !bandwidthTestStreamKey.empty() : !streamKey.empty()) { return BandwidthTestEnabled() @@ -81,8 +131,23 @@ const char *TwitchConfig::ConnectInfo(uint32_t type) bool TwitchConfig::CanTryToConnect() { +#ifdef OAUTH_ENABLED + if (!serverAuto && serverUrl.empty()) + return false; + + if (oauth->Connected()) { + bool ret = oauth->GetStreamKey(streamKey); + if (!streamKey.empty()) + bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; + + return ret; + } else { + return !streamKey.empty(); + } +#else if ((!serverAuto && serverUrl.empty()) || streamKey.empty()) return false; +#endif return true; } @@ -122,11 +187,109 @@ static bool ModifiedProtocolCb(void *priv, obs_properties_t *props, return true; } +#ifdef OAUTH_ENABLED + +static inline void AddDisplayName(obs_properties_t *props, + TwitchApi::ServiceOAuth *oauth) +{ + TwitchApi::User info; + if (oauth->GetUser(info)) { + std::string displayName = ""; + displayName += info.displayName; + displayName += ""; + obs_property_t *p = + obs_properties_get(props, "connected_account"); + obs_property_set_long_description(p, displayName.c_str()); + obs_property_set_visible(p, true); + } +} + +static bool ConnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + TwitchApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->Login()) + return false; + + AddDisplayName(props, oauth); + + obs_property_set_visible(obs_properties_get(props, "connect"), false); + obs_property_set_visible(obs_properties_get(props, "disconnect"), true); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), true); + + return true; +} + +static bool UseStreamKeyCb(obs_properties_t *props, obs_property_t *, void *) +{ + obs_property_set_visible(obs_properties_get(props, "connect"), + obs_frontend_is_browser_available()); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "connected_account"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), true); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + true); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), + false); + + return true; +} + +static bool DisconnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + TwitchApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->SignOut()) + return false; + + if (!obs_frontend_is_browser_available()) + return UseStreamKeyCb(props, nullptr, nullptr); + + obs_property_set_visible(obs_properties_get(props, "connect"), true); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + true); + obs_property_set_visible(obs_properties_get(props, "connected_account"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), + false); + + return true; +} +#endif + obs_properties_t *TwitchConfig::GetProperties() { obs_properties_t *ppts = obs_properties_create(); obs_property_t *p; +#ifdef OAUTH_ENABLED + bool connected = oauth->Connected(); + p = obs_properties_add_text( + ppts, "connected_account", + obs_module_text("Twitch.Auth.ConnectedAccount"), OBS_TEXT_INFO); + obs_property_set_visible(p, false); + if (connected) + AddDisplayName(ppts, oauth); +#endif + p = obs_properties_add_list(ppts, "protocol", obs_module_text("Twitch.Protocol"), OBS_COMBO_TYPE_LIST, @@ -147,6 +310,9 @@ obs_properties_t *TwitchConfig::GetProperties() p = obs_properties_add_text(ppts, "stream_key", obs_module_text("Twitch.StreamKey"), OBS_TEXT_PASSWORD); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); +#endif p = obs_properties_add_button(ppts, "get_stream_key", obs_module_text("Twitch.GetStreamKey"), @@ -154,6 +320,41 @@ obs_properties_t *TwitchConfig::GetProperties() obs_property_button_set_type(p, OBS_BUTTON_URL); obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); + + p = obs_properties_add_list(ppts, "chat_addon", + obs_module_text("Twitch.Addon"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(p, obs_module_text("Twitch.Addon.None"), + TwitchBrowserWidget::NONE); + obs_property_list_add_int(p, obs_module_text("Twitch.Addon.BTTV"), + TwitchBrowserWidget::BTTV); + obs_property_list_add_int(p, obs_module_text("Twitch.Addon.FFZ"), + TwitchBrowserWidget::FFZ); + obs_property_list_add_int(p, obs_module_text("Twitch.Addon.Both"), + TwitchBrowserWidget::BOTH); + obs_property_set_visible(p, connected); + + p = obs_properties_add_button2(ppts, "connect", + obs_module_text("Twitch.Auth.Connect"), + ConnectCb, oauth); + obs_property_set_visible(p, !connected); + + p = obs_properties_add_button2( + ppts, "disconnect", obs_module_text("Twitch.Auth.Disconnect"), + DisconnectCb, oauth); + obs_property_set_visible(p, connected); + + p = obs_properties_add_button(ppts, "use_stream_key", + obs_module_text("Twitch.UseStreamKey"), + UseStreamKeyCb); + obs_property_set_visible(p, streamKey.empty() && !connected); + + if (!obs_frontend_is_browser_available() && !connected) + UseStreamKeyCb(ppts, nullptr, nullptr); +#endif + obs_properties_add_bool( ppts, "enforce_bwtest", obs_module_text("Twitch.EnforceBandwidthTestMode")); diff --git a/plugins/obs-twitch/twitch-config.hpp b/plugins/obs-twitch/twitch-config.hpp index bee8c34eaab286..266f289c63cbab 100644 --- a/plugins/obs-twitch/twitch-config.hpp +++ b/plugins/obs-twitch/twitch-config.hpp @@ -11,6 +11,12 @@ class TwitchConfig { TwitchService *typeData; +#ifdef OAUTH_ENABLED + obs_service_t *serviceObj; + + std::string uuid; + TwitchApi::ServiceOAuth *oauth = nullptr; +#endif std::string protocol; bool serverAuto; diff --git a/plugins/obs-twitch/twitch-oauth.cpp b/plugins/obs-twitch/twitch-oauth.cpp new file mode 100644 index 00000000000000..b9b8db0e851264 --- /dev/null +++ b/plugins/obs-twitch/twitch-oauth.cpp @@ -0,0 +1,471 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "twitch-oauth.hpp" + +#include +#include +#include + +#include +#include +#include + +constexpr const char *AUTH_BASE_URL = (const char *)OAUTH_BASE_URL; +constexpr const char *AUTH_ENDPOINT = "v1/twitch/redirect"; +constexpr const char *TOKEN_URL = (const char *)OAUTH_BASE_URL + "v1/twitch/token"; +constexpr const char *API_URL = "https://api.twitch.tv/helix/"; + +constexpr const char *CLIENT_ID = (const char *)TWITCH_CLIENTID; +constexpr uint64_t CLIENT_ID_HASH = TWITCH_HASH; +constexpr int64_t SCOPE_VERSION = 1; + +constexpr const char *CHAT_URL = "https://www.twitch.tv/popout/%1/chat"; +constexpr const char *MODERATION_TOOLS_URL = + "https://www.twitch.tv/%1/dashboard/settings/moderation?no-reload=true"; +constexpr const char *INFO_URL = + "https://dashboard.twitch.tv/popout/u/%1/stream-manager/edit-stream-info"; +constexpr const char *STATS_URL = + "https://www.twitch.tv/popout/%1/dashboard/live/stats"; +constexpr const char *FEED_URL = + "https://dashboard.twitch.tv/popout/u/%1/stream-manager/activity-feed?uuid=%2"; + +constexpr const char *REFERRER_SCRIPT = + "\ +Object.defineProperty(document, 'referrer', {get : function() { return '" + "https://www.twitch.tv/%1/dashboard/live" + "'; }});"; + +constexpr const char *CHAT_DOCK_NAME = "twitchChat"; +constexpr const char *INFO_DOCK_NAME = "twitchInfo"; +constexpr const char *STATS_DOCK_NAME = "twitchStats"; +constexpr const char *FEED_DOCK_NAME = "twitchFeed"; + +namespace TwitchApi { + +std::string ServiceOAuth::UserAgent() +{ + std::string userAgent("obs-twitch "); + userAgent += obs_get_version_string(); + + return userAgent; +} + +const char *ServiceOAuth::TokenUrl() +{ + return TOKEN_URL; +} + +std::string ServiceOAuth::ClientId() +{ + std::string clientId = CLIENT_ID; + deobfuscate_str(&clientId[0], CLIENT_ID_HASH); + + return clientId; +} + +int64_t ServiceOAuth::ScopeVersion() +{ + return SCOPE_VERSION; +} + +static inline void WarningBox(const QString &title, const QString &text, + QWidget *parent) +{ + QMessageBox warn(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *ok = warn.addButton(QMessageBox::Ok); + ok->setText(ok->tr("Ok")); + warn.exec(); +} + +static int LoginConfirmation(const OAuth::LoginReason &reason, QWidget *parent) +{ + const char *reasonText = nullptr; + switch (reason) { + case OAuth::LoginReason::CONNECT: + return QMessageBox::Ok; + case OAuth::LoginReason::SCOPE_CHANGE: + reasonText = obs_module_text( + "Twitch.Auth.ReLoginDialog.ScopeChange"); + break; + case OAuth::LoginReason::REFRESH_TOKEN_FAILED: + reasonText = obs_module_text( + "Twitch.Auth.ReLoginDialog.RefreshTokenFailed"); + break; + case OAuth::LoginReason::PROFILE_DUPLICATION: + reasonText = obs_module_text( + "Twitch.Auth.ReLoginDialog.ProfileDuplication"); + break; + } + + QString title = QString::fromUtf8( + obs_module_text("Twitch.Auth.ReLoginDialog.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("Twitch.Auth.ReLoginDialog.Text")) + .arg(reasonText); + + QMessageBox ques(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + return ques.exec(); +} + +bool ServiceOAuth::LoginInternal(const OAuth::LoginReason &reason, + std::string &code, std::string &) +{ + if (!obs_frontend_is_browser_available()) { + blog(LOG_ERROR, + "[%s][%s]: Browser is not available, unable to login", + PluginLogName(), __FUNCTION__); + return false; + } + + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + if (reason != OAuth::LoginReason::CONNECT && + LoginConfirmation(reason, parent) != QMessageBox::Ok) { + return false; + } + + OAuth::OBSBrowserLogin login(AUTH_BASE_URL, AUTH_ENDPOINT, {}, parent); + + if (login.exec() != QDialog::Accepted) { + QString error = login.GetLastError(); + + if (!error.isEmpty()) { + blog(LOG_ERROR, "[%s][%s]: %s", PluginLogName(), + __FUNCTION__, error.toUtf8().constData()); + + QString title = QString::fromUtf8( + obs_module_text("Twitch.Auth.LoginError.Title"), + -1); + QString text = + QString::fromUtf8( + obs_module_text( + "Twitch.Auth.LoginError.Text"), + -1) + .arg(error); + + WarningBox(title, text, parent); + } + + return false; + } + + code = login.GetCode().toStdString(); + + return true; +} + +bool ServiceOAuth::SignOutInternal() +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + QString title = QString::fromUtf8( + obs_module_text("Twitch.Auth.SignOutDialog.Title"), -1); + QString text = QString::fromUtf8( + obs_module_text("Twitch.Auth.SignOutDialog.Text"), -1); + + QMessageBox ques(QMessageBox::Question, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + if (ques.exec() != QMessageBox::Yes) + return false; + + obs_frontend_delete_browser_cookie("twitch.tv"); + + return true; +} + +void ServiceOAuth::SetSettings(obs_data_t *data) +{ + feedUuid = obs_data_get_string(data, "feed_uuid"); +} + +obs_data_t *ServiceOAuth::GetSettingsData() +{ + OBSData data = obs_data_create(); + + if (Connected() && !feedUuid.isEmpty()) + obs_data_set_string(data, "feed_uuid", + feedUuid.toUtf8().constData()); + + return data; +} + +void ServiceOAuth::LoadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + if (chat || info || stats || feed) { + blog(LOG_ERROR, "[%s][%s] The docks were not unloaded", + PluginLogName(), __FUNCTION__); + return; + } + + TwitchApi::User user; + if (!GetUser(user)) + return; + + QString login = QString::fromStdString(userInfo.login); + QString url = QString::fromUtf8(CHAT_URL, -1).arg(login); + QString startupScript = + QString::fromUtf8(REFERRER_SCRIPT, -1).arg(login); + + QStringList forcePopupUrl; + forcePopupUrl << QString::fromUtf8(MODERATION_TOOLS_URL, -1).arg(login); + + QStringList popupWhitelistUrl; + popupWhitelistUrl + << "https://twitch.tv/popout/frankerfacez/chat?ffz-settings"; + /* enables javascript-based popups. basically bttv popups */ + popupWhitelistUrl << "about:blank#blocked"; + + chat = new TwitchBrowserWidget(addon, url, "", forcePopupUrl, + popupWhitelistUrl); + chat->resize(300, 600); + obs_frontend_add_dock_by_id(CHAT_DOCK_NAME, + obs_module_text("Twitch.Dock.Chat"), chat); + + url = QString::fromUtf8(INFO_URL, -1).arg(login); + info = new TwitchBrowserWidget(addon, url, startupScript, {}, {}); + info->resize(300, 650); + obs_frontend_add_dock_by_id(INFO_DOCK_NAME, + obs_module_text("Twitch.Dock.StreamInfo"), + info); + + url = QString::fromUtf8(STATS_URL, -1).arg(login); + stats = new TwitchBrowserWidget(addon, url, startupScript, {}, {}); + stats->resize(200, 250); + obs_frontend_add_dock_by_id( + STATS_DOCK_NAME, obs_module_text("Twitch.Dock.Stats"), stats); + + if (feedUuid.isEmpty()) + feedUuid = QUuid::createUuid().toString(QUuid::Id128); + + url = QString::fromUtf8(FEED_URL, -1).arg(login).arg(feedUuid); + feed = new TwitchBrowserWidget(addon, url, startupScript, {}, {}); + feed->resize(300, 650); + obs_frontend_add_dock_by_id(FEED_DOCK_NAME, + obs_module_text("Twitch.Dock.Feed"), feed); +} + +void ServiceOAuth::SetAddon(const TwitchBrowserWidget::Addon &newAddon) +{ + if (addon == newAddon) + return; + + addon = newAddon; + + if (chat) + chat->SetAddon(addon); + + if (info) + info->SetAddon(addon); + + if (stats) + stats->SetAddon(addon); + + if (feed) + feed->SetAddon(addon); +} + +void ServiceOAuth::UnloadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + obs_frontend_remove_dock(CHAT_DOCK_NAME); + chat = nullptr; + obs_frontend_remove_dock(INFO_DOCK_NAME); + info = nullptr; + obs_frontend_remove_dock(STATS_DOCK_NAME); + stats = nullptr; + obs_frontend_remove_dock(FEED_DOCK_NAME); + feed = nullptr; +} + +void ServiceOAuth::DuplicationResetInternal() +{ + obs_frontend_delete_browser_cookie("twitch.tv"); +} + +void ServiceOAuth::LoginError(RequestError &error) +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + QString title = QString::fromUtf8( + obs_module_text("Twitch.Auth.LoginError.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("Twitch.Auth.LoginError.Text2"), -1) + .arg(QString::fromStdString(error.message)) + .arg(QString::fromStdString(error.error)); + + WarningBox(title, text, parent); +} + +bool ServiceOAuth::WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + std::string error; + CURLcode curlCode = + GetRemoteFile(UserAgent().c_str(), url, str, error, + responseCode, contentType, request_type, postData, + {"Client-ID: " + ClientId(), + "Authorization: Bearer " + AccessToken()}, + nullptr, 5, postDataSize); + + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + lastError = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + error); + return false; + } + + if (*responseCode == 200) + return true; + + return false; +} + +bool ServiceOAuth::TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + long responseCode = 0; + if (WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize)) { + return true; + } + + if (lastError.type == RequestErrorType::CURL_REQUEST_FAILED) { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.message.c_str(), lastError.error.c_str()); + } else if (lastError.type == + RequestErrorType::ERROR_JSON_PARSING_FAILED) { + blog(LOG_ERROR, "[%s][%s] HTTP status: %ld\n%s", + PluginLogName(), funcName, responseCode, str.c_str()); + } else { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.error.c_str(), lastError.message.c_str()); + } + + if (responseCode != 401) + return false; + + lastError = RequestError(); + if (!RefreshAccessToken(lastError)) + return false; + + return WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize); +} + +bool ServiceOAuth::GetUser(User &user) +{ + if (!userInfo.id.empty()) { + user = userInfo; + return true; + } + + std::string url = API_URL; + url += "users"; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, "[%s][%s] Failed to retrieve user info", + PluginLogName(), __FUNCTION__); + return false; + } + + UsersResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.data.empty()) { + lastError = RequestError("No user found", ""); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + userInfo = response.data[0]; + user = userInfo; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + return false; +} + +bool ServiceOAuth::GetStreamKey(std::string &streamKey_) +{ + if (!streamKey.empty()) { + streamKey_ = streamKey; + return true; + } + + std::string url = API_URL; + url += "streams/key?broadcaster_id=" + userInfo.id; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, "[%s][%s] Failed to retrieve stream key", + PluginLogName(), __FUNCTION__); + return false; + } + + StreamKeyResponse response; + try { + response = nlohmann::json::parse(output); + + if (response.data.empty()) { + lastError = RequestError("No stream key found", ""); + blog(LOG_ERROR, "[%s][%s] %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str()); + return false; + } + + streamKey = response.data[0].streamKey; + streamKey_ = streamKey; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + return false; +} + +} diff --git a/plugins/obs-twitch/twitch-oauth.hpp b/plugins/obs-twitch/twitch-oauth.hpp new file mode 100644 index 00000000000000..25b923b89d0f8e --- /dev/null +++ b/plugins/obs-twitch/twitch-oauth.hpp @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "twitch-api.hpp" +#include "twitch-browser-widget.hpp" + +namespace TwitchApi { + +class ServiceOAuth : public OAuth::ServiceBase { + RequestError lastError; + + User userInfo; + + std::string streamKey; + + TwitchBrowserWidget::Addon addon = TwitchBrowserWidget::Addon::NONE; + + TwitchBrowserWidget *chat = nullptr; + TwitchBrowserWidget *info = nullptr; + TwitchBrowserWidget *stats = nullptr; + TwitchBrowserWidget *feed = nullptr; + QString feedUuid; + + std::string UserAgent() override; + + const char *TokenUrl() override; + + std::string ClientId() override; + + int64_t ScopeVersion() override; + + bool LoginInternal(const OAuth::LoginReason &reason, std::string &code, + std::string &redirectUri) override; + bool SignOutInternal() override; + + void SetSettings(obs_data_t *data) override; + obs_data_t *GetSettingsData() override; + + void LoadFrontendInternal() override; + void UnloadFrontendInternal() override; + + void DuplicationResetInternal() override; + + const char *PluginLogName() override { return "obs-twitch"; }; + + void LoginError(RequestError &error) override; + + bool WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + + bool TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + +public: + ServiceOAuth() : OAuth::ServiceBase() {} + + void SetAddon(const TwitchBrowserWidget::Addon &newAddon); + + RequestError GetLastError() { return lastError; } + + bool GetUser(User &user); + bool GetStreamKey(std::string &streamKey); +}; + +} diff --git a/plugins/obs-twitch/twitch-service.cpp b/plugins/obs-twitch/twitch-service.cpp index 9fb70b6a871002..1b5ea67b219d21 100644 --- a/plugins/obs-twitch/twitch-service.cpp +++ b/plugins/obs-twitch/twitch-service.cpp @@ -4,8 +4,16 @@ #include "twitch-service.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + #include "twitch-config.hpp" +#ifdef OAUTH_ENABLED +constexpr const char *DATA_FILENAME = "obs-twitch.json"; +#endif + TwitchService::TwitchService() { info.type_data = this; @@ -37,6 +45,10 @@ TwitchService::TwitchService() info.bandwidth_test_enabled = InfoBandwidthTestEnabled; obs_register_service(&info); + +#ifdef OAUTH_ENABLED + obs_frontend_add_event_callback(OBSEvent, this); +#endif } void TwitchService::Register() @@ -108,3 +120,113 @@ std::vector TwitchService::GetIngests(bool refresh) return {}; } + +#ifdef OAUTH_ENABLED +void TwitchService::OBSEvent(obs_frontend_event event, void *priv) +{ + TwitchService *self = reinterpret_cast(priv); + + switch (event) { + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + case OBS_FRONTEND_EVENT_FINISHED_LOADING: { + self->deferUiFunction = false; + break; + } + case OBS_FRONTEND_EVENT_PROFILE_CHANGING: + self->SaveOAuthsData(); + self->deferUiFunction = true; + self->data = nullptr; + break; + case OBS_FRONTEND_EVENT_EXIT: + self->SaveOAuthsData(); + obs_frontend_remove_event_callback(OBSEvent, priv); + return; + default: + break; + } + + if (self->oauths.empty()) + return; + + for (auto const &p : self->oauths) + p.second->OBSEvent(event); +} + +void TwitchService::SaveOAuthsData() +{ + OBSDataAutoRelease saveData = obs_data_create(); + bool writeData = false; + for (auto const &[uuid, oauth] : oauths) { + OBSDataAutoRelease data = oauth->GetData(); + if (data == nullptr) + continue; + + obs_data_set_obj(saveData, uuid.c_str(), data); + writeData = true; + } + + if (!writeData) + return; + + BPtr profilePath = obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + if (!obs_data_save_json_pretty_safe(saveData, dataPath.c_str(), "tmp", + "bak")) + blog(LOG_ERROR, + "[obs-twitch][%s] Failed to save integrations data", + __FUNCTION__); +}; + +TwitchApi::ServiceOAuth *TwitchService::GetOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (data == nullptr) { + BPtr profilePath = + obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + data = obs_data_create_from_json_file_safe(dataPath.c_str(), + "bak"); + + if (!data) { + blog(LOG_DEBUG, + "[obs-twitch][%s] Failed to open integrations data: %s", + __FUNCTION__, dataPath.c_str()); + } + } + + if (oauths.count(uuid) == 0) { + OBSDataAutoRelease oauthData = + obs_data_get_obj(data, uuid.c_str()); + oauths.emplace(uuid, + std::make_shared()); + oauths[uuid]->Setup(oauthData, deferUiFunction); + oauthsRefCounter.emplace(uuid, 0); + oauths[uuid]->AddBondedService(service); + } + + oauthsRefCounter[uuid] += 1; + return oauths[uuid].get(); +} + +void TwitchService::ReleaseOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (oauthsRefCounter.count(uuid) == 0) + return; + + oauths[uuid]->RemoveBondedService(service); + oauthsRefCounter[uuid] -= 1; + + if (oauthsRefCounter[uuid] != 0) + return; + + oauths.erase(uuid); + oauthsRefCounter.erase(uuid); +} +#endif diff --git a/plugins/obs-twitch/twitch-service.hpp b/plugins/obs-twitch/twitch-service.hpp index 50c63022087961..9688331b97e7bd 100644 --- a/plugins/obs-twitch/twitch-service.hpp +++ b/plugins/obs-twitch/twitch-service.hpp @@ -8,6 +8,14 @@ #include "twitch-api.hpp" +#ifdef OAUTH_ENABLED +#include +#include +#include + +#include "twitch-oauth.hpp" +#endif + class TwitchService { obs_service_info info = {0}; @@ -34,6 +42,17 @@ class TwitchService { static void InfoEnableBandwidthTest(void *data, bool enabled); static bool InfoBandwidthTestEnabled(void *data); +#ifdef OAUTH_ENABLED + OBSDataAutoRelease data = nullptr; + + std::unordered_map> + oauths; + std::unordered_map oauthsRefCounter; + bool deferUiFunction = true; + + void SaveOAuthsData(); + static void OBSEvent(obs_frontend_event event, void *priv); +#endif public: TwitchService(); ~TwitchService() {} @@ -41,4 +60,10 @@ class TwitchService { static void Register(); std::vector GetIngests(bool refresh = false); + +#ifdef OAUTH_ENABLED + TwitchApi::ServiceOAuth *GetOAuth(const std::string &uuid, + obs_service_t *service); + void ReleaseOAuth(const std::string &uuid, obs_service_t *service); +#endif }; From 0455aa2b97cf6498c115ec7e36fc6767b3668f43 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 16:18:35 +0200 Subject: [PATCH 44/65] plugin: Add obs-restream --- plugins/CMakeLists.txt | 2 + plugins/obs-restream/CMakeLists.txt | 36 +++++++ .../obs-restream/cmake/macos/Info.plist.in | 28 +++++ .../cmake/windows/obs-module.rc.in | 24 +++++ plugins/obs-restream/data/locale/en-US.ini | 4 + plugins/obs-restream/obs-restream.cpp | 16 +++ plugins/obs-restream/restream-api.hpp | 24 +++++ plugins/obs-restream/restream-config.cpp | 99 +++++++++++++++++ plugins/obs-restream/restream-config.hpp | 41 +++++++ .../obs-restream/restream-service-info.cpp | 98 +++++++++++++++++ plugins/obs-restream/restream-service.cpp | 100 ++++++++++++++++++ plugins/obs-restream/restream-service.hpp | 44 ++++++++ 12 files changed, 516 insertions(+) create mode 100644 plugins/obs-restream/CMakeLists.txt create mode 100644 plugins/obs-restream/cmake/macos/Info.plist.in create mode 100644 plugins/obs-restream/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-restream/data/locale/en-US.ini create mode 100644 plugins/obs-restream/obs-restream.cpp create mode 100644 plugins/obs-restream/restream-api.hpp create mode 100644 plugins/obs-restream/restream-config.cpp create mode 100644 plugins/obs-restream/restream-config.hpp create mode 100644 plugins/obs-restream/restream-service-info.cpp create mode 100644 plugins/obs-restream/restream-service.cpp create mode 100644 plugins/obs-restream/restream-service.hpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 11a1dbc33fb4c3..55c4e525a1be09 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -70,6 +70,7 @@ if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) if(OS_WINDOWS) add_subdirectory(obs-qsv11) endif() + add_subdirectory(obs-restream) add_subdirectory(obs-services) if(OS_WINDOWS) add_subdirectory(obs-text) @@ -206,3 +207,4 @@ add_subdirectory(custom-services) add_subdirectory(orphaned-services) add_subdirectory(obs-youtube) add_subdirectory(obs-twitch) +add_subdirectory(obs-restream) diff --git a/plugins/obs-restream/CMakeLists.txt b/plugins/obs-restream/CMakeLists.txt new file mode 100644 index 00000000000000..4cc0a7e672dd12 --- /dev/null +++ b/plugins/obs-restream/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +add_library(obs-restream MODULE) +add_library(OBS::obs-restream ALIAS obs-restream) + +# Use the included GetRemoteFile function +if(NOT TARGET OBS::oauth) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/oauth" "${CMAKE_BINARY_DIR}/deps/oauth-service/oauth") +endif() + +find_package(nlohmann_json) + +target_sources( + obs-restream + PRIVATE # cmake-format: sortable + obs-restream.cpp + restream-api.hpp + restream-config.cpp + restream-config.hpp + restream-service-info.cpp + restream-service.cpp + restream-service.hpp) + +target_link_libraries(obs-restream PRIVATE OBS::libobs OBS::oauth nlohmann_json::nlohmann_json) + +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-restream.rc) + target_sources(obs-restream PRIVATE obs-restream.rc) +endif() + +if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + set_target_properties_obs(obs-restream PROPERTIES FOLDER plugins PREFIX "") +else() + set_target_properties(obs-restream PROPERTIES FOLDER "plugins" PREFIX "") + setup_plugin_target(obs-restream) +endif() diff --git a/plugins/obs-restream/cmake/macos/Info.plist.in b/plugins/obs-restream/cmake/macos/Info.plist.in new file mode 100644 index 00000000000000..75c4e86e36c188 --- /dev/null +++ b/plugins/obs-restream/cmake/macos/Info.plist.in @@ -0,0 +1,28 @@ + + + + + CFBundleName + obs-youtube + CFBundleIdentifier + com.obsproject.obs-restream + CFBundleVersion + ${MACOSX_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_SHORT_VERSION_STRING} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + obs-youtube + CFBundlePackageType + BNDL + CFBundleSupportedPlatforms + + MacOSX + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + (c) 2012-${CURRENT_YEAR} Lain Bailey + + diff --git a/plugins/obs-restream/cmake/windows/obs-module.rc.in b/plugins/obs-restream/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..a748ed14b55972 --- /dev/null +++ b/plugins/obs-restream/cmake/windows/obs-module.rc.in @@ -0,0 +1,24 @@ +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "OBS Restream service" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-restream" + VALUE "OriginalFilename", "obs-restream" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-restream/data/locale/en-US.ini b/plugins/obs-restream/data/locale/en-US.ini new file mode 100644 index 00000000000000..d1fa2ddeddb3e9 --- /dev/null +++ b/plugins/obs-restream/data/locale/en-US.ini @@ -0,0 +1,4 @@ +Restream.Protocol="Protocol" +Restream.Server="Server" +Restream.StreamKey="Stream Key" +Restream.GetStreamKey="Get Stream Key" diff --git a/plugins/obs-restream/obs-restream.cpp b/plugins/obs-restream/obs-restream.cpp new file mode 100644 index 00000000000000..6a1f795dc00f98 --- /dev/null +++ b/plugins/obs-restream/obs-restream.cpp @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include "restream-service.hpp" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-restream", "en-US") + +bool obs_module_load(void) +{ + RestreamService::Register(); + return true; +} diff --git a/plugins/obs-restream/restream-api.hpp b/plugins/obs-restream/restream-api.hpp new file mode 100644 index 00000000000000..1d3899beee8ffc --- /dev/null +++ b/plugins/obs-restream/restream-api.hpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +namespace RestreamApi { +using nlohmann::json; + +/* NOTE: Geographic, "url" and "id" values are stripped */ +struct Ingest { + std::string name; + std::string rtmpUrl; +}; + +inline void from_json(const json &j, Ingest &s) +{ + s.name = j.at("name").get(); + s.rtmpUrl = j.at("rtmpUrl").get(); +} + +} diff --git a/plugins/obs-restream/restream-config.cpp b/plugins/obs-restream/restream-config.cpp new file mode 100644 index 00000000000000..d39f0a8adc60f0 --- /dev/null +++ b/plugins/obs-restream/restream-config.cpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "restream-config.hpp" + +constexpr const char *STREAM_KEY_LINK = + "https://restream.io/settings/streaming-setup?from=OBS"; +constexpr const char *AUTODETECT_INGEST = "rtmp://live.restream.io/live"; + +constexpr const char *BANDWIDTH_TEST = "?test=true"; + +RestreamConfig::RestreamConfig(obs_data_t *settings, obs_service_t *self) + : typeData(reinterpret_cast( + obs_service_get_type_data(self))) +{ + Update(settings); +} + +void RestreamConfig::Update(obs_data_t *settings) +{ + server = obs_data_get_string(settings, "server"); + + streamKey = obs_data_get_string(settings, "stream_key"); + bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; +} + +RestreamConfig::~RestreamConfig() {} + +void RestreamConfig::InfoGetDefault(obs_data_t *settings) +{ + obs_data_set_string(settings, "server", AUTODETECT_INGEST); +} + +const char *RestreamConfig::ConnectInfo(uint32_t type) +{ + switch ((enum obs_service_connect_info)type) { + case OBS_SERVICE_CONNECT_INFO_SERVER_URL: + if (!server.empty()) + return server.c_str(); + + break; + case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: + if (bandwidthTest ? !bandwidthTestStreamKey.empty() + : !streamKey.empty()) { + return bandwidthTest ? bandwidthTestStreamKey.c_str() + : streamKey.c_str(); + } + break; + default: + break; + } + + return nullptr; +} + +bool RestreamConfig::CanTryToConnect() +{ + if (server.empty() || streamKey.empty()) + return false; + + return true; +} + +obs_properties_t *RestreamConfig::GetProperties() +{ + obs_properties_t *ppts = obs_properties_create(); + obs_property_t *p; + + p = obs_properties_add_list(ppts, "server", + obs_module_text("Restream.Server"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + std::vector ingests = typeData->GetIngests(); + + if (ingests.empty()) { + obs_property_list_add_string(p, "Autodetect", + AUTODETECT_INGEST); + } else { + + for (const RestreamApi::Ingest &ingest : ingests) { + obs_property_list_add_string(p, ingest.name.c_str(), + ingest.rtmpUrl.c_str()); + } + } + + p = obs_properties_add_text(ppts, "stream_key", + obs_module_text("Restream.StreamKey"), + OBS_TEXT_PASSWORD); + + p = obs_properties_add_button(ppts, "get_stream_key", + obs_module_text("Restream.GetStreamKey"), + nullptr); + obs_property_button_set_type(p, OBS_BUTTON_URL); + obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); + + return ppts; +} diff --git a/plugins/obs-restream/restream-config.hpp b/plugins/obs-restream/restream-config.hpp new file mode 100644 index 00000000000000..61589a68457860 --- /dev/null +++ b/plugins/obs-restream/restream-config.hpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "restream-service.hpp" + +class RestreamConfig { + RestreamService *typeData; + + std::string server; + + std::string streamKey; + std::string bandwidthTestStreamKey; + + bool bandwidthTest = false; + +public: + RestreamConfig(obs_data_t *settings, obs_service_t *self); + ~RestreamConfig(); + + void Update(obs_data_t *settings); + + static void InfoGetDefault(obs_data_t *settings); + + const char *Protocol() { return "RTMP"; }; + + const char *ConnectInfo(uint32_t type); + + bool CanTryToConnect(); + + bool CanBandwidthTest() { return true; } + void EnableBandwidthTest(bool enabled) { bandwidthTest = enabled; } + bool BandwidthTestEnabled() { return bandwidthTest; } + + obs_properties_t *GetProperties(); +}; diff --git a/plugins/obs-restream/restream-service-info.cpp b/plugins/obs-restream/restream-service-info.cpp new file mode 100644 index 00000000000000..d0c8fd308e31c2 --- /dev/null +++ b/plugins/obs-restream/restream-service-info.cpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "restream-service.hpp" + +#include "restream-config.hpp" + +static const char *SUPPORTED_VIDEO_CODECS[] = {"h264", NULL}; + +void RestreamService::InfoFreeTypeData(void *typeData) +{ + if (typeData) + delete reinterpret_cast(typeData); +} + +const char *RestreamService::InfoGetName(void *) +{ + return "Restream.io"; +} + +void *RestreamService::InfoCreate(obs_data_t *settings, obs_service_t *service) +{ + return reinterpret_cast(new RestreamConfig(settings, service)); + ; +} + +void RestreamService::InfoDestroy(void *data) +{ + if (data) + delete reinterpret_cast(data); +} + +void RestreamService::InfoUpdate(void *data, obs_data_t *settings) +{ + RestreamConfig *priv = reinterpret_cast(data); + if (priv) + priv->Update(settings); +} + +const char *RestreamService::InfoGetConnectInfo(void *data, uint32_t type) +{ + RestreamConfig *priv = reinterpret_cast(data); + if (priv) + return priv->ConnectInfo(type); + return nullptr; +} + +const char *RestreamService::InfoGetProtocol(void *data) +{ + RestreamConfig *priv = reinterpret_cast(data); + if (priv) + return priv->Protocol(); + return nullptr; +} + +const char **RestreamService::InfoGetSupportedVideoCodecs(void *) +{ + return SUPPORTED_VIDEO_CODECS; +} + +bool RestreamService::InfoCanTryToConnect(void *data) +{ + RestreamConfig *priv = reinterpret_cast(data); + if (priv) + return priv->CanTryToConnect(); + return false; +} + +obs_properties_t *RestreamService::InfoGetProperties(void *data) +{ + if (data) + return reinterpret_cast(data)->GetProperties(); + return nullptr; +} + +bool RestreamService::InfoCanBandwidthTest(void *data) +{ + if (data) + return reinterpret_cast(data) + ->CanBandwidthTest(); + return false; +} + +void RestreamService::InfoEnableBandwidthTest(void *data, bool enabled) +{ + if (data) + return reinterpret_cast(data) + ->EnableBandwidthTest(enabled); +} + +bool RestreamService::InfoBandwidthTestEnabled(void *data) +{ + if (data) + return reinterpret_cast(data) + ->BandwidthTestEnabled(); + return false; +} diff --git a/plugins/obs-restream/restream-service.cpp b/plugins/obs-restream/restream-service.cpp new file mode 100644 index 00000000000000..4a4cfb5254b5af --- /dev/null +++ b/plugins/obs-restream/restream-service.cpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "restream-service.hpp" + +#include "restream-config.hpp" + +RestreamService::RestreamService() +{ + info.type_data = this; + info.free_type_data = InfoFreeTypeData; + + info.id = "restream"; + info.supported_protocols = "RTMP"; + + info.get_name = InfoGetName; + info.create = InfoCreate; + info.destroy = InfoDestroy; + info.update = InfoUpdate; + + info.get_connect_info = InfoGetConnectInfo; + + info.get_protocol = InfoGetProtocol; + + info.get_supported_video_codecs = InfoGetSupportedVideoCodecs; + + info.can_try_to_connect = InfoCanTryToConnect; + + info.flags = 0; + + info.get_defaults = RestreamConfig::InfoGetDefault; + info.get_properties = InfoGetProperties; + + info.can_bandwidth_test = InfoCanBandwidthTest; + info.enable_bandwidth_test = InfoEnableBandwidthTest; + info.bandwidth_test_enabled = InfoBandwidthTestEnabled; + + obs_register_service(&info); +} + +void RestreamService::Register() +{ + new RestreamService(); +} + +std::vector RestreamService::GetIngests(bool refresh) +{ + if (!ingests.empty() && !refresh) + return ingests; + + std::string output; + std::string errorStr; + long responseCode = 0; + std::string userAgent("obs-restream "); + userAgent += obs_get_version_string(); + + CURLcode curlCode = GetRemoteFile( + userAgent.c_str(), "https://api.restream.io/v2/server/all", + output, errorStr, &responseCode, + "application/x-www-form-urlencoded", "", nullptr, + std::vector(), nullptr, 5); + + RequestError error; + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + error = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + errorStr); + blog(LOG_ERROR, "[obs-restream][%s] %s: %s", __FUNCTION__, + error.message.c_str(), error.error.c_str()); + return {}; + } + + std::vector response; + switch (responseCode) { + case 200: + try { + response = nlohmann::json::parse(output); + ingests = response; + + return ingests; + } catch (nlohmann::json::exception &e) { + error = RequestError( + RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[obs-restream][%s] %s: %s", + __FUNCTION__, error.message.c_str(), + error.error.c_str()); + } + break; + default: + error = RequestError( + RequestErrorType::UNMANAGED_HTTP_RESPONSE_CODE, + errorStr); + blog(LOG_ERROR, "[obs-restream][%s] HTTP status: %ld", + __FUNCTION__, responseCode); + break; + } + + return {}; +} diff --git a/plugins/obs-restream/restream-service.hpp b/plugins/obs-restream/restream-service.hpp new file mode 100644 index 00000000000000..f370c7c5cbb75f --- /dev/null +++ b/plugins/obs-restream/restream-service.hpp @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include "restream-api.hpp" + +class RestreamService { + obs_service_info info = {0}; + + std::vector ingests; + + static void InfoFreeTypeData(void *typeData); + + static const char *InfoGetName(void *typeData); + static void *InfoCreate(obs_data_t *settings, obs_service_t *service); + static void InfoDestroy(void *data); + static void InfoUpdate(void *data, obs_data_t *settings); + + static const char *InfoGetConnectInfo(void *data, uint32_t type); + + static const char *InfoGetProtocol(void *data); + + static const char **InfoGetSupportedVideoCodecs(void *data); + + static bool InfoCanTryToConnect(void *data); + + static obs_properties_t *InfoGetProperties(void *data); + + static bool InfoCanBandwidthTest(void *data); + static void InfoEnableBandwidthTest(void *data, bool enabled); + static bool InfoBandwidthTestEnabled(void *data); + +public: + RestreamService(); + ~RestreamService() {} + + static void Register(); + + std::vector GetIngests(bool refresh = false); +}; From 14e5aa333faaebdda3b02af3af489cf640e7641b Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 18:11:35 +0200 Subject: [PATCH 45/65] UI: Remove integrated Restream integration --- UI/CMakeLists.txt | 7 - UI/auth-restream.cpp | 288 -------------------------------- UI/auth-restream.hpp | 27 --- UI/cmake/feature-restream.cmake | 10 -- UI/cmake/legacy.cmake | 23 --- UI/data/locale/en-US.ini | 1 - UI/window-basic-main.cpp | 6 - 7 files changed, 362 deletions(-) delete mode 100644 UI/auth-restream.cpp delete mode 100644 UI/auth-restream.hpp delete mode 100644 UI/cmake/feature-restream.cmake diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index c60e3fad60eae8..243bd09341472a 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -49,13 +49,6 @@ include(cmake/ui-windows.cmake) include(cmake/feature-importers.cmake) include(cmake/feature-browserpanels.cmake) -if(NOT OAUTH_BASE_URL) - # cmake-format: off - set(OAUTH_BASE_URL "https://auth.obsproject.com/" CACHE STRING "Default OAuth base URL") - mark_as_advanced(OAUTH_BASE_URL) - # cmake-format: on -endif() -include(cmake/feature-restream.cmake) include(cmake/feature-sparkle.cmake) include(cmake/feature-whatsnew.cmake) diff --git a/UI/auth-restream.cpp b/UI/auth-restream.cpp deleted file mode 100644 index de3ab60e21309a..00000000000000 --- a/UI/auth-restream.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "auth-restream.hpp" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include "window-dock-browser.hpp" -#include "window-basic-main.hpp" -#include "remote-text.hpp" -#include "ui-config.h" -#include - -using namespace json11; - -/* ------------------------------------------------------------------------- */ - -#define RESTREAM_AUTH_URL OAUTH_BASE_URL "v1/restream/redirect" -#define RESTREAM_TOKEN_URL OAUTH_BASE_URL "v1/restream/token" -#define RESTREAM_STREAMKEY_URL "https://api.restream.io/v2/user/streamKey" -#define RESTREAM_SCOPE_VERSION 1 - -#define RESTREAM_CHAT_DOCK_NAME "restreamChat" -#define RESTREAM_INFO_DOCK_NAME "restreamInfo" -#define RESTREAM_CHANNELS_DOCK_NAME "restreamChannel" - -static Auth::Def restreamDef = {"Restream", Auth::Type::OAuth_StreamKey}; - -/* ------------------------------------------------------------------------- */ - -RestreamAuth::RestreamAuth(const Def &d) : OAuthStreamKey(d) {} - -RestreamAuth::~RestreamAuth() -{ - if (!uiLoaded) - return; - - OBSBasic *main = OBSBasic::Get(); - - main->RemoveDockWidget(RESTREAM_CHAT_DOCK_NAME); - main->RemoveDockWidget(RESTREAM_INFO_DOCK_NAME); - main->RemoveDockWidget(RESTREAM_CHANNELS_DOCK_NAME); -} - -bool RestreamAuth::GetChannelInfo() -try { - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - if (!GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION)) - return false; - if (token.empty()) - return false; - if (!key_.empty()) - return true; - - std::string auth; - auth += "Authorization: Bearer "; - auth += token; - - std::vector headers; - headers.push_back(std::string("Client-ID: ") + client_id); - headers.push_back(std::move(auth)); - - std::string output; - std::string error; - Json json; - bool success; - - auto func = [&]() { - success = GetRemoteFile(RESTREAM_STREAMKEY_URL, output, error, - nullptr, "application/json", "", - nullptr, headers, nullptr, 5); - }; - - ExecThreadedWithoutBlocking( - func, QTStr("Auth.LoadingChannel.Title"), - QTStr("Auth.LoadingChannel.Text").arg(service())); - if (!success || output.empty()) - throw ErrorInfo("Failed to get stream key from remote", error); - - json = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - error = json["error"].string_value(); - if (!error.empty()) - throw ErrorInfo(error, - json["error_description"].string_value()); - - key_ = json["streamKey"].string_value(); - - return true; -} catch (ErrorInfo info) { - QString title = QTStr("Auth.ChannelFailure.Title"); - QString text = QTStr("Auth.ChannelFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void RestreamAuth::SaveInternal() -{ - OAuthStreamKey::SaveInternal(); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool RestreamAuth::LoadInternal() -{ - firstLoad = false; - return OAuthStreamKey::LoadInternal(); -} - -void RestreamAuth::LoadUI() -{ - if (uiLoaded) - return; - if (!GetChannelInfo()) - return; - - OBSBasic::InitBrowserPanelSafeBlock(); - OBSBasic *main = OBSBasic::Get(); - - QCefWidget *browser; - std::string url; - std::string script; - - /* ----------------------------------- */ - - url = "https://restream.io/chat-application"; - - QSize size = main->frameSize(); - QPoint pos = main->pos(); - - BrowserDock *chat = new BrowserDock(); - chat->setObjectName(RESTREAM_CHAT_DOCK_NAME); - chat->resize(420, 600); - chat->setMinimumSize(200, 300); - chat->setWindowTitle(QTStr("Auth.Chat")); - chat->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(chat, url, panel_cookies); - chat->SetWidget(browser); - - main->AddDockWidget(chat, Qt::RightDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://restream.io/titles/embed"; - - BrowserDock *info = new BrowserDock(); - info->setObjectName(RESTREAM_INFO_DOCK_NAME); - info->resize(410, 600); - info->setMinimumSize(200, 150); - info->setWindowTitle(QTStr("Auth.StreamInfo")); - info->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(info, url, panel_cookies); - info->SetWidget(browser); - - main->AddDockWidget(info, Qt::LeftDockWidgetArea); - - /* ----------------------------------- */ - - url = "https://restream.io/channel/embed"; - - BrowserDock *channels = new BrowserDock(); - channels->setObjectName(RESTREAM_CHANNELS_DOCK_NAME); - channels->resize(410, 600); - channels->setMinimumSize(410, 300); - channels->setWindowTitle(QTStr("RestreamAuth.Channels")); - channels->setAllowedAreas(Qt::AllDockWidgetAreas); - - browser = cef->create_widget(channels, url, panel_cookies); - channels->SetWidget(browser); - - main->AddDockWidget(channels, Qt::LeftDockWidgetArea); - - /* ----------------------------------- */ - - chat->setFloating(true); - info->setFloating(true); - channels->setFloating(true); - - chat->move(pos.x() + size.width() - chat->width() - 30, pos.y() + 60); - info->move(pos.x() + 20, pos.y() + 60); - channels->move(pos.x() + 20 + info->width() + 10, pos.y() + 60); - - if (firstLoad) { - chat->setVisible(true); - info->setVisible(true); - channels->setVisible(true); - } else if (!config_has_user_value(main->Config(), "BasicWindow", - "DockState")) { - const char *dockStateStr = config_get_string( - main->Config(), service(), "DockState"); - - config_set_string(main->Config(), "BasicWindow", "DockState", - dockStateStr); - } - - uiLoaded = true; -} - -bool RestreamAuth::RetryLogin() -{ - OAuthLogin login(OBSBasic::Get(), RESTREAM_AUTH_URL, false); - cef->add_popup_whitelist_url("about:blank", &login); - if (login.exec() == QDialog::Rejected) { - return false; - } - - std::shared_ptr auth = - std::make_shared(restreamDef); - - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - return GetToken(RESTREAM_TOKEN_URL, client_id, RESTREAM_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()), true); -} - -std::shared_ptr RestreamAuth::Login(QWidget *parent, const std::string &) -{ - OAuthLogin login(parent, RESTREAM_AUTH_URL, false); - cef->add_popup_whitelist_url("about:blank", &login); - - if (login.exec() == QDialog::Rejected) { - return nullptr; - } - - std::shared_ptr auth = - std::make_shared(restreamDef); - - std::string client_id = RESTREAM_CLIENTID; - deobfuscate_str(&client_id[0], RESTREAM_HASH); - - if (!auth->GetToken(RESTREAM_TOKEN_URL, client_id, - RESTREAM_SCOPE_VERSION, - QT_TO_UTF8(login.GetCode()))) { - return nullptr; - } - - std::string error; - if (auth->GetChannelInfo()) { - return auth; - } - - return nullptr; -} - -static std::shared_ptr CreateRestreamAuth() -{ - return std::make_shared(restreamDef); -} - -static void DeleteCookies() -{ - if (panel_cookies) { - panel_cookies->DeleteCookies("restream.io", std::string()); - } -} - -void RegisterRestreamAuth() -{ -#if !defined(__APPLE__) && !defined(_WIN32) - if (QApplication::platformName().contains("wayland")) - return; -#endif - - OAuth::RegisterOAuth(restreamDef, CreateRestreamAuth, - RestreamAuth::Login, DeleteCookies); -} diff --git a/UI/auth-restream.hpp b/UI/auth-restream.hpp deleted file mode 100644 index 334906c42f7a5e..00000000000000 --- a/UI/auth-restream.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "auth-oauth.hpp" - -class BrowserDock; - -class RestreamAuth : public OAuthStreamKey { - Q_OBJECT - - bool uiLoaded = false; - - virtual bool RetryLogin() override; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - bool GetChannelInfo(); - - virtual void LoadUI() override; - -public: - RestreamAuth(const Def &d); - ~RestreamAuth(); - - static std::shared_ptr Login(QWidget *parent, - const std::string &service_name); -}; diff --git a/UI/cmake/feature-restream.cmake b/UI/cmake/feature-restream.cmake deleted file mode 100644 index 813d45971c005b..00000000000000 --- a/UI/cmake/feature-restream.cmake +++ /dev/null @@ -1,10 +0,0 @@ -if(RESTREAM_CLIENTID - AND RESTREAM_HASH MATCHES "(0|[a-fA-F0-9]+)" - AND TARGET OBS::browser-panels) - target_sources(obs-studio PRIVATE auth-restream.cpp auth-restream.hpp) - target_enable_feature(obs-studio "Restream API connection" RESTREAM_ENABLED) -else() - target_disable_feature(obs-studio "Restream API connection") - set(RESTREAM_CLIENTID "") - set(RESTREAM_HASH "0") -endif() diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index b548e4842f4989..196313eac059a9 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -18,24 +18,6 @@ if(TARGET obs-browser target_include_directories(obs-browser-panels INTERFACE ${CMAKE_SOURCE_DIR}/plugins/obs-browser/panel) endif() -set(OAUTH_BASE_URL - "https://auth.obsproject.com/" - CACHE STRING "Default OAuth base URL") - -mark_as_advanced(OAUTH_BASE_URL) - -if(NOT DEFINED RESTREAM_CLIENTID - OR "${RESTREAM_CLIENTID}" STREQUAL "" - OR NOT DEFINED RESTREAM_HASH - OR "${RESTREAM_HASH}" STREQUAL "" - OR NOT TARGET OBS::browser-panels) - set(RESTREAM_ENABLED OFF) - set(RESTREAM_CLIENTID "") - set(RESTREAM_HASH "0") -else() - set(RESTREAM_ENABLED ON) -endif() - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ui-config.h.in ${CMAKE_CURRENT_BINARY_DIR}/ui-config.h) find_package(FFmpeg REQUIRED COMPONENTS avcodec avutil avformat) @@ -290,11 +272,6 @@ if(TARGET OBS::browser-panels) target_sources(obs PRIVATE window-dock-browser.cpp window-dock-browser.hpp window-extra-browsers.cpp window-extra-browsers.hpp) - if(RESTREAM_ENABLED) - target_compile_definitions(obs PRIVATE RESTREAM_ENABLED) - target_sources(obs PRIVATE auth-restream.cpp auth-restream.hpp) - endif() - if(OS_WINDOWS OR OS_MACOS) set(ENABLE_WHATSNEW ON diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index cd3bb36d87f12f..2aa69bf07ea2ef 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -154,7 +154,6 @@ Auth.ChannelFailure.Title="Failed to load channel" Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3" Auth.Chat="Chat" Auth.StreamInfo="Stream Information" -RestreamAuth.Channels="Restream Channels" # copy filters Copy.Filters="Copy Filters" diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 968203523a8f2d..2e5d74d005c5eb 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -269,8 +269,6 @@ void setupDockAction(QDockWidget *dock) action->connect(action, &QAction::enabledChanged, neverDisable); } -extern void RegisterRestreamAuth(); - OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), @@ -278,10 +276,6 @@ OBSBasic::OBSBasic(QWidget *parent) { setAttribute(Qt::WA_NativeWindow); -#if RESTREAM_ENABLED - RegisterRestreamAuth(); -#endif - setAcceptDrops(true); setContextMenuPolicy(Qt::CustomContextMenu); From 46e2470b6a676231de593cf6858075b76d662fde Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 18:18:01 +0200 Subject: [PATCH 46/65] obs-restream: Add account integration --- plugins/obs-restream/CMakeLists.txt | 2 + .../obs-restream/cmake/feature-oauth.cmake | 48 +++ plugins/obs-restream/data/locale/en-US.ini | 19 + plugins/obs-restream/restream-api.hpp | 11 + .../obs-restream/restream-browser-widget.cpp | 45 +++ .../obs-restream/restream-browser-widget.hpp | 22 ++ plugins/obs-restream/restream-config.cpp | 158 +++++++- plugins/obs-restream/restream-config.hpp | 6 + plugins/obs-restream/restream-oauth.cpp | 342 ++++++++++++++++++ plugins/obs-restream/restream-oauth.hpp | 71 ++++ plugins/obs-restream/restream-service.cpp | 122 +++++++ plugins/obs-restream/restream-service.hpp | 26 ++ 12 files changed, 871 insertions(+), 1 deletion(-) create mode 100644 plugins/obs-restream/cmake/feature-oauth.cmake create mode 100644 plugins/obs-restream/restream-browser-widget.cpp create mode 100644 plugins/obs-restream/restream-browser-widget.hpp create mode 100644 plugins/obs-restream/restream-oauth.cpp create mode 100644 plugins/obs-restream/restream-oauth.hpp diff --git a/plugins/obs-restream/CMakeLists.txt b/plugins/obs-restream/CMakeLists.txt index 4cc0a7e672dd12..6a925f155c047a 100644 --- a/plugins/obs-restream/CMakeLists.txt +++ b/plugins/obs-restream/CMakeLists.txt @@ -23,6 +23,8 @@ target_sources( target_link_libraries(obs-restream PRIVATE OBS::libobs OBS::oauth nlohmann_json::nlohmann_json) +include(cmake/feature-oauth.cmake) + if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-restream.rc) target_sources(obs-restream PRIVATE obs-restream.rc) diff --git a/plugins/obs-restream/cmake/feature-oauth.cmake b/plugins/obs-restream/cmake/feature-oauth.cmake new file mode 100644 index 00000000000000..808cf144bde22e --- /dev/null +++ b/plugins/obs-restream/cmake/feature-oauth.cmake @@ -0,0 +1,48 @@ +if(RESTREAM_CLIENTID AND RESTREAM_HASH MATCHES "(0|[a-fA-F0-9]+)") + if(NOT TARGET OBS::obf) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/obf" "${CMAKE_BINARY_DIR}/deps/obf") + endif() + if(NOT TARGET OBS::oauth-obs-browser-login) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/obs-browser-login" + "${CMAKE_BINARY_DIR}/deps/oauth-service/obs-browser-login") + endif() + if(NOT TARGET OBS::oauth-service-base) + add_subdirectory("${CMAKE_SOURCE_DIR}/deps/oauth-service/service-base" + "${CMAKE_BINARY_DIR}/deps/oauth-service/service-base") + endif() + + if(NOT OAUTH_BASE_URL) + set(OAUTH_BASE_URL + "https://auth.obsproject.com/" + CACHE STRING "Default OAuth base URL") + + mark_as_advanced(OAUTH_BASE_URL) + endif() + + find_qt(COMPONENTS Core Widgets) + + target_sources( + obs-restream PRIVATE # cmake-format: sortable + restream-browser-widget.cpp restream-browser-widget.hpp restream-oauth.cpp restream-oauth.hpp) + + target_link_libraries(obs-restream PRIVATE OBS::obf OBS::oauth-service-base OBS::oauth-obs-browser-login Qt::Core + Qt::Widgets) + + target_compile_definitions( + obs-restream PRIVATE OAUTH_ENABLED OAUTH_BASE_URL="${OAUTH_BASE_URL}" RESTREAM_CLIENTID="${RESTREAM_CLIENTID}" + RESTREAM_HASH=0x${RESTREAM_HASH}) + + set_target_properties( + obs-restream + PROPERTIES AUTOMOC ON + AUTOUIC ON + AUTORCC ON) + + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_enable_feature(obs-restream "Restream OAuth connection") + endif() +else() + if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) + target_disable_feature(obs-restream "Resteam OAuth connection") + endif() +endif() diff --git a/plugins/obs-restream/data/locale/en-US.ini b/plugins/obs-restream/data/locale/en-US.ini index d1fa2ddeddb3e9..90c3cfae9b5918 100644 --- a/plugins/obs-restream/data/locale/en-US.ini +++ b/plugins/obs-restream/data/locale/en-US.ini @@ -2,3 +2,22 @@ Restream.Protocol="Protocol" Restream.Server="Server" Restream.StreamKey="Stream Key" Restream.GetStreamKey="Get Stream Key" +Restream.UseStreamKey="Use Stream Key" + +Restream.Auth.Connect="Connect Account (recommended)" +Restream.Auth.Disconnect="Disconnect Account" +Restream.Auth.ConnectedAccount="Connected account" +Restream.Auth.LoginError.Title="Authentication Failure" +Restream.Auth.LoginError.Text="Failed to authenticate:\n\n%1" +Restream.Auth.LoginError.Text2="Failed to authenticate:\n\n%1: %2" +Restream.Auth.SignOutDialog.Title="Disconnect Account?" +Restream.Auth.SignOutDialog.Text="This change will apply immediately. Are you sure you want to disconnect your account?" +Restream.Auth.ReLoginDialog.Title="Restream Re-Login Required" +Restream.Auth.ReLoginDialog.Text="%1 A re-login is required to keep the integration enabled. Proceed ?" +Restream.Auth.ReLoginDialog.ScopeChange="The authentication requirements for Restream have changed." +Restream.Auth.ReLoginDialog.RefreshTokenFailed="The Restream access token could not be refreshed." +Restream.Auth.ReLoginDialog.ProfileDuplication="The Restream service has been duplicated." + +Restream.Dock.Chat="Chat" +Restream.Dock.StreamInfo="Stream Information" +Restream.Dock.Channels="Restream Channels" diff --git a/plugins/obs-restream/restream-api.hpp b/plugins/obs-restream/restream-api.hpp index 1d3899beee8ffc..8963e84670b86c 100644 --- a/plugins/obs-restream/restream-api.hpp +++ b/plugins/obs-restream/restream-api.hpp @@ -21,4 +21,15 @@ inline void from_json(const json &j, Ingest &s) s.rtmpUrl = j.at("rtmpUrl").get(); } +#ifdef OAUTH_ENABLED + +struct StreamKey { + std::string streamKey; +}; + +inline void from_json(const json &j, StreamKey &s) +{ + s.streamKey = j.at("streamKey").get(); +} +#endif } diff --git a/plugins/obs-restream/restream-browser-widget.cpp b/plugins/obs-restream/restream-browser-widget.cpp new file mode 100644 index 00000000000000..79f549de74dd24 --- /dev/null +++ b/plugins/obs-restream/restream-browser-widget.cpp @@ -0,0 +1,45 @@ +#include "restream-browser-widget.hpp" + +#include + +#include +#include + +RestreamBrowserWidget::RestreamBrowserWidget(const QString &url_) + : url(url_.toStdString()), + QWidget() +{ + setMinimumSize(200, 300); + + QVBoxLayout *layout = new QVBoxLayout(); + layout->setContentsMargins(0, 0, 0, 0); + + setLayout(layout); +} + +void RestreamBrowserWidget::showEvent(QShowEvent *event) +{ + UpdateCefWidget(); + + QWidget::showEvent(event); +} + +void RestreamBrowserWidget::hideEvent(QHideEvent *event) +{ + cefWidget.reset(nullptr); + QWidget::hideEvent(event); +} + +void RestreamBrowserWidget::UpdateCefWidget() +{ + obs_frontend_browser_params params = {0}; + + params.url = url.c_str(); + params.enable_cookie = true; + + cefWidget.reset((QWidget *)obs_frontend_get_browser_widget(¶ms)); + + QVBoxLayout *layout = (QVBoxLayout *)this->layout(); + layout->addWidget(cefWidget.get()); + cefWidget->setParent(this); +} diff --git a/plugins/obs-restream/restream-browser-widget.hpp b/plugins/obs-restream/restream-browser-widget.hpp new file mode 100644 index 00000000000000..7ed297b26b10b4 --- /dev/null +++ b/plugins/obs-restream/restream-browser-widget.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +class RestreamBrowserWidget : public QWidget { + Q_OBJECT + + std::string url; + + QScopedPointer cefWidget; + +public: + RestreamBrowserWidget(const QString &url); + ~RestreamBrowserWidget() {} + + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; +private slots: + void UpdateCefWidget(); +}; diff --git a/plugins/obs-restream/restream-config.cpp b/plugins/obs-restream/restream-config.cpp index d39f0a8adc60f0..6e514d8350f706 100644 --- a/plugins/obs-restream/restream-config.cpp +++ b/plugins/obs-restream/restream-config.cpp @@ -4,6 +4,10 @@ #include "restream-config.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + constexpr const char *STREAM_KEY_LINK = "https://restream.io/settings/streaming-setup?from=OBS"; constexpr const char *AUTODETECT_INGEST = "rtmp://live.restream.io/live"; @@ -13,7 +17,21 @@ constexpr const char *BANDWIDTH_TEST = "?test=true"; RestreamConfig::RestreamConfig(obs_data_t *settings, obs_service_t *self) : typeData(reinterpret_cast( obs_service_get_type_data(self))) +#ifdef OAUTH_ENABLED + , + serviceObj(self) + +#endif { +#ifdef OAUTH_ENABLED + if (!obs_data_has_user_value(settings, "uuid")) { + BPtr newUuid = os_generate_uuid(); + obs_data_set_string(settings, "uuid", newUuid); + } + + uuid = obs_data_get_string(settings, "uuid"); +#endif + Update(settings); } @@ -21,11 +39,35 @@ void RestreamConfig::Update(obs_data_t *settings) { server = obs_data_get_string(settings, "server"); +#ifdef OAUTH_ENABLED + std::string newUuid = obs_data_get_string(settings, "uuid"); + + if (newUuid != uuid) { + if (oauth) { + typeData->ReleaseOAuth(uuid, serviceObj); + oauth = nullptr; + } + + uuid = newUuid; + } + if (!oauth) + oauth = typeData->GetOAuth(uuid, serviceObj); + + if (!oauth->Connected()) { + streamKey = obs_data_get_string(settings, "stream_key"); + } +#else streamKey = obs_data_get_string(settings, "stream_key"); bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; +#endif } -RestreamConfig::~RestreamConfig() {} +RestreamConfig::~RestreamConfig() +{ +#ifdef OAUTH_ENABLED + typeData->ReleaseOAuth(uuid, serviceObj); +#endif +} void RestreamConfig::InfoGetDefault(obs_data_t *settings) { @@ -41,6 +83,13 @@ const char *RestreamConfig::ConnectInfo(uint32_t type) break; case OBS_SERVICE_CONNECT_INFO_STREAM_KEY: +#ifdef OAUTH_ENABLED + if (oauth->Connected() && streamKey.empty()) { + if (oauth->GetStreamKey(streamKey)) + bandwidthTestStreamKey = + streamKey + BANDWIDTH_TEST; + } +#endif if (bandwidthTest ? !bandwidthTestStreamKey.empty() : !streamKey.empty()) { return bandwidthTest ? bandwidthTestStreamKey.c_str() @@ -56,17 +105,99 @@ const char *RestreamConfig::ConnectInfo(uint32_t type) bool RestreamConfig::CanTryToConnect() { +#ifdef OAUTH_ENABLED + if (oauth->Connected()) { + bool ret = oauth->GetStreamKey(streamKey); + if (!streamKey.empty()) + bandwidthTestStreamKey = streamKey + BANDWIDTH_TEST; + + return ret; + } else { + return !streamKey.empty(); + } +#else if (server.empty() || streamKey.empty()) return false; +#endif + + return true; +} + +#ifdef OAUTH_ENABLED +static bool ConnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + RestreamApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->Login()) + return false; + + obs_property_set_visible(obs_properties_get(props, "connect"), false); + obs_property_set_visible(obs_properties_get(props, "disconnect"), true); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), true); + + return true; +} + +static bool UseStreamKeyCb(obs_properties_t *props, obs_property_t *, void *) +{ + obs_property_set_visible(obs_properties_get(props, "connect"), + obs_frontend_is_browser_available()); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "stream_key"), true); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + true); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), + false); + + return true; +} + +static bool DisconnectCb(obs_properties_t *props, obs_property_t *, void *priv) +{ + RestreamApi::ServiceOAuth *oauth = + reinterpret_cast(priv); + + if (!oauth->SignOut()) + return false; + + if (!obs_frontend_is_browser_available()) + return UseStreamKeyCb(props, nullptr, nullptr); + + obs_property_set_visible(obs_properties_get(props, "connect"), true); + obs_property_set_visible(obs_properties_get(props, "disconnect"), + false); + obs_property_set_visible(obs_properties_get(props, "use_stream_key"), + true); + obs_property_set_visible(obs_properties_get(props, "stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "get_stream_key"), + false); + obs_property_set_visible(obs_properties_get(props, "chat_addon"), + false); return true; } +#endif obs_properties_t *RestreamConfig::GetProperties() { obs_properties_t *ppts = obs_properties_create(); obs_property_t *p; +#ifdef OAUTH_ENABLED + bool connected = oauth->Connected(); +#endif + p = obs_properties_add_list(ppts, "server", obs_module_text("Restream.Server"), OBS_COMBO_TYPE_LIST, @@ -88,6 +219,9 @@ obs_properties_t *RestreamConfig::GetProperties() p = obs_properties_add_text(ppts, "stream_key", obs_module_text("Restream.StreamKey"), OBS_TEXT_PASSWORD); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); +#endif p = obs_properties_add_button(ppts, "get_stream_key", obs_module_text("Restream.GetStreamKey"), @@ -95,5 +229,27 @@ obs_properties_t *RestreamConfig::GetProperties() obs_property_button_set_type(p, OBS_BUTTON_URL); obs_property_button_set_url(p, (char *)STREAM_KEY_LINK); +#ifdef OAUTH_ENABLED + obs_property_set_visible(p, !streamKey.empty() && !connected); + + p = obs_properties_add_button2(ppts, "connect", + obs_module_text("Restream.Auth.Connect"), + ConnectCb, oauth); + obs_property_set_visible(p, !connected); + + p = obs_properties_add_button2( + ppts, "disconnect", obs_module_text("Restream.Auth.Disconnect"), + DisconnectCb, oauth); + obs_property_set_visible(p, connected); + + p = obs_properties_add_button(ppts, "use_stream_key", + obs_module_text("Restream.UseStreamKey"), + UseStreamKeyCb); + obs_property_set_visible(p, streamKey.empty() && !connected); + + if (!obs_frontend_is_browser_available() && !connected) + UseStreamKeyCb(ppts, nullptr, nullptr); +#endif + return ppts; } diff --git a/plugins/obs-restream/restream-config.hpp b/plugins/obs-restream/restream-config.hpp index 61589a68457860..f41f0803c7be86 100644 --- a/plugins/obs-restream/restream-config.hpp +++ b/plugins/obs-restream/restream-config.hpp @@ -11,6 +11,12 @@ class RestreamConfig { RestreamService *typeData; +#ifdef OAUTH_ENABLED + obs_service_t *serviceObj; + + std::string uuid; + RestreamApi::ServiceOAuth *oauth = nullptr; +#endif std::string server; diff --git a/plugins/obs-restream/restream-oauth.cpp b/plugins/obs-restream/restream-oauth.cpp new file mode 100644 index 00000000000000..020d61dd123848 --- /dev/null +++ b/plugins/obs-restream/restream-oauth.cpp @@ -0,0 +1,342 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "restream-oauth.hpp" + +#include +#include + +#include +#include +#include + +constexpr const char *AUTH_BASE_URL = (const char *)OAUTH_BASE_URL; +constexpr const char *AUTH_ENDPOINT = "v1/restream/redirect"; +constexpr const char *TOKEN_URL = (const char *)OAUTH_BASE_URL + "v1/restream/token"; +constexpr const char *API_URL = "https://api.restream.io/v2/user/"; + +constexpr const char *CLIENT_ID = (const char *)RESTREAM_CLIENTID; +constexpr uint64_t CLIENT_ID_HASH = RESTREAM_HASH; +constexpr int64_t SCOPE_VERSION = 1; + +constexpr const char *CHAT_URL = "https://restream.io/chat-application"; +constexpr const char *INFO_URL = "https://restream.io/titles/embed"; +constexpr const char *CHANNELS_URL = "https://restream.io/channel/embed"; + +constexpr const char *CHAT_DOCK_NAME = "restreamChat"; +constexpr const char *INFO_DOCK_NAME = "restreamInfo"; +constexpr const char *CHANNELS_DOCK_NAME = "restreamChannel"; + +namespace RestreamApi { + +std::string ServiceOAuth::UserAgent() +{ + std::string userAgent("obs-restream "); + userAgent += obs_get_version_string(); + + return userAgent; +} + +const char *ServiceOAuth::TokenUrl() +{ + return TOKEN_URL; +} + +std::string ServiceOAuth::ClientId() +{ + std::string clientId = CLIENT_ID; + deobfuscate_str(&clientId[0], CLIENT_ID_HASH); + + return clientId; +} + +int64_t ServiceOAuth::ScopeVersion() +{ + return SCOPE_VERSION; +} + +static inline void WarningBox(const QString &title, const QString &text, + QWidget *parent) +{ + QMessageBox warn(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *ok = warn.addButton(QMessageBox::Ok); + ok->setText(ok->tr("Ok")); + warn.exec(); +} + +static int LoginConfirmation(const OAuth::LoginReason &reason, QWidget *parent) +{ + const char *reasonText = nullptr; + switch (reason) { + case OAuth::LoginReason::CONNECT: + return QMessageBox::Ok; + case OAuth::LoginReason::SCOPE_CHANGE: + reasonText = obs_module_text( + "Restream.Auth.ReLoginDialog.ScopeChange"); + break; + case OAuth::LoginReason::REFRESH_TOKEN_FAILED: + reasonText = obs_module_text( + "Restream.Auth.ReLoginDialog.RefreshTokenFailed"); + break; + case OAuth::LoginReason::PROFILE_DUPLICATION: + reasonText = obs_module_text( + "Restream.Auth.ReLoginDialog.ProfileDuplication"); + break; + } + + QString title = QString::fromUtf8( + obs_module_text("Restream.Auth.ReLoginDialog.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("Restream.Auth.ReLoginDialog.Text")) + .arg(reasonText); + + QMessageBox ques(QMessageBox::Warning, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + return ques.exec(); +} + +bool ServiceOAuth::LoginInternal(const OAuth::LoginReason &reason, + std::string &code, std::string &) +{ + if (!obs_frontend_is_browser_available()) { + blog(LOG_ERROR, + "[%s][%s]: Browser is not available, unable to login", + PluginLogName(), __FUNCTION__); + return false; + } + + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + if (reason != OAuth::LoginReason::CONNECT && + LoginConfirmation(reason, parent) != QMessageBox::Ok) { + return false; + } + + OAuth::OBSBrowserLogin login(AUTH_BASE_URL, AUTH_ENDPOINT, + {"about:blank"}, parent); + + if (login.exec() != QDialog::Accepted) { + QString error = login.GetLastError(); + + if (!error.isEmpty()) { + blog(LOG_ERROR, "[%s][%s]: %s", PluginLogName(), + __FUNCTION__, error.toUtf8().constData()); + + QString title = QString::fromUtf8( + obs_module_text( + "Restream.Auth.LoginError.Title"), + -1); + QString text = + QString::fromUtf8( + obs_module_text( + "Restream.Auth.LoginError.Text"), + -1) + .arg(error); + + WarningBox(title, text, parent); + } + + return false; + } + + code = login.GetCode().toStdString(); + + return true; +} + +bool ServiceOAuth::SignOutInternal() +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + + QString title = QString::fromUtf8( + obs_module_text("Restream.Auth.SignOutDialog.Title"), -1); + QString text = QString::fromUtf8( + obs_module_text("Restream.Auth.SignOutDialog.Text"), -1); + + QMessageBox ques(QMessageBox::Question, title, text, + QMessageBox::NoButton, parent); + QPushButton *button = ques.addButton(QMessageBox::Yes); + button->setText(button->tr("Yes")); + button = ques.addButton(QMessageBox::No); + button->setText(button->tr("No")); + + if (ques.exec() != QMessageBox::Yes) + return false; + + obs_frontend_delete_browser_cookie("restream.io"); + + return true; +} + +void ServiceOAuth::LoadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + if (chat || info || channels) { + blog(LOG_ERROR, "[%s][%s] The docks were not unloaded", + PluginLogName(), __FUNCTION__); + return; + } + + chat = new RestreamBrowserWidget(QString::fromUtf8(CHAT_URL, -1)); + chat->resize(420, 600); + obs_frontend_add_dock_by_id( + CHAT_DOCK_NAME, obs_module_text("Restream.Dock.Chat"), chat); + + info = new RestreamBrowserWidget(QString::fromUtf8(INFO_URL, -1)); + info->resize(410, 650); + obs_frontend_add_dock_by_id(INFO_DOCK_NAME, + obs_module_text("Restream.Dock.StreamInfo"), + info); + + channels = + new RestreamBrowserWidget(QString::fromUtf8(CHANNELS_URL, -1)); + channels->resize(410, 250); + obs_frontend_add_dock_by_id(CHANNELS_DOCK_NAME, + obs_module_text("Restream.Dock.Channels"), + channels); +} + +void ServiceOAuth::UnloadFrontendInternal() +{ + if (!obs_frontend_is_browser_available()) + return; + + obs_frontend_remove_dock(CHAT_DOCK_NAME); + chat = nullptr; + obs_frontend_remove_dock(INFO_DOCK_NAME); + info = nullptr; + obs_frontend_remove_dock(CHANNELS_DOCK_NAME); + channels = nullptr; +} + +void ServiceOAuth::DuplicationResetInternal() +{ + obs_frontend_delete_browser_cookie("restream.io"); +} + +void ServiceOAuth::LoginError(RequestError &error) +{ + QWidget *parent = + reinterpret_cast(obs_frontend_get_main_window()); + QString title = QString::fromUtf8( + obs_module_text("Restream.Auth.LoginError.Title"), -1); + QString text = + QString::fromUtf8( + obs_module_text("Restream.Auth.LoginError.Text2"), -1) + .arg(QString::fromStdString(error.message)) + .arg(QString::fromStdString(error.error)); + + WarningBox(title, text, parent); +} + +bool ServiceOAuth::WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + std::string error; + CURLcode curlCode = + GetRemoteFile(UserAgent().c_str(), url, str, error, + responseCode, contentType, request_type, postData, + {"Client-ID: " + ClientId(), + "Authorization: Bearer " + AccessToken()}, + nullptr, 5, postDataSize); + + if (curlCode != CURLE_OK && curlCode != CURLE_HTTP_RETURNED_ERROR) { + lastError = RequestError(RequestErrorType::CURL_REQUEST_FAILED, + error); + return false; + } + + if (*responseCode == 200) + return true; + + return false; +} + +bool ServiceOAuth::TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, const char *contentType, + std::string request_type, + const char *postData, int postDataSize) +{ + long responseCode = 0; + if (WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize)) { + return true; + } + + if (lastError.type == RequestErrorType::CURL_REQUEST_FAILED) { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.message.c_str(), lastError.error.c_str()); + } else if (lastError.type == + RequestErrorType::ERROR_JSON_PARSING_FAILED) { + blog(LOG_ERROR, "[%s][%s] HTTP status: %ld\n%s", + PluginLogName(), funcName, responseCode, str.c_str()); + } else { + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), funcName, + lastError.error.c_str(), lastError.message.c_str()); + } + + if (responseCode != 401) + return false; + + lastError = RequestError(); + if (!RefreshAccessToken(lastError)) + return false; + + return WrappedGetRemoteFile(url, str, &responseCode, contentType, + request_type, postData, postDataSize); +} + +bool ServiceOAuth::GetStreamKey(std::string &streamKey_) +{ + if (!streamKey.empty()) { + streamKey_ = streamKey; + return true; + } + + std::string url = API_URL; + url += "streamKey"; + + std::string output; + lastError = RequestError(); + if (!TryGetRemoteFile(__FUNCTION__, url.c_str(), output, + "application/json", "")) { + blog(LOG_ERROR, "[%s][%s] Failed to retrieve stream key", + PluginLogName(), __FUNCTION__); + return false; + } + + StreamKey response; + try { + response = nlohmann::json::parse(output); + + streamKey = response.streamKey; + streamKey_ = streamKey; + return true; + } catch (nlohmann::json::exception &e) { + lastError = RequestError(RequestErrorType::JSON_PARSING_FAILED, + e.what()); + blog(LOG_ERROR, "[%s][%s] %s: %s", PluginLogName(), + __FUNCTION__, lastError.message.c_str(), + lastError.error.c_str()); + } + + return false; +} + +} diff --git a/plugins/obs-restream/restream-oauth.hpp b/plugins/obs-restream/restream-oauth.hpp new file mode 100644 index 00000000000000..5cd9b51f01993b --- /dev/null +++ b/plugins/obs-restream/restream-oauth.hpp @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2023 tytan652 +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "restream-api.hpp" +#include "restream-browser-widget.hpp" + +namespace RestreamApi { + +class ServiceOAuth : public OAuth::ServiceBase { + RequestError lastError; + + std::string streamKey; + + RestreamBrowserWidget *chat = nullptr; + RestreamBrowserWidget *info = nullptr; + RestreamBrowserWidget *channels = nullptr; + + std::string UserAgent() override; + + const char *TokenUrl() override; + + std::string ClientId() override; + + int64_t ScopeVersion() override; + + bool LoginInternal(const OAuth::LoginReason &reason, std::string &code, + std::string &redirectUri) override; + bool SignOutInternal() override; + + void LoadFrontendInternal() override; + void UnloadFrontendInternal() override; + + void DuplicationResetInternal() override; + + const char *PluginLogName() override { return "obs-restream"; }; + + void LoginError(RequestError &error) override; + + bool WrappedGetRemoteFile(const char *url, std::string &str, + long *responseCode, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + + bool TryGetRemoteFile(const char *funcName, const char *url, + std::string &str, + const char *contentType = nullptr, + std::string request_type = "", + const char *postData = nullptr, + int postDataSize = 0); + +public: + ServiceOAuth() : OAuth::ServiceBase() {} + + RequestError GetLastError() { return lastError; } + + bool GetStreamKey(std::string &streamKey); +}; + +} diff --git a/plugins/obs-restream/restream-service.cpp b/plugins/obs-restream/restream-service.cpp index 4a4cfb5254b5af..7c94fe7b32b344 100644 --- a/plugins/obs-restream/restream-service.cpp +++ b/plugins/obs-restream/restream-service.cpp @@ -4,8 +4,16 @@ #include "restream-service.hpp" +#ifdef OAUTH_ENABLED +#include +#endif + #include "restream-config.hpp" +#ifdef OAUTH_ENABLED +constexpr const char *DATA_FILENAME = "obs-restream.json"; +#endif + RestreamService::RestreamService() { info.type_data = this; @@ -37,6 +45,10 @@ RestreamService::RestreamService() info.bandwidth_test_enabled = InfoBandwidthTestEnabled; obs_register_service(&info); + +#ifdef OAUTH_ENABLED + obs_frontend_add_event_callback(OBSEvent, this); +#endif } void RestreamService::Register() @@ -98,3 +110,113 @@ std::vector RestreamService::GetIngests(bool refresh) return {}; } + +#ifdef OAUTH_ENABLED +void RestreamService::OBSEvent(obs_frontend_event event, void *priv) +{ + RestreamService *self = reinterpret_cast(priv); + + switch (event) { + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + case OBS_FRONTEND_EVENT_FINISHED_LOADING: { + self->deferUiFunction = false; + break; + } + case OBS_FRONTEND_EVENT_PROFILE_CHANGING: + self->SaveOAuthsData(); + self->deferUiFunction = true; + self->data = nullptr; + break; + case OBS_FRONTEND_EVENT_EXIT: + self->SaveOAuthsData(); + obs_frontend_remove_event_callback(OBSEvent, priv); + return; + default: + break; + } + + if (self->oauths.empty()) + return; + + for (auto const &p : self->oauths) + p.second->OBSEvent(event); +} + +void RestreamService::SaveOAuthsData() +{ + OBSDataAutoRelease saveData = obs_data_create(); + bool writeData = false; + for (auto const &[uuid, oauth] : oauths) { + OBSDataAutoRelease data = oauth->GetData(); + if (data == nullptr) + continue; + + obs_data_set_obj(saveData, uuid.c_str(), data); + writeData = true; + } + + if (!writeData) + return; + + BPtr profilePath = obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + if (!obs_data_save_json_pretty_safe(saveData, dataPath.c_str(), "tmp", + "bak")) + blog(LOG_ERROR, + "[obs-restream][%s] Failed to save integrations data", + __FUNCTION__); +}; + +RestreamApi::ServiceOAuth *RestreamService::GetOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (data == nullptr) { + BPtr profilePath = + obs_frontend_get_current_profile_path(); + std::string dataPath = profilePath.Get(); + dataPath += "/"; + dataPath += DATA_FILENAME; + + data = obs_data_create_from_json_file_safe(dataPath.c_str(), + "bak"); + + if (!data) { + blog(LOG_DEBUG, + "[obs-restream][%s] Failed to open integrations data: %s", + __FUNCTION__, dataPath.c_str()); + } + } + + if (oauths.count(uuid) == 0) { + OBSDataAutoRelease oauthData = + obs_data_get_obj(data, uuid.c_str()); + oauths.emplace(uuid, + std::make_shared()); + oauths[uuid]->Setup(oauthData, deferUiFunction); + oauthsRefCounter.emplace(uuid, 0); + oauths[uuid]->AddBondedService(service); + } + + oauthsRefCounter[uuid] += 1; + return oauths[uuid].get(); +} + +void RestreamService::ReleaseOAuth(const std::string &uuid, + obs_service_t *service) +{ + if (oauthsRefCounter.count(uuid) == 0) + return; + + oauths[uuid]->RemoveBondedService(service); + oauthsRefCounter[uuid] -= 1; + + if (oauthsRefCounter[uuid] != 0) + return; + + oauths.erase(uuid); + oauthsRefCounter.erase(uuid); +} +#endif diff --git a/plugins/obs-restream/restream-service.hpp b/plugins/obs-restream/restream-service.hpp index f370c7c5cbb75f..5c1318ceee0b08 100644 --- a/plugins/obs-restream/restream-service.hpp +++ b/plugins/obs-restream/restream-service.hpp @@ -8,6 +8,14 @@ #include "restream-api.hpp" +#ifdef OAUTH_ENABLED +#include +#include +#include + +#include "restream-oauth.hpp" +#endif + class RestreamService { obs_service_info info = {0}; @@ -34,6 +42,18 @@ class RestreamService { static void InfoEnableBandwidthTest(void *data, bool enabled); static bool InfoBandwidthTestEnabled(void *data); +#ifdef OAUTH_ENABLED + OBSDataAutoRelease data = nullptr; + + std::unordered_map> + oauths; + std::unordered_map oauthsRefCounter; + bool deferUiFunction = true; + + void SaveOAuthsData(); + static void OBSEvent(obs_frontend_event event, void *priv); +#endif public: RestreamService(); ~RestreamService() {} @@ -41,4 +61,10 @@ class RestreamService { static void Register(); std::vector GetIngests(bool refresh = false); + +#ifdef OAUTH_ENABLED + RestreamApi::ServiceOAuth *GetOAuth(const std::string &uuid, + obs_service_t *service); + void ReleaseOAuth(const std::string &uuid, obs_service_t *service); +#endif }; From ed687fa2dba5dcd8a71caeda75f642833a798fff Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 1 Jul 2023 18:46:40 +0200 Subject: [PATCH 47/65] UI: Remove auth-related code --- UI/CMakeLists.txt | 6 - UI/auth-base.cpp | 83 ------- UI/auth-base.hpp | 64 ------ UI/auth-listener.cpp | 114 ---------- UI/auth-listener.hpp | 23 -- UI/auth-oauth.cpp | 344 ------------------------------ UI/auth-oauth.hpp | 93 -------- UI/cmake/legacy.cmake | 8 +- UI/data/locale/en-US.ini | 15 -- UI/window-basic-main-outputs.cpp | 8 - UI/window-basic-main-profiles.cpp | 12 -- UI/window-basic-main.cpp | 15 +- UI/window-basic-main.hpp | 5 - 13 files changed, 3 insertions(+), 787 deletions(-) delete mode 100644 UI/auth-base.cpp delete mode 100644 UI/auth-base.hpp delete mode 100644 UI/auth-listener.cpp delete mode 100644 UI/auth-listener.hpp delete mode 100644 UI/auth-oauth.cpp delete mode 100644 UI/auth-oauth.hpp diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 243bd09341472a..4b971940743a8f 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -60,12 +60,6 @@ target_sources( obs-studio PRIVATE # cmake-format: sortable api-interface.cpp - auth-base.cpp - auth-base.hpp - auth-listener.cpp - auth-listener.hpp - auth-oauth.cpp - auth-oauth.hpp broadcast-flow.cpp broadcast-flow.hpp display-helpers.hpp diff --git a/UI/auth-base.cpp b/UI/auth-base.cpp deleted file mode 100644 index 8ffc68c5344994..00000000000000 --- a/UI/auth-base.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include "auth-base.hpp" -#include "window-basic-main.hpp" - -#include -#include - -struct AuthInfo { - Auth::Def def; - Auth::create_cb create; -}; - -static std::vector authDefs; - -void Auth::RegisterAuth(const Def &d, create_cb create) -{ - AuthInfo info = {d, create}; - authDefs.push_back(info); -} - -std::shared_ptr Auth::Create(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.create(); - } - } - - return nullptr; -} - -Auth::Type Auth::AuthType(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.def.type; - } - } - - return Type::None; -} - -bool Auth::External(const std::string &service) -{ - for (auto &a : authDefs) { - if (service.find(a.def.service) != std::string::npos) { - return a.def.externalOAuth; - } - } - - return false; -} - -void Auth::Load() -{ - OBSBasic *main = OBSBasic::Get(); - const char *typeStr = config_get_string(main->Config(), "Auth", "Type"); - if (!typeStr) - typeStr = ""; - - main->auth = Create(typeStr); - if (main->auth) { - if (main->auth->LoadInternal()) { - main->auth->LoadUI(); - } - } -} - -void Auth::Save() -{ - OBSBasic *main = OBSBasic::Get(); - Auth *auth = main->auth.get(); - if (!auth) { - if (config_has_user_value(main->Config(), "Auth", "Type")) { - config_remove_value(main->Config(), "Auth", "Type"); - config_save_safe(main->Config(), "tmp", nullptr); - } - return; - } - - config_set_string(main->Config(), "Auth", "Type", auth->service()); - auth->SaveInternal(); - config_save_safe(main->Config(), "tmp", nullptr); -} diff --git a/UI/auth-base.hpp b/UI/auth-base.hpp deleted file mode 100644 index 97a2148406af15..00000000000000 --- a/UI/auth-base.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include -#include -#include - -class Auth : public QObject { - Q_OBJECT - -protected: - virtual void SaveInternal() = 0; - virtual bool LoadInternal() = 0; - - bool firstLoad = true; - - struct ErrorInfo { - std::string message; - std::string error; - - ErrorInfo(std::string message_, std::string error_) - : message(message_), - error(error_) - { - } - }; - -public: - enum class Type { - None, - OAuth_StreamKey, - OAuth_LinkedAccount, - }; - - struct Def { - std::string service; - Type type; - bool externalOAuth; - }; - - typedef std::function()> create_cb; - - inline Auth(const Def &d) : def(d) {} - virtual ~Auth() {} - - inline Type type() const { return def.type; } - inline const char *service() const { return def.service.c_str(); } - inline bool external() const { return def.externalOAuth; } - - virtual void LoadUI() {} - - virtual void OnStreamConfig() {} - - static std::shared_ptr Create(const std::string &service); - static Type AuthType(const std::string &service); - static bool External(const std::string &service); - static void Load(); - static void Save(); - -protected: - static void RegisterAuth(const Def &d, create_cb create); - -private: - Def def; -}; diff --git a/UI/auth-listener.cpp b/UI/auth-listener.cpp deleted file mode 100644 index 12a7d72fef4118..00000000000000 --- a/UI/auth-listener.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "obs-app.hpp" -#include "qt-wrappers.hpp" - -#define LOGO_URL "https://obsproject.com/assets/images/new_icon_small-r.png" - -static const QString serverResponseHeader = - QStringLiteral("HTTP/1.0 200 OK\n" - "Connection: close\n" - "Content-Type: text/html; charset=UTF-8\n" - "Server: OBS Studio\n" - "\n" - "OBS Studio" - ""); - -static const QString responseTemplate = - "
" - "\"OBS\"" - "
" - "

%1

"; - -AuthListener::AuthListener(QObject *parent) : QObject(parent) -{ - server = new QTcpServer(this); - connect(server, &QTcpServer::newConnection, this, - &AuthListener::NewConnection); - if (!server->listen(QHostAddress::LocalHost, 0)) { - blog(LOG_DEBUG, "Server could not start"); - emit fail(); - } else { - blog(LOG_DEBUG, "Server started at port %d", - server->serverPort()); - } -} - -quint16 AuthListener::GetPort() -{ - return server ? server->serverPort() : 0; -} - -void AuthListener::SetState(QString state) -{ - this->state = state; -} - -void AuthListener::NewConnection() -{ - QTcpSocket *socket = server->nextPendingConnection(); - if (socket) { - connect(socket, &QTcpSocket::disconnected, socket, - &QTcpSocket::deleteLater); - connect(socket, &QTcpSocket::readyRead, socket, [&, socket]() { - QByteArray buffer; - while (socket->bytesAvailable() > 0) { - buffer.append(socket->readAll()); - } - socket->write(QT_TO_UTF8(serverResponseHeader)); - QString redirect = QString::fromLatin1(buffer); - blog(LOG_DEBUG, "redirect: %s", QT_TO_UTF8(redirect)); - - QRegularExpression re_state( - "(&|\\?)state=(?[^&]+)"); - QRegularExpression re_code( - "(&|\\?)code=(?[^&]+)"); - - QRegularExpressionMatch match = - re_state.match(redirect); - - QString code; - - if (match.hasMatch()) { - if (state == match.captured("state")) { - match = re_code.match(redirect); - if (!match.hasMatch()) - blog(LOG_DEBUG, "no 'code' " - "in server " - "redirect"); - - code = match.captured("code"); - } else { - blog(LOG_WARNING, "state mismatch " - "while handling " - "redirect"); - } - } else { - blog(LOG_DEBUG, "no 'state' in " - "server redirect"); - } - - if (code.isEmpty()) { - auto data = responseTemplate.arg( - QTStr("YouTube.Auth.NoCode")); - socket->write(QT_TO_UTF8(data)); - emit fail(); - } else { - auto data = responseTemplate.arg( - QTStr("YouTube.Auth.Ok")); - socket->write(QT_TO_UTF8(data)); - emit ok(code); - } - socket->flush(); - socket->close(); - }); - } else { - emit fail(); - } -} diff --git a/UI/auth-listener.hpp b/UI/auth-listener.hpp deleted file mode 100644 index fa1db2deafd97a..00000000000000 --- a/UI/auth-listener.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include - -class AuthListener : public QObject { - Q_OBJECT - - QTcpServer *server; - QString state; - -signals: - void ok(const QString &code); - void fail(); - -protected: - void NewConnection(); - -public: - explicit AuthListener(QObject *parent = 0); - quint16 GetPort(); - void SetState(QString state); -}; diff --git a/UI/auth-oauth.cpp b/UI/auth-oauth.cpp deleted file mode 100644 index bb2fd714ca8501..00000000000000 --- a/UI/auth-oauth.cpp +++ /dev/null @@ -1,344 +0,0 @@ -#include "auth-oauth.hpp" - -#include -#include -#include - -#include -#include - -#include "window-basic-main.hpp" -#include "remote-text.hpp" - -#include - -#include - -#include "ui-config.h" - -using namespace json11; - -#ifdef BROWSER_AVAILABLE -#include -extern QCef *cef; -extern QCefCookieManager *panel_cookies; -#endif - -/* ------------------------------------------------------------------------- */ - -OAuthLogin::OAuthLogin(QWidget *parent, const std::string &url, bool token) - : QDialog(parent), - get_token(token) -{ -#ifdef BROWSER_AVAILABLE - if (!cef) { - return; - } - - setWindowTitle("Auth"); - setMinimumSize(400, 400); - resize(700, 700); - - Qt::WindowFlags flags = windowFlags(); - Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; - setWindowFlags(flags & (~helpFlag)); - - OBSBasic::InitBrowserPanelSafeBlock(); - - cefWidget = cef->create_widget(nullptr, url, panel_cookies); - if (!cefWidget) { - fail = true; - return; - } - - connect(cefWidget, SIGNAL(titleChanged(const QString &)), this, - SLOT(setWindowTitle(const QString &))); - connect(cefWidget, SIGNAL(urlChanged(const QString &)), this, - SLOT(urlChanged(const QString &))); - - QPushButton *close = new QPushButton(QTStr("Cancel")); - connect(close, &QAbstractButton::clicked, this, &QDialog::reject); - - QHBoxLayout *bottomLayout = new QHBoxLayout(); - bottomLayout->addStretch(); - bottomLayout->addWidget(close); - bottomLayout->addStretch(); - - QVBoxLayout *topLayout = new QVBoxLayout(this); - topLayout->addWidget(cefWidget); - topLayout->addLayout(bottomLayout); -#else - UNUSED_PARAMETER(url); -#endif -} - -OAuthLogin::~OAuthLogin() {} - -int OAuthLogin::exec() -{ -#ifdef BROWSER_AVAILABLE - if (cefWidget) { - return QDialog::exec(); - } -#endif - return QDialog::Rejected; -} - -void OAuthLogin::reject() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::reject(); -} - -void OAuthLogin::accept() -{ -#ifdef BROWSER_AVAILABLE - delete cefWidget; -#endif - QDialog::accept(); -} - -void OAuthLogin::urlChanged(const QString &url) -{ - std::string uri = get_token ? "access_token=" : "code="; - int code_idx = url.indexOf(uri.c_str()); - if (code_idx == -1) - return; - - if (!url.startsWith(OAUTH_BASE_URL)) - return; - - code_idx += (int)uri.size(); - - int next_idx = url.indexOf("&", code_idx); - if (next_idx != -1) - code = url.mid(code_idx, next_idx - code_idx); - else - code = url.right(url.size() - code_idx); - - accept(); -} - -/* ------------------------------------------------------------------------- */ - -struct OAuthInfo { - Auth::Def def; - OAuth::login_cb login; - OAuth::delete_cookies_cb delete_cookies; -}; - -static std::vector loginCBs; - -void OAuth::RegisterOAuth(const Def &d, create_cb create, login_cb login, - delete_cookies_cb delete_cookies) -{ - OAuthInfo info = {d, login, delete_cookies}; - loginCBs.push_back(info); - RegisterAuth(d, create); -} - -std::shared_ptr OAuth::Login(QWidget *parent, const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - return a.login(parent, service); - } - } - - return nullptr; -} - -void OAuth::DeleteCookies(const std::string &service) -{ - for (auto &a : loginCBs) { - if (service.find(a.def.service) != std::string::npos) { - a.delete_cookies(); - } - } -} - -void OAuth::SaveInternal() -{ - OBSBasic *main = OBSBasic::Get(); - config_set_string(main->Config(), service(), "RefreshToken", - refresh_token.c_str()); - config_set_string(main->Config(), service(), "Token", token.c_str()); - config_set_uint(main->Config(), service(), "ExpireTime", expire_time); - config_set_int(main->Config(), service(), "ScopeVer", currentScopeVer); -} - -static inline std::string get_config_str(OBSBasic *main, const char *section, - const char *name) -{ - const char *val = config_get_string(main->Config(), section, name); - return val ? val : ""; -} - -bool OAuth::LoadInternal() -{ - OBSBasic *main = OBSBasic::Get(); - refresh_token = get_config_str(main, service(), "RefreshToken"); - token = get_config_str(main, service(), "Token"); - expire_time = config_get_uint(main->Config(), service(), "ExpireTime"); - currentScopeVer = - (int)config_get_int(main->Config(), service(), "ScopeVer"); - return implicit ? !token.empty() : !refresh_token.empty(); -} - -bool OAuth::TokenExpired() -{ - if (token.empty()) - return true; - if ((uint64_t)time(nullptr) > expire_time - 5) - return true; - return false; -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, - const std::string &secret, const std::string &redirect_uri, - int scope_ver, const std::string &auth_code, bool retry) -{ - return GetTokenInternal(url, client_id, secret, redirect_uri, scope_ver, - auth_code, retry); -} - -bool OAuth::GetToken(const char *url, const std::string &client_id, - int scope_ver, const std::string &auth_code, bool retry) -{ - return GetTokenInternal(url, client_id, {}, {}, scope_ver, auth_code, - retry); -} - -bool OAuth::GetTokenInternal(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry) -try { - std::string output; - std::string error; - std::string desc; - - if (currentScopeVer > 0 && currentScopeVer < scope_ver) { - if (RetryLogin()) { - return true; - } else { - QString title = QTStr("Auth.InvalidScope.Title"); - QString text = - QTStr("Auth.InvalidScope.Text").arg(service()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - } - - if (auth_code.empty() && !TokenExpired()) { - return true; - } - - std::string post_data; - post_data += "action=redirect&client_id="; - post_data += client_id; - if (!secret.empty()) { - post_data += "&client_secret="; - post_data += secret; - } - if (!redirect_uri.empty()) { - post_data += "&redirect_uri="; - post_data += redirect_uri; - } - - if (!auth_code.empty()) { - post_data += "&grant_type=authorization_code&code="; - post_data += auth_code; - } else { - post_data += "&grant_type=refresh_token&refresh_token="; - post_data += refresh_token; - } - - bool success = false; - - auto func = [&]() { - success = GetRemoteFile(url, output, error, nullptr, - "application/x-www-form-urlencoded", "", - post_data.c_str(), - std::vector(), nullptr, 5); - }; - - ExecThreadedWithoutBlocking(func, QTStr("Auth.Authing.Title"), - QTStr("Auth.Authing.Text").arg(service())); - if (!success || output.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - Json json = Json::parse(output, error); - if (!error.empty()) - throw ErrorInfo("Failed to parse json", error); - - /* -------------------------- */ - /* error handling */ - - error = json["error"].string_value(); - if (!retry && error == "invalid_grant") { - if (RetryLogin()) { - return true; - } - } - if (!error.empty()) - throw ErrorInfo(error, - json["error_description"].string_value()); - - /* -------------------------- */ - /* success! */ - - expire_time = (uint64_t)time(nullptr) + json["expires_in"].int_value(); - token = json["access_token"].string_value(); - if (token.empty()) - throw ErrorInfo("Failed to get token from remote", error); - - if (!auth_code.empty()) { - refresh_token = json["refresh_token"].string_value(); - if (refresh_token.empty()) - throw ErrorInfo("Failed to get refresh token from " - "remote", - error); - - currentScopeVer = scope_ver; - } - - return true; - -} catch (ErrorInfo &info) { - if (!retry) { - QString title = QTStr("Auth.AuthFailure.Title"); - QString text = QTStr("Auth.AuthFailure.Text") - .arg(service(), info.message.c_str(), - info.error.c_str()); - - QMessageBox::warning(OBSBasic::Get(), title, text); - } - - blog(LOG_WARNING, "%s: %s: %s", __FUNCTION__, info.message.c_str(), - info.error.c_str()); - return false; -} - -void OAuthStreamKey::OnStreamConfig() -{ - if (key_.empty()) - return; - - OBSBasic *main = OBSBasic::Get(); - obs_service_t *service = main->GetService(); - - OBSDataAutoRelease settings = obs_service_get_settings(service); - - bool bwtest = obs_data_get_bool(settings, "bwtest"); - - if (bwtest && strcmp(this->service(), "Twitch") == 0) - obs_data_set_string(settings, "key", - (key_ + "?bandwidthtest=true").c_str()); - else - obs_data_set_string(settings, "key", key_.c_str()); - - obs_service_update(service, settings); -} diff --git a/UI/auth-oauth.hpp b/UI/auth-oauth.hpp deleted file mode 100644 index 364dccdc4442ab..00000000000000 --- a/UI/auth-oauth.hpp +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "auth-base.hpp" - -class QCefWidget; - -class OAuthLogin : public QDialog { - Q_OBJECT - - QCefWidget *cefWidget = nullptr; - QString code; - bool get_token = false; - bool fail = false; - -public: - OAuthLogin(QWidget *parent, const std::string &url, bool token); - ~OAuthLogin(); - - inline QString GetCode() const { return code; } - inline bool LoadFail() const { return fail; } - - virtual int exec() override; - virtual void reject() override; - virtual void accept() override; - -public slots: - void urlChanged(const QString &url); -}; - -class OAuth : public Auth { - Q_OBJECT - -public: - inline OAuth(const Def &d) : Auth(d) {} - - typedef std::function( - QWidget *, const std::string &service_name)> - login_cb; - typedef std::function delete_cookies_cb; - - static std::shared_ptr Login(QWidget *parent, - const std::string &service); - static void DeleteCookies(const std::string &service); - - static void RegisterOAuth(const Def &d, create_cb create, - login_cb login, - delete_cookies_cb delete_cookies); - -protected: - std::string refresh_token; - std::string token; - bool implicit = false; - uint64_t expire_time = 0; - int currentScopeVer = 0; - - virtual void SaveInternal() override; - virtual bool LoadInternal() override; - - virtual bool RetryLogin() = 0; - bool TokenExpired(); - bool GetToken(const char *url, const std::string &client_id, - int scope_ver, - const std::string &auth_code = std::string(), - bool retry = false); - bool GetToken(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry); - -private: - bool GetTokenInternal(const char *url, const std::string &client_id, - const std::string &secret, - const std::string &redirect_uri, int scope_ver, - const std::string &auth_code, bool retry); -}; - -class OAuthStreamKey : public OAuth { - Q_OBJECT - -protected: - std::string key_; - -public: - inline OAuthStreamKey(const Def &d) : OAuth(d) {} - - inline const std::string &key() const { return key_; } - - virtual void OnStreamConfig() override; -}; diff --git a/UI/cmake/legacy.cmake b/UI/cmake/legacy.cmake index 196313eac059a9..18351dd56b638f 100644 --- a/UI/cmake/legacy.cmake +++ b/UI/cmake/legacy.cmake @@ -80,17 +80,11 @@ target_sources( target_sources( obs - PRIVATE auth-oauth.cpp - auth-oauth.hpp - auth-listener.cpp - auth-listener.hpp - obs-app.cpp + PRIVATE obs-app.cpp obs-app.hpp obs-proxy-style.cpp obs-proxy-style.hpp api-interface.cpp - auth-base.cpp - auth-base.hpp broadcast-flow.cpp broadcast-flow.hpp display-helpers.hpp diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 2aa69bf07ea2ef..c5f5432fa19b93 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -140,21 +140,6 @@ ExtraBrowsers="Custom Browser Docks" ExtraBrowsers.Info="Add docks by giving them a name and URL, then click Apply or Close to open the docks. You can add or remove docks at any time." ExtraBrowsers.DockName="Dock Name" -# Auth -Auth.Authing.Title="Authenticating..." -Auth.Authing.Text="Authenticating with %1, please wait..." -Auth.AuthFailure.Title="Authentication Failure" -Auth.AuthFailure.Text="Failed to authenticate with %1:\n\n%2: %3" -Auth.InvalidScope.Title="Authentication Required" -Auth.InvalidScope.Text="The authentication requirements for %1 have changed. Some features may not be available." -Auth.LoadingChannel.Title="Loading channel information..." -Auth.LoadingChannel.Text="Loading channel information for %1, please wait..." -Auth.LoadingChannel.Error="Couldn't get channel information." -Auth.ChannelFailure.Title="Failed to load channel" -Auth.ChannelFailure.Text="Failed to load channel information for %1\n\n%2: %3" -Auth.Chat="Chat" -Auth.StreamInfo="Stream Information" - # copy filters Copy.Filters="Copy Filters" Paste.Filters="Paste Filters" diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index f4e548e8bc6cf0..73cc91811d2f2d 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1091,10 +1091,6 @@ bool SimpleOutput::SetupStreaming(obs_service_t *service) if (!Active()) SetupOutputs(); - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - /* --------------------- */ const char *type = GetStreamOutputType(service); @@ -2071,10 +2067,6 @@ bool AdvancedOutput::SetupStreaming(obs_service_t *service) if (!Active()) SetupOutputs(); - Auth *auth = main->GetAuth(); - if (auth) - auth->OnStreamConfig(); - /* --------------------- */ const char *type = GetStreamOutputType(service); diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index da2334dca2c604..e4348c1f682c26 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -313,9 +313,7 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir.c_str()); - Auth::Save(); if (create_new) { - auth.reset(); DestroyPanelCookieManager(); } else if (!rename) { DuplicateCurrentCookieProfile(config); @@ -351,8 +349,6 @@ bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - // Run auto configuration setup wizard when a new profile is made to assist // setting up blank settings if (create_new && showWizardChecked) { @@ -626,8 +622,6 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) bool needsRestart = ProfileNeedsRestart(config, settingsRequiringRestart); - Auth::Save(); - auth.reset(); DeleteCookies(); DestroyPanelCookieManager(); @@ -646,8 +640,6 @@ void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - if (api) { api->on_event(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); api->on_event(OBS_FRONTEND_EVENT_PROFILE_CHANGED); @@ -824,8 +816,6 @@ void OBSBasic::ChangeProfile() config_set_string(App()->GlobalConfig(), "Basic", "Profile", newName); config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir); - Auth::Save(); - auth.reset(); DestroyPanelCookieManager(); config.Swap(basicConfig); @@ -837,8 +827,6 @@ void OBSBasic::ChangeProfile() UpdateTitleBar(); UpdateVolumeControlsDecayRate(); - Auth::Load(); - CheckForSimpleModeX264Fallback(); blog(LOG_INFO, "Switched to profile '%s' (%s)", newName, newDir); diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 2e5d74d005c5eb..bca98b94bd5b7a 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -2164,9 +2164,6 @@ void OBSBasic::OnFirstLoad() introCheckThread->start(); } #endif - - Auth::Load(); - bool showLogViewerOnStartup = config_get_bool( App()->GlobalConfig(), "LogViewer", "ShowLogStartup"); @@ -4931,9 +4928,7 @@ void OBSBasic::closeEvent(QCloseEvent *event) signalHandlers.clear(); - Auth::Save(); SaveProjectNow(); - auth.reset(); delete extraBrowsers; @@ -7849,14 +7844,8 @@ void OBSBasic::on_streamButton_clicked() return; } - Auth *auth = GetAuth(); - - auto action = - (auth && auth->external()) - ? StreamSettingsAction::ContinueStream - : UIValidation::StreamSettingsConfirmation( - this, service); - switch (action) { + switch (UIValidation::StreamSettingsConfirmation(this, + service)) { case StreamSettingsAction::ContinueStream: break; case StreamSettingsAction::OpenSettings: diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 8dbb260e9f21a8..4f508e1e05e18a 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -36,7 +36,6 @@ #include "window-missing-files.hpp" #include "window-projector.hpp" #include "window-basic-about.hpp" -#include "auth-base.hpp" #include "log-viewer.hpp" #include "undo-stack-obs.hpp" #include "broadcast-flow.hpp" @@ -223,8 +222,6 @@ class OBSBasic : public OBSMainWindow { private: obs_frontend_callbacks *api = nullptr; - std::shared_ptr auth; - std::vector volumes; std::vector signalHandlers; @@ -944,8 +941,6 @@ private slots: void SaveService(); bool LoadService(); - inline Auth *GetAuth() { return auth.get(); } - inline void EnableOutputs(bool enable) { if (enable) { From 03bcbb01ddf38bf1c4285c8e1fbeef556a5cffe5 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 6 Jul 2023 16:25:58 +0200 Subject: [PATCH 48/65] libobs,UI,docs: Replace supported resolution and max bitrate getters --- UI/window-basic-auto-config-test.cpp | 28 ++++++-- UI/window-basic-settings-stream.cpp | 104 ++++++++++++++++++++------- UI/window-basic-settings.cpp | 9 +++ docs/sphinx/reference-services.rst | 36 ++++++++++ libobs/obs-service.c | 54 ++++++++++++++ libobs/obs-service.h | 13 ++++ libobs/obs.h | 16 ++++- 7 files changed, 227 insertions(+), 33 deletions(-) diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index 11ce8d8cef2a58..d0926b449f8c4b 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -1004,17 +1004,35 @@ void AutoConfigTestPage::FinalizeResults() vencoder_settings, nullptr); BPtr res_list; - size_t res_count; - int maxFPS; - obs_service_get_supported_resolutions(wiz->service, &res_list, - &res_count); - obs_service_get_max_fps(wiz->service, &maxFPS); + size_t res_count = 0; + bool res_with_fps = false; + int maxFPS = 0; + obs_service_get_supported_resolutions2( + wiz->service, &res_list, &res_count, &res_with_fps); + if (!res_with_fps) + obs_service_get_max_fps(wiz->service, &maxFPS); if (res_list) { set_closest_res(wiz->idealResolutionCX, wiz->idealResolutionCY, res_list, res_count); } + + if (res_count && res_with_fps) { + for (size_t i = 0; i < res_count; i++) { + { + if (res_list[i].cx != + wiz->idealResolutionCX || + res_list[i].cy != + wiz->idealResolutionCY) + continue; + + if (res_list[i].fps > maxFPS) + maxFPS = res_list[i].fps; + } + } + } + if (maxFPS) { double idealFPS = (double)wiz->idealFPSNum / (double)wiz->idealFPSDen; diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index d0b9ccee9a30c2..206071c3de6d77 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -79,7 +79,6 @@ void OBSBasicSettings::LoadStream1Settings() streamServiceProps = CreateTempServicePropertyView(settings); ui->serviceLayout->addWidget(streamServiceProps); - UpdateServiceRecommendations(); DisplayEnforceWarning(ignoreRecommended); loading = false; @@ -340,17 +339,66 @@ void OBSBasicSettings::UpdateVodTrackSetting() } } +extern const char *get_simple_output_encoder(const char *name); + void OBSBasicSettings::UpdateServiceRecommendations() { - int vbitrate, abitrate; + bool simple = (ui->outputMode->currentIndex() == 0); + QString vcodec, acodec; + int vbitrate = 0, abitrate; BPtr res_list; size_t res_count; - int fps; + bool res_with_fps = false; + obs_service_resolution *best_res = nullptr; + int fps = 0; + + obs_service_get_supported_resolutions2(tempService, &res_list, + &res_count, &res_with_fps); + + if (res_count) { + int best_res_pixels = 0; + + for (size_t i = 0; i < res_count; i++) { + obs_service_resolution *res = &res_list[i]; + int res_pixels = res->cx + res->cy; + if (res_pixels > best_res_pixels || + (res_pixels == best_res_pixels && + res->fps > best_res->fps)) { + best_res = res; + best_res_pixels = res_pixels; + } + } + + if (res_with_fps) + fps = best_res->fps; + } + + if (!res_with_fps) + obs_service_get_max_fps(tempService, &fps); + + if (simple) { + QString encoder = + ui->simpleOutStrEncoder->currentData().toString(); + const char *id = get_simple_output_encoder(QT_TO_UTF8(encoder)); + vcodec = obs_get_encoder_codec(id); + acodec = ui->simpleOutStrAEncoder->currentData().toString(); + } else { + QString vencoder = ui->advOutEncoder->currentData().toString(); + QString aencoder = ui->advOutAEncoder->currentData().toString(); + vcodec = obs_get_encoder_codec(QT_TO_UTF8(vencoder)); + acodec = obs_get_encoder_codec(QT_TO_UTF8(aencoder)); + } - obs_service_get_max_bitrate(tempService, &vbitrate, &abitrate); - obs_service_get_supported_resolutions(tempService, &res_list, - &res_count); - obs_service_get_max_fps(tempService, &fps); + if (best_res) + vbitrate = obs_service_get_max_video_bitrate( + tempService, QT_TO_UTF8(vcodec), *best_res); + + if (!vbitrate) + vbitrate = obs_service_get_max_codec_bitrate( + tempService, QT_TO_UTF8(vcodec)); + + abitrate = obs_service_get_max_codec_bitrate(tempService, + QT_TO_UTF8(acodec)); QString text; @@ -364,25 +412,13 @@ void OBSBasicSettings::UpdateServiceRecommendations() text += ENFORCE_TEXT("MaxAudioBitrate") .arg(QString::number(abitrate)); } - if (res_count) { + if (best_res) { if (!text.isEmpty()) text += "
"; - obs_service_resolution best_res = {}; - int best_res_pixels = 0; - - for (size_t i = 0; i < res_count; i++) { - obs_service_resolution res = res_list[i]; - int res_pixels = res.cx + res.cy; - if (res_pixels > best_res_pixels) { - best_res = res; - best_res_pixels = res_pixels; - } - } - QString res_str = - QString("%1x%2").arg(QString::number(best_res.cx), - QString::number(best_res.cy)); + QString("%1x%2").arg(QString::number(best_res->cx), + QString::number(best_res->cy)); text += ENFORCE_TEXT("MaxResolution").arg(res_str); } if (fps) { @@ -499,12 +535,14 @@ bool OBSBasicSettings::UpdateResFPSLimits() bool ignoreRecommended = ui->ignoreRecommended->isChecked(); BPtr res_list; size_t res_count = 0; + bool res_with_fps = false; int max_fps = 0; - if (!IsCustomOrInternalService() && !ignoreRecommended) { - obs_service_get_supported_resolutions(tempService, &res_list, - &res_count); - obs_service_get_max_fps(tempService, &max_fps); + if (!ignoreRecommended) { + obs_service_get_supported_resolutions2( + tempService, &res_list, &res_count, &res_with_fps); + if (!res_with_fps) + obs_service_get_max_fps(tempService, &max_fps); } /* ------------------------------------ */ @@ -522,6 +560,19 @@ bool OBSBasicSettings::UpdateResFPSLimits() if (res_count) set_closest_res(cx, cy, res_list, res_count); + if (res_count && res_with_fps) { + for (size_t i = 0; i < res_count; i++) { + { + if (res_list[i].cx != cx || + res_list[i].cy != cy) + continue; + + if (res_list[i].fps > max_fps) + max_fps = res_list[i].fps; + } + } + } + if (max_fps) { int fpsType = ui->fpsType->currentIndex(); @@ -681,7 +732,6 @@ static bool service_supports_codec(const char **codecs, const char *codec) } extern bool EncoderAvailable(const char *encoder); -extern const char *get_simple_output_encoder(const char *name); static inline bool service_supports_encoder(const char **codecs, const char *encoder) diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 20a3ea6609ad30..f0285883eeb306 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -2502,6 +2502,9 @@ void OBSBasicSettings::LoadOutputSettings() ui->advNetworkGroupBox->setEnabled(false); } + /* Services side but requires to be done once encoders are loaded */ + UpdateServiceRecommendations(); + loading = false; } @@ -4364,6 +4367,9 @@ void OBSBasicSettings::on_advOutEncoder_currentIndexChanged() ui->advOutUseRescale->setVisible(true); ui->advOutRescale->setVisible(true); + + /* Update services page test if codec has changed */ + UpdateServiceRecommendations(); } void OBSBasicSettings::on_advOutRecEncoder_currentIndexChanged(int idx) @@ -5500,6 +5506,9 @@ void OBSBasicSettings::SimpleStreamingEncoderChanged() idx = ui->simpleOutPreset->findData(QVariant(defaultPreset)); ui->simpleOutPreset->setCurrentIndex(idx); + + /* Update services page test if codec has changed */ + UpdateServiceRecommendations(); } #define ESTIMATE_STR "Basic.Settings.Output.ReplayBuffer.Estimate" diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index 501aced97de1d9..27fba584eb9a63 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -248,6 +248,24 @@ Service Definition Structure :return: If the bandwith test is enabled or not +.. member:: void (*obs_service_info.get_supported_resolutions2)(void *data, struct obs_service_resolution **resolutions, size_t *count, bool *with_fps) + + (Optional) + + :return: Supported resolutions, number of those and if they provide a FPS value + +.. member:: int (*obs_service_info.get_max_video_bitrate)(void *data, const char *codec, struct obs_service_resolution resolution) + + (Optional) + + :return: Maximum video bitrate for a given codec and resolution + +.. member:: int (*obs_service_info.get_max_codec_bitrate)(void *data, const char *codec) + + (Optional) + + :return: Maximum bitrate for a given codec + General Service Functions ------------------------- @@ -499,6 +517,24 @@ General Service Functions :return: If the service has bandwidth test enabled +--------------------- + +.. function:: void obs_service_get_supported_resolutions2(const obs_service_t *service, struct obs_service_resolution **resolutions, size_t *count, bool *with_fps) + + :return: Supported resolutions, number of those and if they provide a FPS value + +--------------------- + +.. function:: int obs_service_get_max_codec_bitrate(const obs_service_t *service, const char *codec) + + :return: Maximum bitrate for a given codec + +--------------------- + +.. function:: int obs_service_get_max_video_bitrate(const obs_service_t *service, const char *codec, struct obs_service_resolution resolution) + + :return: Maximum video bitrate for a given codec and resolution + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-service.c b/libobs/obs-service.c index 6646808a691236..113ffc597f211e 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -601,3 +601,57 @@ bool obs_service_bandwidth_test_enabled(const obs_service_t *service) return service->info.bandwidth_test_enabled(service->context.data); } + +int obs_service_get_max_codec_bitrate(const obs_service_t *service, + const char *codec) +{ + if (!obs_service_valid(service, "obs_service_get_max_codec_bitrate")) + return 0; + + if (!service->info.get_max_codec_bitrate) + return 0; + + return service->info.get_max_codec_bitrate(service->context.data, + codec); +} + +void obs_service_get_supported_resolutions2( + const obs_service_t *service, + struct obs_service_resolution **resolutions, size_t *count, + bool *with_fps) +{ + if (!obs_service_valid(service, "obs_service_supported_resolutions2")) + return; + if (!obs_ptr_valid(resolutions, "obs_service_supported_resolutions2")) + return; + if (!obs_ptr_valid(count, "obs_service_supported_resolutions2")) + return; + if (!obs_ptr_valid(with_fps, "obs_service_supported_resolutions2")) + return; + + *resolutions = NULL; + *count = 0; + *with_fps = false; + + if (service->info.get_supported_resolutions2) { + service->info.get_supported_resolutions2( + service->context.data, resolutions, count, with_fps); + } else if (service->info.get_supported_resolutions) { + service->info.get_supported_resolutions(service->context.data, + resolutions, count); + } +} + +int obs_service_get_max_video_bitrate(const obs_service_t *service, + const char *codec, + struct obs_service_resolution resolution) +{ + if (!obs_service_valid(service, "obs_service_get_max_video_bitrate")) + return 0; + + if (!service->info.get_max_video_bitrate) + return 0; + + return service->info.get_max_video_bitrate(service->context.data, codec, + resolution); +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index ae8f14b5834b4c..210ef0a36759dc 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -37,6 +37,7 @@ enum obs_service_info_flag { struct obs_service_resolution { int cx; int cy; + int fps; }; /* NOTE: Odd numbers are reserved for custom info from third-party protocols */ @@ -104,11 +105,14 @@ struct obs_service_info { /* TODO: Rename to 'get_preferred_output_type' once a API/ABI break happen */ const char *(*get_output_type)(void *data); + /* deprecated */ void (*get_supported_resolutions)( void *data, struct obs_service_resolution **resolutions, size_t *count); + void (*get_max_fps)(void *data, int *fps); + /* deprecated */ void (*get_max_bitrate)(void *data, int *video_bitrate, int *audio_bitrate); @@ -134,6 +138,15 @@ struct obs_service_info { bool (*can_bandwidth_test)(void *data); void (*enable_bandwidth_test)(void *data, bool enabled); bool (*bandwidth_test_enabled)(void *data); + + void (*get_supported_resolutions2)( + void *data, struct obs_service_resolution **resolutions, + size_t *count, bool *with_fps); + + int (*get_max_video_bitrate)(void *data, const char *codec, + struct obs_service_resolution resolution); + + int (*get_max_codec_bitrate)(void *data, const char *codec); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index 071330fa68f7b1..96cb389936eea3 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2640,11 +2640,12 @@ EXPORT void *obs_service_get_type_data(obs_service_t *service); EXPORT const char *obs_service_get_id(const obs_service_t *service); -EXPORT void obs_service_get_supported_resolutions( +OBS_DEPRECATED EXPORT void obs_service_get_supported_resolutions( const obs_service_t *service, struct obs_service_resolution **resolutions, size_t *count); EXPORT void obs_service_get_max_fps(const obs_service_t *service, int *fps); +OBS_DEPRECATED EXPORT void obs_service_get_max_bitrate(const obs_service_t *service, int *video_bitrate, int *audio_bitrate); @@ -2683,6 +2684,19 @@ EXPORT void obs_service_enable_bandwidth_test(const obs_service_t *service, bool enabled); EXPORT bool obs_service_bandwidth_test_enabled(const obs_service_t *service); +EXPORT int obs_service_get_max_codec_bitrate(const obs_service_t *service, + const char *codec); + +EXPORT void obs_service_get_supported_resolutions2( + const obs_service_t *service, + struct obs_service_resolution **resolutions, size_t *count, + bool *with_fps); + +EXPORT int +obs_service_get_max_video_bitrate(const obs_service_t *service, + const char *codec, + struct obs_service_resolution resolution); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, From 29a1b5975d6eeb321ca827e21a0ea53c33b906d1 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 6 Jul 2023 16:01:41 +0200 Subject: [PATCH 49/65] obs-youtube: Add maximum bitrates --- plugins/obs-youtube/youtube-service-info.cpp | 13 +++++++++++++ plugins/obs-youtube/youtube-service.cpp | 2 ++ plugins/obs-youtube/youtube-service.hpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/plugins/obs-youtube/youtube-service-info.cpp b/plugins/obs-youtube/youtube-service-info.cpp index 8de1cbf5f6c19b..134f2119fd90a8 100644 --- a/plugins/obs-youtube/youtube-service-info.cpp +++ b/plugins/obs-youtube/youtube-service-info.cpp @@ -67,6 +67,19 @@ bool YouTubeService::InfoCanTryToConnect(void *data) return false; } +int YouTubeService::InfoGetMaxCodecBitrate(void *, const char *codec_) +{ + std::string codec(codec_); + + if (codec == "h264" || codec == "hevc" || codec == "av1") + return 51000; + + if (codec == "aac") + return 160; + + return 0; +} + obs_properties_t *YouTubeService::InfoGetProperties(void *data) { if (data) diff --git a/plugins/obs-youtube/youtube-service.cpp b/plugins/obs-youtube/youtube-service.cpp index 9ce7c55842551f..279aa8637a98b5 100644 --- a/plugins/obs-youtube/youtube-service.cpp +++ b/plugins/obs-youtube/youtube-service.cpp @@ -38,6 +38,8 @@ YouTubeService::YouTubeService() info.get_defaults = YouTubeConfig::InfoGetDefault; info.get_properties = InfoGetProperties; + info.get_max_codec_bitrate = InfoGetMaxCodecBitrate; + #ifdef OAUTH_ENABLED info.can_bandwidth_test = InfoCanBandwidthTest; info.enable_bandwidth_test = InfoEnableBandwidthTest; diff --git a/plugins/obs-youtube/youtube-service.hpp b/plugins/obs-youtube/youtube-service.hpp index 2534a0d210cd3c..4894a79390fc2f 100644 --- a/plugins/obs-youtube/youtube-service.hpp +++ b/plugins/obs-youtube/youtube-service.hpp @@ -32,6 +32,8 @@ class YouTubeService { static bool InfoCanTryToConnect(void *data); + static int InfoGetMaxCodecBitrate(void *data, const char *codec); + static obs_properties_t *InfoGetProperties(void *data); #ifdef OAUTH_ENABLED From 16e74f47ab742d7720d4f809fd707bdda0613e0f Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 6 Jul 2023 16:01:59 +0200 Subject: [PATCH 50/65] obs-twitch: Add maximum bitrates --- plugins/obs-twitch/twitch-service-info.cpp | 13 +++++++++++++ plugins/obs-twitch/twitch-service.cpp | 2 ++ plugins/obs-twitch/twitch-service.hpp | 2 ++ 3 files changed, 17 insertions(+) diff --git a/plugins/obs-twitch/twitch-service-info.cpp b/plugins/obs-twitch/twitch-service-info.cpp index d9fbb3bb2195b8..5e7675a5769169 100644 --- a/plugins/obs-twitch/twitch-service-info.cpp +++ b/plugins/obs-twitch/twitch-service-info.cpp @@ -67,6 +67,19 @@ bool TwitchService::InfoCanTryToConnect(void *data) return false; } +int TwitchService::InfoGetMaxCodecBitrate(void *, const char *codec_) +{ + std::string codec(codec_); + + if (codec == "h264") + return 6000; + + if (codec == "aac") + return 320; + + return 0; +} + obs_properties_t *TwitchService::InfoGetProperties(void *data) { if (data) diff --git a/plugins/obs-twitch/twitch-service.cpp b/plugins/obs-twitch/twitch-service.cpp index 1b5ea67b219d21..f7f7a9319cc3a3 100644 --- a/plugins/obs-twitch/twitch-service.cpp +++ b/plugins/obs-twitch/twitch-service.cpp @@ -44,6 +44,8 @@ TwitchService::TwitchService() info.enable_bandwidth_test = InfoEnableBandwidthTest; info.bandwidth_test_enabled = InfoBandwidthTestEnabled; + info.get_max_codec_bitrate = InfoGetMaxCodecBitrate; + obs_register_service(&info); #ifdef OAUTH_ENABLED diff --git a/plugins/obs-twitch/twitch-service.hpp b/plugins/obs-twitch/twitch-service.hpp index 9688331b97e7bd..de0ed0d913afca 100644 --- a/plugins/obs-twitch/twitch-service.hpp +++ b/plugins/obs-twitch/twitch-service.hpp @@ -36,6 +36,8 @@ class TwitchService { static bool InfoCanTryToConnect(void *data); + static int InfoGetMaxCodecBitrate(void *data, const char *codec); + static obs_properties_t *InfoGetProperties(void *data); static bool InfoCanBandwidthTest(void *data); From 03d14c59221f1f85f2db82bc42a12da4f3f1a1b9 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Thu, 6 Jul 2023 16:33:10 +0200 Subject: [PATCH 51/65] obs-services: Add supported resolutions and maximums --- build-aux/json-schema/service.json | 103 +++ .../obs-services/generated/services-json.hpp | 49 ++ plugins/obs-services/json/services.json | 585 ++++++++++++++++++ plugins/obs-services/service-config.cpp | 18 + plugins/obs-services/service-config.hpp | 9 + .../obs-services/service-instance-info.cpp | 26 + plugins/obs-services/service-instance.cpp | 93 +++ plugins/obs-services/service-instance.hpp | 24 + 8 files changed, 907 insertions(+) diff --git a/build-aux/json-schema/service.json b/build-aux/json-schema/service.json index e76011369c8b47..23652be09c053e 100644 --- a/build-aux/json-schema/service.json +++ b/build-aux/json-schema/service.json @@ -127,13 +127,116 @@ }, "minProperties": 1, "unevaluatedProperties": false + }, + "supported_resolutions": { + "type": "array", + "title": "Supported Resolutions", + "description": "Resolution supported by the service. All with or all withtout the framerate.", + "items": { + "type": "string", + "pattern": "^[0-9]+x[0-9]+(|@[0-9]+)$" + }, + "allOf": [ + { + "if": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } }, + "then": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } } + }, + { + "if": { "items": { "$ref": "#/$defs/resolutionPattern" } }, + "then": { "items": { "$ref": "#/$defs/resolutionPattern" } } + } + ], + "minItems": 1, + "uniqueItems": true, + "additionalItems": false + }, + "maximums": { + "type": "object", + "title": "Maximums", + "description": "Maximum values allowed by the service", + "properties": { + "framerate": { + "type": "integer", + "title": "Maximum Framerate", + "exclusiveMinimum": 0 + }, + "video_bitrate": { + "type": "object", + "title": "Maximum Video Bitrate", + "description": "Maximum video bitrate per codec", + "propertyNames": { "$ref": "codecDefs.json#/$defs/videoCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "audio_bitrate": { + "type": "object", + "title": "Maximum Audio Bitrate", + "description": "Maximum audio bitrate per codec", + "propertyNames": { "$ref": "codecDefs.json#/$defs/audioCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "video_bitrate_matrix": { + "type": "object", + "title": "Maximum Video Bitrate Matrix", + "description": "Maximum video bitrate per supported resolution with framerate and per codec", + "propertyNames": { "$ref": "#/$defs/resolutionFrameratePattern" }, + "additionalProperties": { + "type": "object", + "propertyNames": { "$ref": "codecDefs.json#/$defs/videoCodecEnum" }, + "additionalProperties": { "type": "integer", "exclusiveMinimum": 0 }, + "minProperties": 1, + "unevaluatedProperties":false + }, + "minProperties": 1, + "unevaluatedProperties": false + } + }, + "minProperties": 1, + "unevaluatedProperties": false } }, + "allOf": [ + { + "$comment": "Make 'video_bitrate_matrix' unusable if 'supported_resolutions' is empty", + "if": { "properties": { "supported_resolutions": { "maxItems": 0 } } }, + "then": { "properties": { + "maximums": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if supported resolutions with framerates are set", "maxProperties": 0 } } }, + "recommended": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if supported resolutions with framerates are set", "maxProperties": 0 } } } + } } + }, + { + "$comment": "Make 'video_bitrate_matrix' unusable if supported resolutions are without framerate", + "if": { "not": { "properties": { "supported_resolutions": { "items": { "$ref": "#/$defs/resolutionFrameratePattern" } } } } }, + "then": { "properties": { + "maximums": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if 'supported_resolutions' is with framerates", "maxProperties": 0 } } }, + "recommended": { "properties": { "video_bitrate_matrix": { "description": "This matrix is only available if 'supported_resolutions' is with framerates", "maxProperties": 0 } } } + } } + }, + { + "$comment": "Make 'framerate' unusable if supported resolutions are with framerate", + "if": { "not": {"properties": { "supported_resolutions": { "items": { "$ref": "#/$defs/resolutionPattern" } } } } }, + "then": { "properties": { + "maximums": { "properties": { "framerate": { "description": "This property is only available if 'supported_resolutions' is without framerates or not set", "const": 0 } } }, + "recommended": { "properties": { "framerate": { "description": "This property is only available if 'supported_resolutions' is without framerates or not set", "const": 0 } } } + } } + } + ], "required": ["servers"], "$defs": { "httpPattern": { "$comment": "Pattern to enforce HTTP(S) links", "pattern": "^https?://" + }, + "resolutionPattern": { + "$comment": "Pattern for resolution without framerate", + "pattern": "^[0-9]+x[0-9]+$" + }, + "resolutionFrameratePattern": { + "$comment": "Pattern for resolution with framerate", + "pattern": "^[0-9]+x[0-9]+@[0-9]+$" } } } diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp index a2162bf983c748..004f4d7fe13f6e 100644 --- a/plugins/obs-services/generated/services-json.hpp +++ b/plugins/obs-services/generated/services-json.hpp @@ -84,6 +84,25 @@ namespace OBSServices { } #endif + /** + * Maximum values allowed by the service + */ + struct Maximums { + /** + * Maximum audio bitrate per codec + */ + std::optional> audioBitrate; + std::optional framerate; + /** + * Maximum video bitrate per codec + */ + std::optional> videoBitrate; + /** + * Maximum video bitrate per supported resolution with framerate and per codec + */ + std::optional>> videoBitrateMatrix; + }; + /** * Properties related to the RIST protocol */ @@ -157,6 +176,10 @@ namespace OBSServices { * Name of the streaming service. Will be displayed in the Service dropdown. */ std::string name; + /** + * Maximum values allowed by the service + */ + std::optional maximums; /** * Link that provides additional info about the service, presented in the UI as a button * next to the services dropdown. @@ -175,6 +198,10 @@ namespace OBSServices { * Codecs that are supported by the service. */ std::optional supportedCodecs; + /** + * Resolution supported by the service. All with or all withtout the framerate. + */ + std::optional> supportedResolutions; /** * Properties related to the RIST protocol */ @@ -192,6 +219,9 @@ namespace OBSServices { } namespace OBSServices { + void from_json(const json & j, Maximums & x); + void to_json(json & j, const Maximums & x); + void from_json(const json & j, RistProperties & x); void to_json(json & j, const RistProperties & x); @@ -219,6 +249,21 @@ namespace OBSServices { void from_json(const json & j, ProtocolSupportedVideoCodec & x); void to_json(json & j, const ProtocolSupportedVideoCodec & x); + inline void from_json(const json & j, Maximums& x) { + x.audioBitrate = get_stack_optional>(j, "audio_bitrate"); + x.framerate = get_stack_optional(j, "framerate"); + x.videoBitrate = get_stack_optional>(j, "video_bitrate"); + x.videoBitrateMatrix = get_stack_optional>>(j, "video_bitrate_matrix"); + } + + inline void to_json(json & j, const Maximums & x) { + j = json::object(); + j["audio_bitrate"] = x.audioBitrate; + j["framerate"] = x.framerate; + j["video_bitrate"] = x.videoBitrate; + j["video_bitrate_matrix"] = x.videoBitrateMatrix; + } + inline void from_json(const json & j, RistProperties& x) { x.encryptPassphrase = j.at("encrypt_passphrase").get(); x.srpUsernamePassword = j.at("srp_username_password").get(); @@ -269,10 +314,12 @@ namespace OBSServices { x.common = get_stack_optional(j, "common"); x.id = j.at("id").get(); x.name = j.at("name").get(); + x.maximums = get_stack_optional(j, "maximums"); x.moreInfoLink = get_stack_optional(j, "more_info_link"); x.servers = j.at("servers").get>(); x.streamKeyLink = get_stack_optional(j, "stream_key_link"); x.supportedCodecs = get_stack_optional(j, "supported_codecs"); + x.supportedResolutions = get_stack_optional>(j, "supported_resolutions"); x.rist = get_stack_optional(j, "RIST"); x.srt = get_stack_optional(j, "SRT"); } @@ -282,10 +329,12 @@ namespace OBSServices { j["common"] = x.common; j["id"] = x.id; j["name"] = x.name; + j["maximums"] = x.maximums; j["more_info_link"] = x.moreInfoLink; j["servers"] = x.servers; j["stream_key_link"] = x.streamKeyLink; j["supported_codecs"] = x.supportedCodecs; + j["supported_resolutions"] = x.supportedResolutions; j["RIST"] = x.rist; j["SRT"] = x.srt; } diff --git a/plugins/obs-services/json/services.json b/plugins/obs-services/json/services.json index 60fdce67be11a8..04a4022d692137 100644 --- a/plugins/obs-services/json/services.json +++ b/plugins/obs-services/json/services.json @@ -38,6 +38,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 2500 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -56,6 +64,19 @@ "h264" ] } + }, + "supported_resolutions": [ + "1920x1080", + "1280x720" + ], + "maximums": { + "video_bitrate": { + "h264": 8000 + }, + "framerate": 30, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -80,6 +101,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 256 + } } }, { @@ -103,6 +132,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 8000 + }, + "audio_bitrate": { + "aac": 320 + } } }, { @@ -121,6 +158,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3500 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -211,6 +256,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 15000 + }, + "audio_bitrate": { + "aac": 320 + } } }, { @@ -264,6 +317,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 15000 + }, + "audio_bitrate": { + "aac": 320 + } } }, { @@ -284,6 +345,47 @@ "h264" ] } + }, + "supported_resolutions": [ + "640x360@30", + "640x360@60", + "852x480@30", + "852x480@60", + "1280x720@30", + "1280x720@60", + "1920x1080@30", + "1920x1080@60" + ], + "maximums": { + "video_bitrate_matrix": { + "640x360@30": { + "h264": 1000 + }, + "640x360@60": { + "h264": 1500 + }, + "852x480@30": { + "h264": 2000 + }, + "852x480@60": { + "h264": 3000 + }, + "1280x720@30": { + "h264": 4000 + }, + "1280x720@60": { + "h264": 6000 + }, + "1920x1080@30": { + "h264": 6000 + }, + "1920x1080@60": { + "h264": 9000 + } + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -438,6 +540,11 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3500 + } } }, { @@ -481,6 +588,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 8000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -499,6 +614,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3000 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -517,6 +640,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 7500 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -560,6 +691,11 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3500 + } } }, { @@ -596,6 +732,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 8000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -614,6 +758,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -652,6 +804,21 @@ "h264" ] } + }, + "supported_resolutions": [ + "1920x1080", + "1280x720", + "852x480", + "480x360" + ], + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "framerate": 30, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -745,6 +912,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 50000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -790,6 +965,11 @@ "h264" ] } + }, + "maximums": { + "audio_bitrate": { + "aac": 160 + } } }, { @@ -865,6 +1045,15 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 12000 + }, + "framerate": 60, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -909,6 +1098,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -927,6 +1124,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 900 + }, + "audio_bitrate": { + "aac": 96 + } } }, { @@ -945,6 +1150,19 @@ "h264" ] } + }, + "supported_resolutions": [ + "1920x1080", + "1280x720" + ], + "maximums": { + "video_bitrate": { + "h264": 3000 + }, + "framerate": 30, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -968,6 +1186,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 4000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -986,6 +1212,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 7000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1005,6 +1239,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 9000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1023,6 +1265,20 @@ "h264" ] } + }, + "supported_resolutions": [ + "1280x720", + "852x480", + "480x360" + ], + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "framerate": 30, + "audio_bitrate": { + "aac": 320 + } } }, { @@ -1041,6 +1297,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 2000 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -1077,6 +1341,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 320 + } } }, { @@ -1095,6 +1367,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 8000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1113,6 +1393,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 2500 + }, + "audio_bitrate": { + "aac": 256 + } } }, { @@ -1131,6 +1419,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 4000 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -1149,6 +1445,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1197,6 +1501,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1230,6 +1542,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1248,6 +1568,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3600 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -1271,6 +1599,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 2500 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1289,6 +1625,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 7000 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -1343,6 +1687,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5808 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1361,6 +1713,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 904 + }, + "audio_bitrate": { + "aac": 96 + } } }, { @@ -1394,6 +1754,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 10000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1500,6 +1868,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 3500 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1518,6 +1894,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 20000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1544,6 +1928,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1562,6 +1954,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1615,6 +2015,15 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 10000 + }, + "framerate": 60, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1687,6 +2096,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1775,6 +2192,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -1839,6 +2264,12 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 20000 + }, + "framerate": 60 } }, { @@ -1899,6 +2330,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 32000 + }, + "audio_bitrate": { + "aac": 256 + } } }, { @@ -1941,6 +2380,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1959,6 +2406,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 2500 + }, + "audio_bitrate": { + "aac": 128 + } } }, { @@ -1978,6 +2433,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 128 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -1996,6 +2459,20 @@ "h264" ] } + }, + "supported_resolutions": [ + "960x540@30", + "1280x720@30" + ], + "maximums": { + "video_bitrate_matrix": { + "960x540@30": { + "h264": 3000 + }, + "1280x720@30": { + "h264": 4000 + } + } } }, { @@ -2031,6 +2508,35 @@ "h264" ] } + }, + "supported_resolutions": [ + "852x480@30", + "1280x720@30", + "1280x720@60", + "1920x1080@30", + "1920x1080@60" + ], + "maximums": { + "video_bitrate_matrix": { + "852x480@30": { + "h264": 1200 + }, + "1280x720@30": { + "h264": 3600 + }, + "1280x720@60": { + "h264": 4200 + }, + "1920x1080@30": { + "h264": 5000 + }, + "1920x1080@60": { + "h264": 7200 + } + }, + "audio_bitrate": { + "aac": 196 + } } }, { @@ -2049,6 +2555,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 5000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -2069,6 +2583,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -2099,6 +2621,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + }, + "audio_bitrate": { + "aac": 160 + } } }, { @@ -2222,6 +2752,11 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 16000 + } } }, { @@ -2242,6 +2777,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 20000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -2262,6 +2805,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 1800 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -2311,6 +2862,11 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 20000 + } } }, { @@ -2351,6 +2907,11 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 6000 + } } }, { @@ -2370,6 +2931,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 9000 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -2390,6 +2959,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 7500 + }, + "audio_bitrate": { + "aac": 192 + } } }, { @@ -2420,6 +2997,14 @@ "h264" ] } + }, + "maximums": { + "video_bitrate": { + "h264": 20000 + }, + "audio_bitrate": { + "aac": 512 + } } } ] diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index 3bd5e66e0e6c1c..7c2aa73d1a1e64 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -120,3 +120,21 @@ bool ServiceConfig::CanTryToConnect() return true; } + +void ServiceConfig::GetSupportedResolutions( + struct obs_service_resolution **resolutions, size_t *count, + bool *withFps) +{ + service->GetSupportedResolutions(resolutions, count, withFps); +} + +int ServiceConfig::GetMaxCodecBitrate(const char *codec) +{ + return service->GetMaxCodecBitrate(codec); +} + +int ServiceConfig::GetMaxVideoBitrate(const char *codec, + struct obs_service_resolution resolution) +{ + return service->GetMaxVideoBitrate(codec, resolution); +} diff --git a/plugins/obs-services/service-config.hpp b/plugins/obs-services/service-config.hpp index 8ac9130fceac2e..ba37ee1b645170 100644 --- a/plugins/obs-services/service-config.hpp +++ b/plugins/obs-services/service-config.hpp @@ -47,4 +47,13 @@ class ServiceConfig { const char *ConnectInfo(uint32_t type); bool CanTryToConnect(); + + void + GetSupportedResolutions(struct obs_service_resolution **resolutions, + size_t *count, bool *withFps); + + int GetMaxCodecBitrate(const char *codec); + + int GetMaxVideoBitrate(const char *codec, + struct obs_service_resolution resolution); }; diff --git a/plugins/obs-services/service-instance-info.cpp b/plugins/obs-services/service-instance-info.cpp index 08b50ac0de8e02..b033c5015ae69a 100644 --- a/plugins/obs-services/service-instance-info.cpp +++ b/plugins/obs-services/service-instance-info.cpp @@ -71,6 +71,32 @@ bool ServiceInstance::InfoCanTryToConnect(void *data) return false; } +int ServiceInstance::InfoGetMaxCodecBitrate(void *data, const char *codec) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->GetMaxCodecBitrate(codec); + return 0; +} + +void ServiceInstance::InfoGetSupportedResolutions2( + void *data, struct obs_service_resolution **resolutions, size_t *count, + bool *withFps) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + priv->GetSupportedResolutions(resolutions, count, withFps); +} + +int ServiceInstance::InfoGetMaxVideoBitrate( + void *data, const char *codec, struct obs_service_resolution resolution) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + return priv->GetMaxVideoBitrate(codec, resolution); + return 0; +} + void ServiceInstance::InfoGetDefault2(void *typeData, obs_data_t *settings) { if (typeData) diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp index f04a9ea7577a81..aa5f6a2f8fbe82 100644 --- a/plugins/obs-services/service-instance.cpp +++ b/plugins/obs-services/service-instance.cpp @@ -41,6 +41,50 @@ ServiceInstance::ServiceInstance(const OBSServices::Service &service_) } supportedProtocols = bstrdup(protocols.c_str()); + /* Generate supported resolution */ + if (service.supportedResolutions.has_value()) { + DARRAY(struct obs_service_resolution) res_list; + supportedResolutionsWithFps = + service.supportedResolutions->at(0).find("@") != + std::string::npos; + + da_init(res_list); + for (size_t i = 0; i < service.supportedResolutions->size(); + i++) { + std::string res_str = + service.supportedResolutions->at(i); + obs_service_resolution res = {}; + if (supportedResolutionsWithFps) { + sscanf(res_str.c_str(), "%dx%d@%d", &res.cx, + &res.cy, &res.fps); + } else { + sscanf(res_str.c_str(), "%dx%d", &res.cx, + &res.cy); + res.fps = 0; + } + da_push_back(res_list, &res); + } + + if (res_list.num == service.supportedResolutions->size()) { + supportedResolutions = res_list.array; + supportedResolutionsCount = res_list.num; + + info.get_supported_resolutions2 = + InfoGetSupportedResolutions2; + + if (supportedResolutionsWithFps && + service.maximums->videoBitrateMatrix.has_value()) { + info.get_max_video_bitrate = + InfoGetMaxVideoBitrate; + } + } + } + + if (service.maximums.has_value() && + (service.maximums->videoBitrate.has_value() || + service.maximums->audioBitrate.has_value())) + info.get_max_codec_bitrate = InfoGetMaxCodecBitrate; + /* Fill service info and register it */ info.type_data = this; info.id = service.id.c_str(); @@ -164,6 +208,55 @@ void ServiceInstance::GetDefaults(obs_data_t *settings) service.servers[0].url.c_str()); } +void ServiceInstance::GetSupportedResolutions( + struct obs_service_resolution **resolutions, size_t *count, + bool *withFps) const +{ + *withFps = supportedResolutionsWithFps; + *count = supportedResolutionsCount; + *resolutions = (struct obs_service_resolution *)bmemdup( + supportedResolutions.Get(), + supportedResolutionsCount * + sizeof(struct obs_service_resolution)); +} + +int ServiceInstance::GetMaxCodecBitrate(const char *codec_) const +{ + std::string codec(codec_); + + if (!service.maximums->videoBitrateMatrix.has_value() && + service.maximums->videoBitrate.has_value()) { + if (service.maximums->videoBitrate->count(codec)) + return (int)service.maximums->videoBitrate->at(codec); + } + + if (service.maximums->audioBitrate.has_value()) { + if (service.maximums->audioBitrate->count(codec)) + return (int)service.maximums->audioBitrate->at(codec); + } + + return 0; +} + +int ServiceInstance::GetMaxVideoBitrate( + const char *codec_, struct obs_service_resolution resolution) const +{ + DStr res_dstr; + dstr_catf(res_dstr, "%dx%d@%d", resolution.cx, resolution.cy, + resolution.fps); + std::string res_str(res_dstr); + std::string codec(codec_); + + if (service.maximums->videoBitrateMatrix->count(res_str)) { + const auto bitrates = + service.maximums->videoBitrateMatrix->at(res_str); + if (bitrates.count(codec)) + return (int)bitrates.at(codec); + } + + return 0; +} + bool ModifiedProtocolCb(void *service_, obs_properties_t *props, obs_property_t *, obs_data_t *settings) { diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp index b322ccbc0b9cef..a74735b2df6951 100644 --- a/plugins/obs-services/service-instance.hpp +++ b/plugins/obs-services/service-instance.hpp @@ -15,6 +15,9 @@ class ServiceInstance { const OBSServices::Service service; BPtr supportedProtocols; + BPtr supportedResolutions; + size_t supportedResolutionsCount; + bool supportedResolutionsWithFps; static const char *InfoGetName(void *typeData); static void *InfoCreate(obs_data_t *settings, obs_service_t *service); @@ -30,6 +33,18 @@ class ServiceInstance { static bool InfoCanTryToConnect(void *data); + static void InfoGetMaxFps(void *data, int *fps); + + static int InfoGetMaxCodecBitrate(void *data, const char *codec); + + static void InfoGetSupportedResolutions2( + void *data, struct obs_service_resolution **resolutions, + size_t *count, bool *withFps); + + static int + InfoGetMaxVideoBitrate(void *data, const char *codec, + struct obs_service_resolution resolution); + static void InfoGetDefault2(void *type_data, obs_data_t *settings); static obs_properties_t *InfoGetProperties2(void *data, void *typeData); @@ -58,5 +73,14 @@ class ServiceInstance { void GetDefaults(obs_data_t *settings); + void + GetSupportedResolutions(struct obs_service_resolution **resolutions, + size_t *count, bool *withFps) const; + + int GetMaxCodecBitrate(const char *codec) const; + + int GetMaxVideoBitrate(const char *codec, + struct obs_service_resolution resolution) const; + obs_properties_t *GetProperties(); }; From eb7eeb1f84d8c3275bb36cafdb8a6d83a3145889 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 31 May 2023 14:40:56 +0200 Subject: [PATCH 52/65] custom-services,obs-services: Apply MPEG TS fixes --- plugins/custom-services/custom-rist.c | 13 +++++++++++ plugins/custom-services/custom-service.c | 18 +++++++++++++++ plugins/custom-services/custom-srt.c | 13 +++++++++++ plugins/obs-services/service-config.cpp | 22 +++++++++++++++++++ plugins/obs-services/service-config.hpp | 3 +++ .../obs-services/service-instance-info.cpp | 8 +++++++ plugins/obs-services/service-instance.cpp | 2 ++ plugins/obs-services/service-instance.hpp | 3 +++ 8 files changed, 82 insertions(+) diff --git a/plugins/custom-services/custom-rist.c b/plugins/custom-services/custom-rist.c index 9c1826f5dfea46..1e1123816cfdf8 100644 --- a/plugins/custom-services/custom-rist.c +++ b/plugins/custom-services/custom-rist.c @@ -106,6 +106,18 @@ bool rist_service_can_try_to_connect(void *data) return true; } +static void rist_service_apply_settings(void *data, obs_data_t *video_settings, + obs_data_t *audio_settings) +{ + UNUSED_PARAMETER(data); + + if (video_settings != NULL) + obs_data_set_bool(video_settings, "repeat_headers", true); + + if (audio_settings != NULL) + obs_data_set_bool(audio_settings, "set_to_ADTS", true); +} + static obs_properties_t *rist_service_properties(void *data) { UNUSED_PARAMETER(data); @@ -147,4 +159,5 @@ const struct obs_service_info custom_rist = { .get_connect_info = rist_service_connect_info, .can_try_to_connect = rist_service_can_try_to_connect, .get_audio_track_cap = rist_service_audio_track_cap, + .apply_encoder_settings = rist_service_apply_settings, }; diff --git a/plugins/custom-services/custom-service.c b/plugins/custom-services/custom-service.c index 7a960de4a17ece..9fc102f401ac26 100644 --- a/plugins/custom-services/custom-service.c +++ b/plugins/custom-services/custom-service.c @@ -158,6 +158,23 @@ bool custom_service_can_try_to_connect(void *data) return true; } +static void custom_service_apply_settings(void *data, + obs_data_t *video_settings, + obs_data_t *audio_settings) +{ + struct custom_service *service = data; + if ((strcmp(service->protocol, "SRT") != 0) && + (strcmp(service->protocol, "RIST") != 0)) { + return; + } + + if (video_settings != NULL) + obs_data_set_bool(video_settings, "repeat_headers", true); + + if (audio_settings != NULL) + obs_data_set_bool(audio_settings, "set_to_ADTS", true); +} + bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, obs_data_t *settings) { @@ -337,4 +354,5 @@ const struct obs_service_info custom_service = { .get_connect_info = custom_service_connect_info, .can_try_to_connect = custom_service_can_try_to_connect, .get_audio_track_cap = custom_service_audio_track_cap, + .apply_encoder_settings = custom_service_apply_settings, }; diff --git a/plugins/custom-services/custom-srt.c b/plugins/custom-services/custom-srt.c index 1960d8cf7c290b..22460ddbe69635 100644 --- a/plugins/custom-services/custom-srt.c +++ b/plugins/custom-services/custom-srt.c @@ -100,6 +100,18 @@ bool srt_service_can_try_to_connect(void *data) return true; } +static void srt_service_apply_settings(void *data, obs_data_t *video_settings, + obs_data_t *audio_settings) +{ + UNUSED_PARAMETER(data); + + if (video_settings != NULL) + obs_data_set_bool(video_settings, "repeat_headers", true); + + if (audio_settings != NULL) + obs_data_set_bool(audio_settings, "set_to_ADTS", true); +} + static obs_properties_t *srt_service_properties(void *data) { UNUSED_PARAMETER(data); @@ -136,4 +148,5 @@ const struct obs_service_info custom_srt = { .get_connect_info = srt_service_connect_info, .can_try_to_connect = srt_service_can_try_to_connect, .get_audio_track_cap = srt_service_audio_track_cap, + .apply_encoder_settings = srt_service_apply_settings, }; diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index 7c2aa73d1a1e64..91206dd4f3a4e3 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -138,3 +138,25 @@ int ServiceConfig::GetMaxVideoBitrate(const char *codec, { return service->GetMaxVideoBitrate(codec, resolution); } + +void ServiceConfig::ApplySettings(obs_data_t *videoSettings, + obs_data_t *audioSettings) +{ + switch (StdStringToServerProtocol(protocol)) { + case OBSServices::ServerProtocol::RIST: + case OBSServices::ServerProtocol::SRT: { + if (videoSettings != NULL) + obs_data_set_bool(videoSettings, "repeat_headers", + true); + + if (audioSettings != NULL) + obs_data_set_bool(audioSettings, "set_to_ADTS", true); + break; + } + case OBSServices::ServerProtocol::RTMP: + case OBSServices::ServerProtocol::RTMPS: + case OBSServices::ServerProtocol::HLS: + case OBSServices::ServerProtocol::WHIP: + break; + } +} diff --git a/plugins/obs-services/service-config.hpp b/plugins/obs-services/service-config.hpp index ba37ee1b645170..cfe6d03ad8344e 100644 --- a/plugins/obs-services/service-config.hpp +++ b/plugins/obs-services/service-config.hpp @@ -56,4 +56,7 @@ class ServiceConfig { int GetMaxVideoBitrate(const char *codec, struct obs_service_resolution resolution); + + void ApplySettings(obs_data_t *videoSettings, + obs_data_t *audioSettings); }; diff --git a/plugins/obs-services/service-instance-info.cpp b/plugins/obs-services/service-instance-info.cpp index b033c5015ae69a..6b3e9abee9cfb1 100644 --- a/plugins/obs-services/service-instance-info.cpp +++ b/plugins/obs-services/service-instance-info.cpp @@ -112,3 +112,11 @@ obs_properties_t *ServiceInstance::InfoGetProperties2(void * /* data */, ->GetProperties(); return nullptr; } + +void ServiceInstance::InfoApplySettings(void *data, obs_data_t *videoSettings, + obs_data_t *audioSettings) +{ + ServiceConfig *priv = reinterpret_cast(data); + if (priv) + priv->ApplySettings(videoSettings, audioSettings); +} diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp index aa5f6a2f8fbe82..138b98739af0c7 100644 --- a/plugins/obs-services/service-instance.cpp +++ b/plugins/obs-services/service-instance.cpp @@ -117,6 +117,8 @@ ServiceInstance::ServiceInstance(const OBSServices::Service &service_) info.supported_protocols = supportedProtocols; + info.apply_encoder_settings = InfoApplySettings; + obs_register_service(&info); } diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp index a74735b2df6951..c76798246f5b7f 100644 --- a/plugins/obs-services/service-instance.hpp +++ b/plugins/obs-services/service-instance.hpp @@ -48,6 +48,9 @@ class ServiceInstance { static void InfoGetDefault2(void *type_data, obs_data_t *settings); static obs_properties_t *InfoGetProperties2(void *data, void *typeData); + static void InfoApplySettings(void *data, obs_data_t *videoSettings, + obs_data_t *audioSettings); + public: ServiceInstance(const OBSServices::Service &service); inline ~ServiceInstance(){}; From d5f5674f92d7af983e8081bfa55f242bab981781 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Mon, 12 Jun 2023 14:17:22 +0200 Subject: [PATCH 53/65] custom-services,obs-services Apply WHIP fixes --- plugins/custom-services/custom-service.c | 13 +++++++++++-- plugins/custom-services/custom-whip.c | 16 ++++++++++++++++ plugins/obs-services/service-config.cpp | 10 +++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/plugins/custom-services/custom-service.c b/plugins/custom-services/custom-service.c index 9fc102f401ac26..bbb55c20b9b87d 100644 --- a/plugins/custom-services/custom-service.c +++ b/plugins/custom-services/custom-service.c @@ -164,13 +164,22 @@ static void custom_service_apply_settings(void *data, { struct custom_service *service = data; if ((strcmp(service->protocol, "SRT") != 0) && - (strcmp(service->protocol, "RIST") != 0)) { + (strcmp(service->protocol, "RIST") != 0) && + (strcmp(service->protocol, "WHIP") != 0)) { return; } - if (video_settings != NULL) + if (video_settings != NULL) { obs_data_set_bool(video_settings, "repeat_headers", true); + if (strcmp(service->protocol, "WHIP") == 0) { + obs_data_set_int(video_settings, "bf", 0); + obs_data_set_string(video_settings, "rate_control", + "CBR"); + return; + } + } + if (audio_settings != NULL) obs_data_set_bool(audio_settings, "set_to_ADTS", true); } diff --git a/plugins/custom-services/custom-whip.c b/plugins/custom-services/custom-whip.c index c34fbe073f6214..8f6a56925bef4e 100644 --- a/plugins/custom-services/custom-whip.c +++ b/plugins/custom-services/custom-whip.c @@ -93,6 +93,21 @@ bool whip_service_can_try_to_connect(void *data) return true; } +static void whip_service_apply_settings(void *data, obs_data_t *video_settings, + obs_data_t *audio_settings) +{ + UNUSED_PARAMETER(data); + UNUSED_PARAMETER(audio_settings); + + if (video_settings == NULL) + return; + + obs_data_set_bool(video_settings, "repeat_headers", true); + + obs_data_set_int(video_settings, "bf", 0); + obs_data_set_string(video_settings, "rate_control", "CBR"); +} + static obs_properties_t *whip_service_properties(void *data) { UNUSED_PARAMETER(data); @@ -125,4 +140,5 @@ const struct obs_service_info custom_whip = { .get_connect_info = whip_service_connect_info, .can_try_to_connect = whip_service_can_try_to_connect, .get_audio_track_cap = whip_service_audio_track_cap, + .apply_encoder_settings = whip_service_apply_settings, }; diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index 91206dd4f3a4e3..f6ca1a209c3001 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -153,10 +153,18 @@ void ServiceConfig::ApplySettings(obs_data_t *videoSettings, obs_data_set_bool(audioSettings, "set_to_ADTS", true); break; } + case OBSServices::ServerProtocol::WHIP: + if (videoSettings != NULL) { + obs_data_set_bool(videoSettings, "repeat_headers", + true); + obs_data_set_int(videoSettings, "bf", 0); + obs_data_set_string(videoSettings, "rate_control", + "CBR"); + } + break; case OBSServices::ServerProtocol::RTMP: case OBSServices::ServerProtocol::RTMPS: case OBSServices::ServerProtocol::HLS: - case OBSServices::ServerProtocol::WHIP: break; } } From 52cf1ecf821bea5f3042a62b3ea9fd30200ebeeb Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Jul 2023 18:21:08 +0200 Subject: [PATCH 54/65] libobs,UI,docs: Add apply encoder settings 2 Allow to apply encoder settings per encoder and codec. --- UI/window-basic-auto-config-test.cpp | 18 ++++++++++-------- UI/window-basic-auto-config.cpp | 2 +- UI/window-basic-main-outputs.cpp | 23 +++++++++++++++-------- UI/window-basic-settings.cpp | 27 +++++++++++++++++++++++++-- docs/sphinx/reference-services.rst | 26 ++++++++++++++++++++++++++ libobs/obs-service.c | 14 ++++++++++++++ libobs/obs-service.h | 4 ++++ libobs/obs.h | 6 +++++- 8 files changed, 100 insertions(+), 20 deletions(-) diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index d0926b449f8c4b..cccc19ff938f7c 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -211,12 +211,6 @@ void AutoConfigTestPage::TestBandwidthThread() * * TODO: Create API of proc handler to restore this feature */ - /* -----------------------------------*/ - /* apply service settings */ - - obs_service_apply_encoder_settings(wiz->service, vencoder_settings, - aencoder_settings); - /* -----------------------------------*/ /* create output */ @@ -264,6 +258,14 @@ void AutoConfigTestPage::TestBandwidthThread() 0, nullptr); } + /* -----------------------------------*/ + /* apply service settings */ + + obs_service_apply_encoder_settings2( + wiz->service, obs_encoder_get_id(vencoder), vencoder_settings); + obs_service_apply_encoder_settings2( + wiz->service, obs_encoder_get_id(aencoder), aencoder_settings); + /* -----------------------------------*/ /* connect encoders/services/outputs */ @@ -1000,8 +1002,8 @@ void AutoConfigTestPage::FinalizeResults() obs_data_set_int(vencoder_settings, "bitrate", wiz->idealBitrate); - obs_service_apply_encoder_settings(wiz->service, - vencoder_settings, nullptr); + obs_service_apply_encoder_settings2(wiz->service, "obs_x264", + vencoder_settings); BPtr res_list; size_t res_count = 0; diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 273134148daae7..d136e4103bb6b7 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -299,7 +299,7 @@ bool AutoConfigStreamPage::validatePage() OBSDataAutoRelease settings = obs_data_create(); obs_data_set_int(settings, "bitrate", bitrate); - obs_service_apply_encoder_settings(tempService, settings, nullptr); + obs_service_apply_encoder_settings2(tempService, "obs_x264", settings); wiz->service = obs_service_get_ref(tempService); diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 73cc91811d2f2d..dcf85836558002 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -818,8 +818,12 @@ void SimpleOutput::Update() obs_data_set_string(audioSettings, "rate_control", "CBR"); obs_data_set_int(audioSettings, "bitrate", audioBitrate); - obs_service_apply_encoder_settings(main->GetService(), videoSettings, - audioSettings); + obs_service_apply_encoder_settings2(main->GetService(), + obs_encoder_get_id(videoStreaming), + videoSettings); + obs_service_apply_encoder_settings2(main->GetService(), + obs_encoder_get_id(audioStreaming), + audioSettings); if (!enforceBitrate) { obs_data_set_int(videoSettings, "bitrate", videoBitrate); @@ -1703,8 +1707,9 @@ void AdvancedOutput::UpdateStreamSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings, "bitrate"); int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + obs_service_apply_encoder_settings2( + main->GetService(), obs_encoder_get_id(videoStreaming), + settings); if (!enforceBitrate) obs_data_set_int(settings, "bitrate", bitrate); @@ -1769,8 +1774,9 @@ inline void AdvancedOutput::SetupStreaming() const char *id = obs_service_get_id(main->GetService()); if (strcmp(id, "rtmp_custom") == 0) { OBSDataAutoRelease settings = obs_data_create(); - obs_service_apply_encoder_settings(main->GetService(), settings, - nullptr); + obs_service_apply_encoder_settings2( + main->GetService(), obs_encoder_get_id(videoStreaming), + settings); obs_encoder_update(videoStreaming, settings); } } @@ -1983,8 +1989,9 @@ inline void AdvancedOutput::UpdateAudioSettings() if (applyServiceSettings) { int bitrate = (int)obs_data_get_int(settings[i], "bitrate"); - obs_service_apply_encoder_settings( - main->GetService(), nullptr, + obs_service_apply_encoder_settings2( + main->GetService(), + obs_encoder_get_id(streamAudioEnc), settings[i]); if (!enforceBitrate) diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index f0285883eeb306..56232ee0a9f9ae 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -5808,8 +5808,31 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() obs_data_set_int(videoSettings, "bitrate", oldVBitrate); obs_data_set_int(audioSettings, "bitrate", oldABitrate); - obs_service_apply_encoder_settings(tempService, videoSettings, - audioSettings); + QString streamEnc = + ui->simpleOutStrEncoder->currentData().toString(); + + obs_service_apply_encoder_settings2( + tempService, + get_simple_output_encoder(QT_TO_UTF8(streamEnc)), + videoSettings); + + const char *aencoder = EncoderAvailable("ffmpeg_opus") + ? "ffmpeg_opus" + : nullptr; + if (ui->simpleOutStrAEncoder->currentData().toString() == + "aac") { + aencoder = nullptr; + if (EncoderAvailable("CoreAudio_AAC")) { + aencoder = "CoreAudio_AAC"; + } else if (EncoderAvailable("libfdk_aac")) { + aencoder = "libfdk_aac"; + } else if (EncoderAvailable("ffmpeg_aac")) { + aencoder = "ffmpeg_aac"; + } + } + + obs_service_apply_encoder_settings2(tempService, aencoder, + audioSettings); int newVBitrate = obs_data_get_int(videoSettings, "bitrate"); int newABitrate = obs_data_get_int(audioSettings, "bitrate"); diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index 27fba584eb9a63..86204d2de7260d 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -123,6 +123,8 @@ Service Definition Structure :param video_encoder_settings: The audio encoder settings to change :param audio_encoder_settings: The video encoder settings to change + .. deprecated::Next-29.1.x + .. member:: void *obs_service_info.type_data void (*obs_service_info.free_type_data)(void *type_data) @@ -266,6 +268,19 @@ Service Definition Structure :return: Maximum bitrate for a given codec +.. member:: void (*obs_service_info.apply_encoder_settings2)(void *data, const char *encoder_id, obs_data_t *encoder_settings) + + This function is called to apply custom encoder settings specific to + this service. For example, if a service requires a specific keyframe + interval, or has a bitrate limit, the settings for the video and + audio encoders can be optionally modified if the front-end optionally + calls :c:func:`obs_service_apply_encoder_settings2()`. + + (Optional) + + :param encoder_id: The id of the encoder settings to change + :param encoder_settings: The encoder settings to change + General Service Functions ------------------------- @@ -426,6 +441,8 @@ General Service Functions :param video_encoder_settings: Video encoder settings. Can be *NULL* :param audio_encoder_settings: Audio encoder settings. Can be *NULL* + .. deprecated::Next-29.1.x + --------------------- .. function:: const char **obs_service_get_supported_video_codecs(const obs_service_t *service) @@ -535,6 +552,15 @@ General Service Functions :return: Maximum video bitrate for a given codec and resolution +--------------------- + +.. function:: void obs_service_apply_encoder_settings2(obs_service_t *service, const char *encoder_id, obs_data_t *encoder_settings) + + Applies service-specific encoder settings. + + :param encoder_id: Encoder id. + :param encoder_settings: Encoder settings. + .. --------------------------------------------------------------------------- .. _libobs/obs-service.h: https://github.com/obsproject/obs-studio/blob/master/libobs/obs-service.h diff --git a/libobs/obs-service.c b/libobs/obs-service.c index 113ffc597f211e..d49552d9b40bd9 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -655,3 +655,17 @@ int obs_service_get_max_video_bitrate(const obs_service_t *service, return service->info.get_max_video_bitrate(service->context.data, codec, resolution); } + +void obs_service_apply_encoder_settings2(obs_service_t *service, + const char *encoder_id, + obs_data_t *encoder_settings) +{ + if (!obs_service_valid(service, "obs_service_apply_encoder_settings2")) + return; + if (!service->info.apply_encoder_settings2) + return; + + if (encoder_id && encoder_settings) + service->info.apply_encoder_settings2( + service->context.data, encoder_id, encoder_settings); +} diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 210ef0a36759dc..14a04f07091268 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -95,6 +95,7 @@ struct obs_service_info { bool (*deprecated_1)(); + /* deprecated */ void (*apply_encoder_settings)(void *data, obs_data_t *video_encoder_settings, obs_data_t *audio_encoder_settings); @@ -147,6 +148,9 @@ struct obs_service_info { struct obs_service_resolution resolution); int (*get_max_codec_bitrate)(void *data, const char *codec); + + void (*apply_encoder_settings2)(void *data, const char *encoder_id, + obs_data_t *encoder_settings); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, diff --git a/libobs/obs.h b/libobs/obs.h index 96cb389936eea3..75d6bcd69f471a 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -2631,7 +2631,7 @@ obs_service_get_password(const obs_service_t *service); * @param video_encoder_settings Video encoder settings. Optional. * @param audio_encoder_settings Audio encoder settings. Optional. */ -EXPORT void +OBS_DEPRECATED EXPORT void obs_service_apply_encoder_settings(obs_service_t *service, obs_data_t *video_encoder_settings, obs_data_t *audio_encoder_settings); @@ -2697,6 +2697,10 @@ obs_service_get_max_video_bitrate(const obs_service_t *service, const char *codec, struct obs_service_resolution resolution); +EXPORT void obs_service_apply_encoder_settings2(obs_service_t *service, + const char *encoder_id, + obs_data_t *encoder_settings); + /* ------------------------------------------------------------------------- */ /* Source frame allocation functions */ EXPORT void obs_source_frame_init(struct obs_source_frame *frame, From 03adb6d23bb208b1d9b1fb1c92178500d618485b Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Jul 2023 18:23:55 +0200 Subject: [PATCH 55/65] custom-services,obs-services: Migrate apply encoder settings --- plugins/custom-services/custom-rist.c | 21 +++++++----- plugins/custom-services/custom-service.c | 30 +++++++++------- plugins/custom-services/custom-srt.c | 21 +++++++----- plugins/custom-services/custom-whip.c | 23 +++++++------ plugins/obs-services/service-config.cpp | 34 +++++++++++-------- plugins/obs-services/service-config.hpp | 3 +- .../obs-services/service-instance-info.cpp | 6 ++-- plugins/obs-services/service-instance.cpp | 2 +- plugins/obs-services/service-instance.hpp | 4 +-- 9 files changed, 82 insertions(+), 62 deletions(-) diff --git a/plugins/custom-services/custom-rist.c b/plugins/custom-services/custom-rist.c index 1e1123816cfdf8..5f9f003d73a87c 100644 --- a/plugins/custom-services/custom-rist.c +++ b/plugins/custom-services/custom-rist.c @@ -106,16 +106,21 @@ bool rist_service_can_try_to_connect(void *data) return true; } -static void rist_service_apply_settings(void *data, obs_data_t *video_settings, - obs_data_t *audio_settings) +static void rist_service_apply_settings2(void *data, const char *encoder_id, + obs_data_t *encoder_settings) { UNUSED_PARAMETER(data); - if (video_settings != NULL) - obs_data_set_bool(video_settings, "repeat_headers", true); - - if (audio_settings != NULL) - obs_data_set_bool(audio_settings, "set_to_ADTS", true); + switch (obs_get_encoder_type(encoder_id)) { + case OBS_ENCODER_VIDEO: + obs_data_set_bool(encoder_settings, "repeat_headers", true); + break; + case OBS_ENCODER_AUDIO: + if (strcmp(encoder_id, "libfdk_aac") == 0) + obs_data_set_bool(encoder_settings, "set_to_ADTS", + true); + break; + } } static obs_properties_t *rist_service_properties(void *data) @@ -159,5 +164,5 @@ const struct obs_service_info custom_rist = { .get_connect_info = rist_service_connect_info, .can_try_to_connect = rist_service_can_try_to_connect, .get_audio_track_cap = rist_service_audio_track_cap, - .apply_encoder_settings = rist_service_apply_settings, + .apply_encoder_settings2 = rist_service_apply_settings2, }; diff --git a/plugins/custom-services/custom-service.c b/plugins/custom-services/custom-service.c index bbb55c20b9b87d..380a502c0603f4 100644 --- a/plugins/custom-services/custom-service.c +++ b/plugins/custom-services/custom-service.c @@ -158,9 +158,8 @@ bool custom_service_can_try_to_connect(void *data) return true; } -static void custom_service_apply_settings(void *data, - obs_data_t *video_settings, - obs_data_t *audio_settings) +static void custom_service_apply_settings2(void *data, const char *encoder_id, + obs_data_t *encoder_settings) { struct custom_service *service = data; if ((strcmp(service->protocol, "SRT") != 0) && @@ -169,19 +168,24 @@ static void custom_service_apply_settings(void *data, return; } - if (video_settings != NULL) { - obs_data_set_bool(video_settings, "repeat_headers", true); - + switch (obs_get_encoder_type(encoder_id)) { + case OBS_ENCODER_VIDEO: + obs_data_set_bool(encoder_settings, "repeat_headers", true); if (strcmp(service->protocol, "WHIP") == 0) { - obs_data_set_int(video_settings, "bf", 0); - obs_data_set_string(video_settings, "rate_control", + obs_data_set_int(encoder_settings, "bf", 0); + obs_data_set_string(encoder_settings, "rate_control", "CBR"); - return; } + break; + case OBS_ENCODER_AUDIO: + if (strcmp(service->protocol, "WHIP") == 0) + break; + + if (strcmp(encoder_id, "libfdk_aac") == 0) + obs_data_set_bool(encoder_settings, "set_to_ADTS", + true); + break; } - - if (audio_settings != NULL) - obs_data_set_bool(audio_settings, "set_to_ADTS", true); } bool update_protocol_cb(obs_properties_t *props, obs_property_t *prop, @@ -363,5 +367,5 @@ const struct obs_service_info custom_service = { .get_connect_info = custom_service_connect_info, .can_try_to_connect = custom_service_can_try_to_connect, .get_audio_track_cap = custom_service_audio_track_cap, - .apply_encoder_settings = custom_service_apply_settings, + .apply_encoder_settings2 = custom_service_apply_settings2, }; diff --git a/plugins/custom-services/custom-srt.c b/plugins/custom-services/custom-srt.c index 22460ddbe69635..dd75a511f94c31 100644 --- a/plugins/custom-services/custom-srt.c +++ b/plugins/custom-services/custom-srt.c @@ -100,16 +100,21 @@ bool srt_service_can_try_to_connect(void *data) return true; } -static void srt_service_apply_settings(void *data, obs_data_t *video_settings, - obs_data_t *audio_settings) +static void srt_service_apply_settings2(void *data, const char *encoder_id, + obs_data_t *encoder_settings) { UNUSED_PARAMETER(data); - if (video_settings != NULL) - obs_data_set_bool(video_settings, "repeat_headers", true); - - if (audio_settings != NULL) - obs_data_set_bool(audio_settings, "set_to_ADTS", true); + switch (obs_get_encoder_type(encoder_id)) { + case OBS_ENCODER_VIDEO: + obs_data_set_bool(encoder_settings, "repeat_headers", true); + break; + case OBS_ENCODER_AUDIO: + if (strcmp(encoder_id, "libfdk_aac") == 0) + obs_data_set_bool(encoder_settings, "set_to_ADTS", + true); + break; + } } static obs_properties_t *srt_service_properties(void *data) @@ -148,5 +153,5 @@ const struct obs_service_info custom_srt = { .get_connect_info = srt_service_connect_info, .can_try_to_connect = srt_service_can_try_to_connect, .get_audio_track_cap = srt_service_audio_track_cap, - .apply_encoder_settings = srt_service_apply_settings, + .apply_encoder_settings2 = srt_service_apply_settings2, }; diff --git a/plugins/custom-services/custom-whip.c b/plugins/custom-services/custom-whip.c index 8f6a56925bef4e..e1226c4bd91fa8 100644 --- a/plugins/custom-services/custom-whip.c +++ b/plugins/custom-services/custom-whip.c @@ -93,19 +93,20 @@ bool whip_service_can_try_to_connect(void *data) return true; } -static void whip_service_apply_settings(void *data, obs_data_t *video_settings, - obs_data_t *audio_settings) +static void whip_service_apply_settings2(void *data, const char *encoder_id, + obs_data_t *encoder_settings) { UNUSED_PARAMETER(data); - UNUSED_PARAMETER(audio_settings); - if (video_settings == NULL) - return; - - obs_data_set_bool(video_settings, "repeat_headers", true); - - obs_data_set_int(video_settings, "bf", 0); - obs_data_set_string(video_settings, "rate_control", "CBR"); + switch (obs_get_encoder_type(encoder_id)) { + case OBS_ENCODER_VIDEO: + obs_data_set_bool(encoder_settings, "repeat_headers", true); + obs_data_set_int(encoder_settings, "bf", 0); + obs_data_set_string(encoder_settings, "rate_control", "CBR"); + break; + case OBS_ENCODER_AUDIO: + break; + } } static obs_properties_t *whip_service_properties(void *data) @@ -140,5 +141,5 @@ const struct obs_service_info custom_whip = { .get_connect_info = whip_service_connect_info, .can_try_to_connect = whip_service_can_try_to_connect, .get_audio_track_cap = whip_service_audio_track_cap, - .apply_encoder_settings = whip_service_apply_settings, + .apply_encoder_settings2 = whip_service_apply_settings2, }; diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index f6ca1a209c3001..81705cf3090a39 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -139,28 +139,34 @@ int ServiceConfig::GetMaxVideoBitrate(const char *codec, return service->GetMaxVideoBitrate(codec, resolution); } -void ServiceConfig::ApplySettings(obs_data_t *videoSettings, - obs_data_t *audioSettings) +void ServiceConfig::ApplySettings2(const char *encoderId, + obs_data_t *encoderSettings) { + enum obs_encoder_type type = obs_get_encoder_type(encoderId); + switch (StdStringToServerProtocol(protocol)) { case OBSServices::ServerProtocol::RIST: case OBSServices::ServerProtocol::SRT: { - if (videoSettings != NULL) - obs_data_set_bool(videoSettings, "repeat_headers", + switch (type) { + case OBS_ENCODER_VIDEO: + obs_data_set_bool(encoderSettings, "repeat_headers", true); - - if (audioSettings != NULL) - obs_data_set_bool(audioSettings, "set_to_ADTS", true); + break; + case OBS_ENCODER_AUDIO: + if (strcmp(encoderId, "libfdk_aac") == 0) + obs_data_set_bool(encoderSettings, + "set_to_ADTS", true); + break; + } break; } case OBSServices::ServerProtocol::WHIP: - if (videoSettings != NULL) { - obs_data_set_bool(videoSettings, "repeat_headers", - true); - obs_data_set_int(videoSettings, "bf", 0); - obs_data_set_string(videoSettings, "rate_control", - "CBR"); - } + if (type != OBS_ENCODER_VIDEO) + break; + + obs_data_set_bool(encoderSettings, "repeat_headers", true); + obs_data_set_int(encoderSettings, "bf", 0); + obs_data_set_string(encoderSettings, "rate_control", "CBR"); break; case OBSServices::ServerProtocol::RTMP: case OBSServices::ServerProtocol::RTMPS: diff --git a/plugins/obs-services/service-config.hpp b/plugins/obs-services/service-config.hpp index cfe6d03ad8344e..bfbdcd9cb295e2 100644 --- a/plugins/obs-services/service-config.hpp +++ b/plugins/obs-services/service-config.hpp @@ -57,6 +57,5 @@ class ServiceConfig { int GetMaxVideoBitrate(const char *codec, struct obs_service_resolution resolution); - void ApplySettings(obs_data_t *videoSettings, - obs_data_t *audioSettings); + void ApplySettings2(const char *encoderId, obs_data_t *encoderSettings); }; diff --git a/plugins/obs-services/service-instance-info.cpp b/plugins/obs-services/service-instance-info.cpp index 6b3e9abee9cfb1..518fe47923cd7d 100644 --- a/plugins/obs-services/service-instance-info.cpp +++ b/plugins/obs-services/service-instance-info.cpp @@ -113,10 +113,10 @@ obs_properties_t *ServiceInstance::InfoGetProperties2(void * /* data */, return nullptr; } -void ServiceInstance::InfoApplySettings(void *data, obs_data_t *videoSettings, - obs_data_t *audioSettings) +void ServiceInstance::InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings) { ServiceConfig *priv = reinterpret_cast(data); if (priv) - priv->ApplySettings(videoSettings, audioSettings); + priv->ApplySettings2(encoderId, encoderSettings); } diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp index 138b98739af0c7..efc7ef4a035582 100644 --- a/plugins/obs-services/service-instance.cpp +++ b/plugins/obs-services/service-instance.cpp @@ -117,7 +117,7 @@ ServiceInstance::ServiceInstance(const OBSServices::Service &service_) info.supported_protocols = supportedProtocols; - info.apply_encoder_settings = InfoApplySettings; + info.apply_encoder_settings2 = InfoApplySettings2; obs_register_service(&info); } diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp index c76798246f5b7f..75039712951c4f 100644 --- a/plugins/obs-services/service-instance.hpp +++ b/plugins/obs-services/service-instance.hpp @@ -48,8 +48,8 @@ class ServiceInstance { static void InfoGetDefault2(void *type_data, obs_data_t *settings); static obs_properties_t *InfoGetProperties2(void *data, void *typeData); - static void InfoApplySettings(void *data, obs_data_t *videoSettings, - obs_data_t *audioSettings); + static void InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings); public: ServiceInstance(const OBSServices::Service &service); From bee433464258b8edd04090ac505836bfabf1838c Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Jul 2023 19:15:07 +0200 Subject: [PATCH 56/65] obs-services,json-schema: Apply recommendation and maximums --- build-aux/json-schema/codecDefs.json | 37 ++ build-aux/json-schema/service.json | 13 + .../obs-services/generated/services-json.hpp | 80 ++++ plugins/obs-services/json/services.json | 385 ++++++++++++++++++ plugins/obs-services/service-config.cpp | 2 + plugins/obs-services/service-instance.cpp | 61 +++ plugins/obs-services/service-instance.hpp | 3 + plugins/obs-services/services-json-util.hpp | 8 + 8 files changed, 589 insertions(+) diff --git a/build-aux/json-schema/codecDefs.json b/build-aux/json-schema/codecDefs.json index 2ae63aa6aef618..894b1de68d1a2e 100644 --- a/build-aux/json-schema/codecDefs.json +++ b/build-aux/json-schema/codecDefs.json @@ -12,6 +12,43 @@ "audioCodecEnum": { "$comment": "Enumeration of audio codecs", "enum": ["aac","opus"] + }, + "codecProperties": { + "$comment": "Per-codec properties meant for obs-services schema", + "type": "object", + "properties": { + "h264": + { + "type": "object", + "title": "H264 Settings", + "description": "Properties related to the H264 codec", + "properties": { + "profile": { + "type": "string", + "title": "H264 Profile", + "enum": [ "baseline", "main", "high" ] + }, + "keyint": { + "type": "integer", + "title": "Keyframe Interval (seconds)", + "minimum": 0 + }, + "bframes": + { + "type": "integer", + "title": "B-Frames", + "minimum": 0 + } + }, + "minProperties": 1 + }, + "obs_x264": { + "type": "string", + "title": "x264 Encoder Options", + "description": "Options meant for the x264 encoder implementation with the id 'obs_x264'", + "minLength": 1 + } + } } } } diff --git a/build-aux/json-schema/service.json b/build-aux/json-schema/service.json index 23652be09c053e..d05d564d3bad59 100644 --- a/build-aux/json-schema/service.json +++ b/build-aux/json-schema/service.json @@ -196,6 +196,19 @@ }, "minProperties": 1, "unevaluatedProperties": false + }, + "recommended": { + "type": "object", + "title": "Recommended settings", + "description": "Settings that are applied only if the user wants to do so.", + "allOf": [ + { + "$comment": "Add codec properties", + "$ref": "codecDefs.json#/$defs/codecProperties" + } + ], + "minProperties": 1, + "unevaluatedProperties": true } }, "allOf": [ diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp index 004f4d7fe13f6e..43b3c7e2f716c5 100644 --- a/plugins/obs-services/generated/services-json.hpp +++ b/plugins/obs-services/generated/services-json.hpp @@ -103,6 +103,31 @@ namespace OBSServices { std::optional>> videoBitrateMatrix; }; + enum class H264Profile : int { BASELINE, HIGH, MAIN }; + + /** + * Properties related to the H264 codec + */ + struct H264Settings { + std::optional bframes; + std::optional keyint; + std::optional profile; + }; + + /** + * Settings that are applied only if the user wants to do so. + */ + struct RecommendedSettings { + /** + * Properties related to the H264 codec + */ + std::optional h264; + /** + * Options meant for the x264 encoder implementation with the id 'obs_x264' + */ + std::optional obsX264; + }; + /** * Properties related to the RIST protocol */ @@ -185,6 +210,10 @@ namespace OBSServices { * next to the services dropdown. */ std::optional moreInfoLink; + /** + * Settings that are applied only if the user wants to do so. + */ + std::optional recommended; /** * Array of server objects */ @@ -222,6 +251,12 @@ namespace OBSServices { void from_json(const json & j, Maximums & x); void to_json(json & j, const Maximums & x); + void from_json(const json & j, H264Settings & x); + void to_json(json & j, const H264Settings & x); + + void from_json(const json & j, RecommendedSettings & x); + void to_json(json & j, const RecommendedSettings & x); + void from_json(const json & j, RistProperties & x); void to_json(json & j, const RistProperties & x); @@ -240,6 +275,9 @@ namespace OBSServices { void from_json(const json & j, ServicesJson & x); void to_json(json & j, const ServicesJson & x); + void from_json(const json & j, H264Profile & x); + void to_json(json & j, const H264Profile & x); + void from_json(const json & j, ServerProtocol & x); void to_json(json & j, const ServerProtocol & x); @@ -264,6 +302,30 @@ namespace OBSServices { j["video_bitrate_matrix"] = x.videoBitrateMatrix; } + inline void from_json(const json & j, H264Settings& x) { + x.bframes = get_stack_optional(j, "bframes"); + x.keyint = get_stack_optional(j, "keyint"); + x.profile = get_stack_optional(j, "profile"); + } + + inline void to_json(json & j, const H264Settings & x) { + j = json::object(); + j["bframes"] = x.bframes; + j["keyint"] = x.keyint; + j["profile"] = x.profile; + } + + inline void from_json(const json & j, RecommendedSettings& x) { + x.h264 = get_stack_optional(j, "h264"); + x.obsX264 = get_stack_optional(j, "obs_x264"); + } + + inline void to_json(json & j, const RecommendedSettings & x) { + j = json::object(); + j["h264"] = x.h264; + j["obs_x264"] = x.obsX264; + } + inline void from_json(const json & j, RistProperties& x) { x.encryptPassphrase = j.at("encrypt_passphrase").get(); x.srpUsernamePassword = j.at("srp_username_password").get(); @@ -316,6 +378,7 @@ namespace OBSServices { x.name = j.at("name").get(); x.maximums = get_stack_optional(j, "maximums"); x.moreInfoLink = get_stack_optional(j, "more_info_link"); + x.recommended = get_stack_optional(j, "recommended"); x.servers = j.at("servers").get>(); x.streamKeyLink = get_stack_optional(j, "stream_key_link"); x.supportedCodecs = get_stack_optional(j, "supported_codecs"); @@ -331,6 +394,7 @@ namespace OBSServices { j["name"] = x.name; j["maximums"] = x.maximums; j["more_info_link"] = x.moreInfoLink; + j["recommended"] = x.recommended; j["servers"] = x.servers; j["stream_key_link"] = x.streamKeyLink; j["supported_codecs"] = x.supportedCodecs; @@ -350,6 +414,22 @@ namespace OBSServices { j["services"] = x.services; } + inline void from_json(const json & j, H264Profile & x) { + if (j == "baseline") x = H264Profile::BASELINE; + else if (j == "high") x = H264Profile::HIGH; + else if (j == "main") x = H264Profile::MAIN; + else { throw std::runtime_error("Input JSON does not conform to schema!"); } + } + + inline void to_json(json & j, const H264Profile & x) { + switch (x) { + case H264Profile::BASELINE: j = "baseline"; break; + case H264Profile::HIGH: j = "high"; break; + case H264Profile::MAIN: j = "main"; break; + default: throw std::runtime_error("This should not happen"); + } + } + inline void from_json(const json & j, ServerProtocol & x) { if (j == "HLS") x = ServerProtocol::HLS; else if (j == "RIST") x = ServerProtocol::RIST; diff --git a/plugins/obs-services/json/services.json b/plugins/obs-services/json/services.json index 04a4022d692137..0b4c833db9a907 100644 --- a/plugins/obs-services/json/services.json +++ b/plugins/obs-services/json/services.json @@ -46,6 +46,13 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -77,6 +84,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -109,6 +122,12 @@ "audio_bitrate": { "aac": 256 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -140,6 +159,12 @@ "audio_bitrate": { "aac": 320 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -166,6 +191,12 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -264,6 +295,11 @@ "audio_bitrate": { "aac": 320 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -325,6 +361,11 @@ "audio_bitrate": { "aac": 320 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -386,6 +427,12 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -504,6 +551,11 @@ "h264" ] } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -596,6 +648,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -622,6 +680,12 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "profile": "baseline", + "keyint": 1 + } } }, { @@ -648,6 +712,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -696,6 +766,12 @@ "video_bitrate": { "h264": 3500 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -740,6 +816,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -766,6 +847,13 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -819,6 +907,9 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "obs_x264": "tune=zerolatency" } }, { @@ -920,6 +1011,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -970,6 +1066,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1054,6 +1155,11 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "keyint": 3 + } } }, { @@ -1080,6 +1186,12 @@ "h264" ] } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + } } }, { @@ -1106,6 +1218,12 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -1132,6 +1250,12 @@ "audio_bitrate": { "aac": 96 } + }, + "recommended": { + "h264": { + "profile": "baseline", + "keyint": 1 + } } }, { @@ -1163,6 +1287,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1194,6 +1323,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -1220,6 +1355,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + } } }, { @@ -1247,6 +1388,12 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -1279,6 +1426,12 @@ "audio_bitrate": { "aac": 320 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -1349,6 +1502,12 @@ "audio_bitrate": { "aac": 320 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -1401,6 +1560,12 @@ "audio_bitrate": { "aac": 256 } + }, + "recommended": { + "h264": { + "keyint": 4 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -1427,6 +1592,12 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "profile": "baseline", + "keyint": 2 + } } }, { @@ -1453,6 +1624,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1509,6 +1685,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1550,6 +1731,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -1576,6 +1763,11 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1607,6 +1799,13 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -1633,6 +1832,12 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + } } }, { @@ -1651,6 +1856,12 @@ "h264" ] } + }, + "recommended": { + "h264": { + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -1669,6 +1880,11 @@ "h264" ] } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1695,6 +1911,13 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -1721,6 +1944,13 @@ "audio_bitrate": { "aac": 96 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -1762,6 +1992,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1815,6 +2050,9 @@ "h264" ] } + }, + "recommended": { + "obs_x264": "scenecut=0" } }, { @@ -1876,6 +2114,12 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + } } }, { @@ -1902,6 +2146,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1936,6 +2185,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -1962,6 +2216,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2024,6 +2283,13 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 1 + }, + "obs_x264": "tune=zerolatency scenecut=0" } }, { @@ -2077,6 +2343,11 @@ "h264" ] } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2104,6 +2375,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2200,6 +2476,13 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "baseline", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency b-pyramid=0 scenecut=0" } }, { @@ -2270,6 +2553,11 @@ "h264": 20000 }, "framerate": 60 + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2338,6 +2626,11 @@ "audio_bitrate": { "aac": 256 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2388,6 +2681,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2414,6 +2712,11 @@ "audio_bitrate": { "aac": 128 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2441,6 +2744,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2473,6 +2781,11 @@ "h264": 4000 } } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2537,6 +2850,13 @@ "audio_bitrate": { "aac": 196 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + }, + "obs_x264": "scenecut=0" } }, { @@ -2563,6 +2883,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2591,6 +2916,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2629,6 +2959,11 @@ "audio_bitrate": { "aac": 160 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2757,6 +3092,11 @@ "video_bitrate": { "h264": 16000 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2785,6 +3125,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 1 + } } }, { @@ -2813,6 +3159,11 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2867,6 +3218,11 @@ "video_bitrate": { "h264": 20000 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2887,6 +3243,11 @@ "h264" ] } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2912,6 +3273,11 @@ "video_bitrate": { "h264": 6000 } + }, + "recommended": { + "h264": { + "keyint": 2 + } } }, { @@ -2939,6 +3305,12 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 2 + } } }, { @@ -2967,6 +3339,13 @@ "audio_bitrate": { "aac": 192 } + }, + "recommended": { + "h264": { + "profile": "main", + "keyint": 2 + }, + "obs_x264": "tune=zerolatency" } }, { @@ -3005,6 +3384,12 @@ "audio_bitrate": { "aac": 512 } + }, + "recommended": { + "h264": { + "profile": "high", + "keyint": 1 + } } } ] diff --git a/plugins/obs-services/service-config.cpp b/plugins/obs-services/service-config.cpp index 81705cf3090a39..b7ce1435a1cbb5 100644 --- a/plugins/obs-services/service-config.cpp +++ b/plugins/obs-services/service-config.cpp @@ -173,4 +173,6 @@ void ServiceConfig::ApplySettings2(const char *encoderId, case OBSServices::ServerProtocol::HLS: break; } + + service->ApplySettings2(type, encoderId, encoderSettings); } diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp index efc7ef4a035582..fa7f118228fb61 100644 --- a/plugins/obs-services/service-instance.cpp +++ b/plugins/obs-services/service-instance.cpp @@ -196,6 +196,67 @@ ServiceInstance::GetSupportedAudioCodecs(const std::string &protocol) const return strlist_split(codecs.c_str(), ';', false); } +void ServiceInstance::ApplySettings2(obs_encoder_type encoderType, + const char *encoderId_, + obs_data_t *encoderSettings) const +{ + std::string codec = obs_get_encoder_codec(encoderId_); + + if (service.maximums.has_value()) { + int maxbitrate = 0; + + struct obs_video_info ovi; + if (encoderType == OBS_ENCODER_VIDEO && + supportedResolutionsWithFps && + service.maximums->videoBitrateMatrix.has_value() && + obs_get_video_info(&ovi)) { + int cur_fps = ovi.fps_num / ovi.fps_den; + obs_service_resolution res{(int)ovi.output_width, + (int)ovi.output_height, + cur_fps}; + + maxbitrate = GetMaxVideoBitrate(codec.c_str(), res); + } else { + maxbitrate = GetMaxCodecBitrate(codec.c_str()); + } + + if (maxbitrate != 0 && + obs_data_get_int(encoderSettings, "bitrate") < maxbitrate) + obs_data_set_int(encoderSettings, "bitrate", + maxbitrate); + } + + if (!service.recommended.has_value()) + return; + + if (codec == "h264" && service.recommended->h264.has_value()) { + if (service.recommended->h264->profile.has_value()) { + std::string profile = H264ProfileToStdString( + service.recommended->h264->profile.value()); + obs_data_set_string(encoderSettings, "profile", + profile.c_str()); + } + + if (service.recommended->h264->keyint.has_value()) + obs_data_set_int( + encoderSettings, "keyint_sec", + service.recommended->h264->keyint.value()); + + if (service.recommended->h264->bframes.has_value()) + obs_data_set_int( + encoderSettings, "bf", + service.recommended->h264->bframes.value()); + } + + std::string encoderId(encoderId_); + if (encoderId == "obs_x264" && + service.recommended->obsX264.has_value()) { + obs_data_set_string( + encoderSettings, "x264opts", + service.recommended->obsX264.value().c_str()); + } +} + const char *ServiceInstance::GetName() { return service.name.c_str(); diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp index 75039712951c4f..cd9f7a204a3b02 100644 --- a/plugins/obs-services/service-instance.hpp +++ b/plugins/obs-services/service-instance.hpp @@ -72,6 +72,9 @@ class ServiceInstance { char **GetSupportedVideoCodecs(const std::string &protocol) const; char **GetSupportedAudioCodecs(const std::string &protocol) const; + void ApplySettings2(obs_encoder_type encoderType, const char *encoderId, + obs_data_t *encoderSettings) const; + const char *GetName(); void GetDefaults(obs_data_t *settings); diff --git a/plugins/obs-services/services-json-util.hpp b/plugins/obs-services/services-json-util.hpp index f66cabe82063e9..63011622ce00da 100644 --- a/plugins/obs-services/services-json-util.hpp +++ b/plugins/obs-services/services-json-util.hpp @@ -38,3 +38,11 @@ static inline std::string SupportedAudioCodecToStdString( OBSServices::to_json(j, codec); return j.get(); } + +static inline std::string +H264ProfileToStdString(const OBSServices::H264Profile &profile) +{ + nlohmann::json j; + OBSServices::to_json(j, profile); + return j.get(); +} From 296e7deaa30a866046872f6f3ab8b4355c8e644d Mon Sep 17 00:00:00 2001 From: tytan652 Date: Fri, 7 Jul 2023 19:22:54 +0200 Subject: [PATCH 57/65] obs-services,json-schema: Add Github login to services --- build-aux/json-schema/obs-services.json | 13 +- .../obs-services/generated/services-json.hpp | 7 + plugins/obs-services/json/services.json | 250 ++++++++++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) diff --git a/build-aux/json-schema/obs-services.json b/build-aux/json-schema/obs-services.json index ccff171c445360..8701e11d6a520b 100644 --- a/build-aux/json-schema/obs-services.json +++ b/build-aux/json-schema/obs-services.json @@ -33,12 +33,23 @@ "title": "Property reserved to OBS Project developers", "description": "Whether or not the service is shown in the list before it is expanded to all services by the user.", "default": false + }, + "github_logins": { + "type": "array", + "title": "Github logins", + "description": "Login to ping on Github when the services check is failing.\n 'obsproject' is used as a placeholder.", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true, + "additionalItems": false } }, "$commit": "Forbid 'common' being set if false", "if": { "properties": { "common": { "const": false } } }, "then": { "properties": { "common": { "description": "This property is unneeded if set to false" , "const": true } } }, - "required": ["id","name"] + "required": ["id","name", "github_logins"] }, { "$comment": "Add base service JSON schema", diff --git a/plugins/obs-services/generated/services-json.hpp b/plugins/obs-services/generated/services-json.hpp index 43b3c7e2f716c5..923dc2095da42b 100644 --- a/plugins/obs-services/generated/services-json.hpp +++ b/plugins/obs-services/generated/services-json.hpp @@ -192,6 +192,11 @@ namespace OBSServices { * the user. */ std::optional common; + /** + * Login to ping on Github when the services check is failing. + * 'obsproject' is used as a placeholder. + */ + std::vector githubLogins; /** * Human readable identifier used to register the service in OBS * Making it human readable is meant to allow users to use it through scripts and plugins @@ -374,6 +379,7 @@ namespace OBSServices { inline void from_json(const json & j, Service& x) { x.common = get_stack_optional(j, "common"); + x.githubLogins = j.at("github_logins").get>(); x.id = j.at("id").get(); x.name = j.at("name").get(); x.maximums = get_stack_optional(j, "maximums"); @@ -390,6 +396,7 @@ namespace OBSServices { inline void to_json(json & j, const Service & x) { j = json::object(); j["common"] = x.common; + j["github_logins"] = x.githubLogins; j["id"] = x.id; j["name"] = x.name; j["maximums"] = x.maximums; diff --git a/plugins/obs-services/json/services.json b/plugins/obs-services/json/services.json index 0b4c833db9a907..1e768da07316a8 100644 --- a/plugins/obs-services/json/services.json +++ b/plugins/obs-services/json/services.json @@ -5,6 +5,9 @@ { "id": "loola", "name": "Loola.tv", + "github_logins": [ + "atyachin" + ], "servers": [ { "name": "US East: Virginia", @@ -58,6 +61,9 @@ { "id": "lovecast", "name": "Lovecast", + "github_logins": [ + "LovecastNeil" + ], "servers": [ { "name": "Default", @@ -96,6 +102,9 @@ "id": "luzento", "name": "Luzento.com - RTMP", "stream_key_link": "https://cms.luzento.com/dashboard/stream-key?from=OBS", + "github_logins": [ + "joeflateau" + ], "servers": [ { "name": "Primary", @@ -133,6 +142,9 @@ { "id": "vimm", "name": "VIMM", + "github_logins": [ + "chirenonhive" + ], "servers": [ { "name": "Europe: Frankfurt", @@ -170,6 +182,9 @@ { "id": "web_tv", "name": "Web.TV", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Primary", @@ -202,6 +217,9 @@ { "id": "goodgame", "name": "GoodGame.ru", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Моscow", @@ -221,6 +239,9 @@ "id": "youstreamer", "name": "YouStreamer", "stream_key_link": "https://www.app.youstreamer.com/stream/", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Moscow", @@ -239,6 +260,10 @@ { "id": "vaughn_live", "name": "Vaughn Live / iNSTAGIB", + "github_logins": [ + "notmark", + "invalidtask" + ], "servers": [ { "name": "US: Chicago, IL", @@ -305,6 +330,10 @@ { "id": "breakers_tv", "name": "Breakers.TV", + "github_logins": [ + "notmark", + "invalidtask" + ], "servers": [ { "name": "US: Chicago, IL", @@ -373,6 +402,9 @@ "name": "Facebook Live", "common": true, "stream_key_link": "https://www.facebook.com/live/producer?ref=OBS", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -438,6 +470,9 @@ { "id": "castr", "name": "Castr.io", + "github_logins": [ + "wahajdar" + ], "servers": [ { "name": "US-East (Chicago, IL)", @@ -561,6 +596,9 @@ { "id": "boomstream", "name": "Boomstream", + "github_logins": [ + "gkozlenko" + ], "servers": [ { "name": "Default", @@ -579,6 +617,9 @@ { "id": "meridix", "name": "Meridix Live Sports Platform", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Primary", @@ -602,6 +643,10 @@ { "id": "afreecatv", "name": "AfreecaTV", + "github_logins": [ + "karenkim-AfreecaTV", + "rpslack" + ], "servers": [ { "name": "Asia : Korea", @@ -659,6 +704,9 @@ { "id": "cam4", "name": "CAM4", + "github_logins": [ + "andrein" + ], "servers": [ { "name": "CAM4", @@ -691,6 +739,9 @@ { "id": "eplay", "name": "ePlay", + "github_logins": [ + "thomaspicquet" + ], "servers": [ { "name": "ePlay Primary", @@ -723,6 +774,9 @@ { "id": "picarto", "name": "Picarto", + "github_logins": [ + "Thulinma" + ], "servers": [ { "name": "Autoselect closest server", @@ -777,6 +831,9 @@ { "id": "livestream", "name": "Livestream", + "github_logins": [ + "neogenix" + ], "servers": [ { "name": "Primary", @@ -795,6 +852,9 @@ { "id": "uscreen", "name": "Uscreen", + "github_logins": [ + "matticusfinch" + ], "servers": [ { "name": "Default", @@ -826,6 +886,9 @@ { "id": "stripchat", "name": "Stripchat", + "github_logins": [ + "crazyheart-wisebits" + ], "servers": [ { "name": "Auto", @@ -859,6 +922,9 @@ { "id": "camsoda", "name": "CamSoda", + "github_logins": [ + "pardo-bsso" + ], "servers": [ { "name": "North America", @@ -915,6 +981,9 @@ { "id": "chartubate", "name": "Chaturbate", + "github_logins": [ + "chaturbateorg" + ], "servers": [ { "name": "Global Main Fastest - Recommended", @@ -1023,6 +1092,9 @@ "name": "WpStream", "more_info_link": "https://wpstream.net/obs-more-info", "stream_key_link": "https://wpstream.net/obs-get-stream-key", + "github_logins": [ + "wpstream" + ], "servers": [ { "name": "Closest server - Automatic", @@ -1078,6 +1150,9 @@ "name": "Twitter", "common": true, "stream_key_link": "https://studio.twitter.com/producer/sources", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "US West: California", @@ -1165,6 +1240,9 @@ { "id": "switchboard_live", "name": "Switchboard Live", + "github_logins": [ + "one0ten" + ], "servers": [ { "name": "Global - Recommended", @@ -1197,6 +1275,9 @@ { "id": "looch", "name": "Looch", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Primary Looch ingest server", @@ -1229,6 +1310,9 @@ { "id": "eventials", "name": "Eventials", + "github_logins": [ + "alexandrevicenzi" + ], "servers": [ { "name": "Default", @@ -1261,6 +1345,9 @@ { "id": "eventlive", "name": "EventLive.pro", + "github_logins": [ + "mkrn" + ], "servers": [ { "name": "Default", @@ -1297,6 +1384,10 @@ { "id": "lahzenegar", "name": "Lahzenegar - StreamG | لحظه‌نگار - استریمجی", + "github_logins": [ + "FarzadKarkhani", + "mhabedinpour" + ], "servers": [ { "name": "Primary", @@ -1334,6 +1425,9 @@ { "id": "mylive", "name": "MyLive", + "github_logins": [ + "Alcaros" + ], "servers": [ { "name": "Default", @@ -1367,6 +1461,9 @@ "id": "trovo", "name": "Trovo", "stream_key_link": "https://studio.trovo.live/mychannel/stream", + "github_logins": [ + "tlivegaming" + ], "servers": [ { "name": "Default", @@ -1399,6 +1496,9 @@ { "id": "mixcloud", "name": "Mixcloud", + "github_logins": [ + "matclayton" + ], "servers": [ { "name": "Default", @@ -1437,6 +1537,9 @@ { "id": "sermonaudio", "name": "SermonAudio Cloud", + "github_logins": [ + "astudios123" + ], "servers": [ { "name": "Primary", @@ -1463,6 +1566,9 @@ { "id": "vimeo", "name": "Vimeo", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -1481,6 +1587,9 @@ { "id": "aparat", "name": "Aparat", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -1513,6 +1622,9 @@ { "id": "kakaotv", "name": "KakaoTV", + "github_logins": [ + "eastkiki" + ], "servers": [ { "name": "Default", @@ -1539,6 +1651,9 @@ { "id": "piczel_tv", "name": "Piczel.tv", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -1571,6 +1686,9 @@ { "id": "stage_ten", "name": "STAGE TEN", + "github_logins": [ + "tmatth" + ], "servers": [ { "name": "STAGE TEN", @@ -1603,6 +1721,9 @@ { "id": "dlive", "name": "DLive", + "github_logins": [ + "fsh905" + ], "servers": [ { "name": "Default", @@ -1634,6 +1755,9 @@ { "id": "lightcast_com", "name": "Lightcast.com", + "github_logins": [ + "lylyahiko" + ], "servers": [ { "name": "North America / East", @@ -1695,6 +1819,9 @@ { "id": "bongacams", "name": "Bongacams", + "github_logins": [ + "bongalive" + ], "servers": [ { "name": "Automatic / Default", @@ -1742,6 +1869,9 @@ { "id": "chathostess", "name": "Chathostess", + "github_logins": [ + "wolf247" + ], "servers": [ { "name": "Chathostess - Backup", @@ -1773,6 +1903,9 @@ { "id": "onlyfans", "name": "OnlyFans.com", + "github_logins": [ + "lorskun" + ], "servers": [ { "name": "USA", @@ -1811,6 +1944,9 @@ { "id": "steam", "name": "Steam", + "github_logins": [ + "Linnun" + ], "servers": [ { "name": "Default", @@ -1843,6 +1979,9 @@ { "id": "konduit", "name": "Konduit.live", + "github_logins": [ + "Kraz3D" + ], "servers": [ { "name": "Default", @@ -1867,6 +2006,9 @@ { "id": "loco", "name": "LOCO", + "github_logins": [ + "praveenkumarKajla" + ], "servers": [ { "name": "Default", @@ -1890,6 +2032,9 @@ { "id": "niconico-premium", "name": "niconico, premium member (ニコニコ生放送 プレミアム会員)", + "github_logins": [ + "koizuka" + ], "servers": [ { "name": "Default", @@ -1923,6 +2068,9 @@ { "id": "niconico-free", "name": "niconico, free member (ニコニコ生放送 一般会員)", + "github_logins": [ + "koizuka" + ], "servers": [ { "name": "Default", @@ -1956,6 +2104,9 @@ { "id": "wasd_tv", "name": "WASD.TV", + "github_logins": [ + "keylase" + ], "servers": [ { "name": "Automatic", @@ -2002,6 +2153,9 @@ { "id": "xlovecam", "name": "XLoveCam.com", + "github_logins": [ + "nickosmoi" + ], "servers": [ { "name": "Europe(main)", @@ -2058,6 +2212,9 @@ { "id": "angelthump", "name": "AngelThump", + "github_logins": [ + "jbpratt" + ], "servers": [ { "name": "Auto", @@ -2125,6 +2282,9 @@ { "id": "api_video", "name": "api.video", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -2156,6 +2316,9 @@ { "id": "mux", "name": "Mux", + "github_logins": [ + "eropple" + ], "servers": [ { "name": "Global (RTMPS)", @@ -2195,6 +2358,9 @@ { "id": "viloud", "name": "Viloud", + "github_logins": [ + "sangatxo" + ], "servers": [ { "name": "Default", @@ -2226,6 +2392,9 @@ { "id": "myfreecams", "name": "MyFreeCams", + "github_logins": [ + "SCG82" + ], "servers": [ { "name": "Automatic", @@ -2295,6 +2464,9 @@ { "id": "polystreamer", "name": "PolyStreamer.com", + "github_logins": [ + "jcward" + ], "servers": [ { "name": "Auto-select closest server", @@ -2354,6 +2526,9 @@ "id": "openrec_tv", "name": "OPENREC.tv - Premium member (プレミアム会員)", "stream_key_link": "https://www.openrec.tv/login?keep_login=true&url=https://www.openrec.tv/dashboard/live?from=obs", + "github_logins": [ + "poccariswet" + ], "servers": [ { "name": "Default", @@ -2387,6 +2562,9 @@ "name": "nanoStream Cloud / bintu", "more_info_link": "https://www.nanocosmos.de/obs", "stream_key_link": "https://bintu-cloud-frontend.nanocosmos.de/organisation", + "github_logins": [ + "nanocosmos-ol" + ], "servers": [ { "name": "bintu-stream global ingest (rtmp)", @@ -2490,6 +2668,9 @@ "name": "Bilibili Live - RTMP | 哔哩哔哩直播 - RTMP", "more_info_link": "https://link.bilibili.com/p/help/index?id=4#/tools-tutorial", "stream_key_link": "https://link.bilibili.com/p/center/index#/my-room/start-live", + "github_logins": [ + "TianQiBuTian" + ], "servers": [ { "name": "Global - Primary | 全球 - 主要", @@ -2524,6 +2705,9 @@ "id": "volume_com", "name": "Volume.com", "stream_key_link": "https://volume.com/b?show_key=1&webrtc=0", + "github_logins": [ + "sergey-mm" + ], "servers": [ { "name": "Default - Recommended", @@ -2564,6 +2748,9 @@ "id": "boxcast", "name": "BoxCast", "stream_key_link": "https://dashboard.boxcast.com/#/sources", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "BoxCast", @@ -2582,6 +2769,9 @@ { "id": "disciple", "name": "Disciple Media", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -2600,6 +2790,9 @@ { "id": "jio_games", "name": "Jio Games", + "github_logins": [ + "vishakh-h" + ], "servers": [ { "name": "Primary", @@ -2637,6 +2830,9 @@ "id": "kuaishou", "name": "Kuaishou Live", "stream_key_link": "https://studio.kuaishou.com/live/list", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -2660,6 +2856,9 @@ { "id": "ultreon", "name": "Utreon", + "github_logins": [ + "DeepDiver1975" + ], "servers": [ { "name": "Default", @@ -2691,6 +2890,9 @@ { "id": "autistici_org", "name": "Autistici.org Live", + "github_logins": [ + "controesempio" + ], "servers": [ { "name": "Default", @@ -2723,6 +2925,9 @@ "id": "phonelivestreaming", "name": "PhoneLiveStreaming", "stream_key_link": "https://app.phonelivestreaming.com/media/rtmp", + "github_logins": [ + "davidplappert" + ], "servers": [ { "name": "PhoneLiveStreaming", @@ -2754,6 +2959,9 @@ { "id": "manyvids", "name": "ManyVids", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Default", @@ -2793,6 +3001,9 @@ "name": "Fantasy.Club", "more_info_link": "https://help.fantasy.club/", "stream_key_link": "https://fantasy.club/app/create-content/stream-now", + "github_logins": [ + "PalmerEk" + ], "servers": [ { "name": "US: East", @@ -2862,6 +3073,9 @@ { "id": "sympla", "name": "Sympla", + "github_logins": [ + "vfernandes-sympla" + ], "servers": [ { "name": "Sympla RTMP", @@ -2895,6 +3109,9 @@ "name": "Mildom", "more_info_link": "https://support.mildom.com/hc/ja/articles/360056569954", "stream_key_link": "https://www.mildom.com/creator/live", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Global", @@ -2928,6 +3145,9 @@ "name": "Nonolive", "more_info_link": "https://wia.nonolive.com/views/obs_assistant_tutorial.html", "stream_key_link": "https://www.nonolive.com/room_setting", + "github_logins": [ + "obsproject" + ], "servers": [ { "name": "Asia: Hong Kong, China", @@ -2970,6 +3190,9 @@ "id": "streamvi", "name": "StreamVi", "stream_key_link": "https://streamvi.ru/settings", + "github_logins": [ + "Be1erafon" + ], "servers": [ { "name": "Default", @@ -3004,6 +3227,9 @@ "id": "livepush", "name": "Livepush", "more_info_link": "https://docs.livepush.io/en/articles/5065323-how-to-stream-live-from-obs-to-livepush", + "github_logins": [ + "wahajdar" + ], "servers": [ { "name": "Livepush Global (Default)", @@ -3104,6 +3330,9 @@ "name": "Vindral", "more_info_link": "https://docs.vindral.com/docs/vindral-cdn/", "stream_key_link": "https://portal.cdn.vindral.com/channels", + "github_logins": [ + "mattiaslandin" + ], "servers": [ { "name": "Global", @@ -3138,6 +3367,9 @@ "name": "Whowatch (ふわっち)", "more_info_link": "https://whowatch.tv/help/encoder", "stream_key_link": "https://whowatch.tv/publish", + "github_logins": [ + "rch850" + ], "servers": [ { "name": "default", @@ -3170,6 +3402,9 @@ "id": "irltoolkit", "name": "IRLToolkit", "stream_key_link": "https://irl.run/settings/ingest/", + "github_logins": [ + "tt2468" + ], "servers": [ { "name": "Global (Recommended)", @@ -3230,6 +3465,9 @@ "name": "Bitmovin", "more_info_link": "https://developer.bitmovin.com/docs/overview", "stream_key_link": "https://bitmovin.com/dashboard/streams?streamsTab=LIVE", + "github_logins": [ + "CMThF" + ], "servers": [ { "name": "Streams Live", @@ -3255,6 +3493,9 @@ "name": "Live Streamer Cafe", "more_info_link": "https://livestreamercafe.com/help.php", "stream_key_link": "https://livestreamercafe.com/profile.php", + "github_logins": [ + "Tophicles" + ], "servers": [ { "name": "Live Streamer Cafe Server", @@ -3284,6 +3525,9 @@ "id": "enchant_events", "name": "Enchant.events", "more_info_link": "https://docs.enchant.events/knowledge-base-y4pOb", + "github_logins": [ + "micohasanen" + ], "servers": [ { "name": "Primary RTMPS", @@ -3318,6 +3562,9 @@ "name": "Joystick.TV", "more_info_link": "https://support.joystick.tv/support/creator-support/setting-up-your-stream/", "stream_key_link": "https://joystick.tv/stream-settings", + "github_logins": [ + "jwoertink" + ], "servers": [ { "name": "RTMP", @@ -3353,6 +3600,9 @@ "name": "Livepeer Studio", "more_info_link": "https://docs.livepeer.org/guides/developing/stream-via-obs", "stream_key_link": "https://livepeer.studio/dashboard/streams", + "github_logins": [ + "0xcadams" + ], "servers": [ { "name": "Global (RTMP)", From f4647cc67dd4b72120ccfeacd9411665144a2dd6 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 14:40:25 +0200 Subject: [PATCH 58/65] obs-youtube: Add apply encoder settings --- plugins/obs-youtube/youtube-service-info.cpp | 12 ++++++++++++ plugins/obs-youtube/youtube-service.cpp | 2 ++ plugins/obs-youtube/youtube-service.hpp | 3 +++ 3 files changed, 17 insertions(+) diff --git a/plugins/obs-youtube/youtube-service-info.cpp b/plugins/obs-youtube/youtube-service-info.cpp index 134f2119fd90a8..8bbc8a686a11eb 100644 --- a/plugins/obs-youtube/youtube-service-info.cpp +++ b/plugins/obs-youtube/youtube-service-info.cpp @@ -87,6 +87,18 @@ obs_properties_t *YouTubeService::InfoGetProperties(void *data) return nullptr; } +void YouTubeService::InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings) +{ + if (obs_get_encoder_type(encoderId) == OBS_ENCODER_VIDEO) + obs_data_set_int(encoderSettings, "keyint_sec", 2); + + int maxBitrate = + InfoGetMaxCodecBitrate(data, obs_get_encoder_codec(encoderId)); + if (maxBitrate) + obs_data_set_int(encoderSettings, "bitrate", maxBitrate); +} + #ifdef OAUTH_ENABLED bool YouTubeService::InfoCanBandwidthTest(void *data) { diff --git a/plugins/obs-youtube/youtube-service.cpp b/plugins/obs-youtube/youtube-service.cpp index 279aa8637a98b5..deabd36f1b43c0 100644 --- a/plugins/obs-youtube/youtube-service.cpp +++ b/plugins/obs-youtube/youtube-service.cpp @@ -40,6 +40,8 @@ YouTubeService::YouTubeService() info.get_max_codec_bitrate = InfoGetMaxCodecBitrate; + info.apply_encoder_settings2 = InfoApplySettings2; + #ifdef OAUTH_ENABLED info.can_bandwidth_test = InfoCanBandwidthTest; info.enable_bandwidth_test = InfoEnableBandwidthTest; diff --git a/plugins/obs-youtube/youtube-service.hpp b/plugins/obs-youtube/youtube-service.hpp index 4894a79390fc2f..9f504ebff64fbe 100644 --- a/plugins/obs-youtube/youtube-service.hpp +++ b/plugins/obs-youtube/youtube-service.hpp @@ -36,6 +36,9 @@ class YouTubeService { static obs_properties_t *InfoGetProperties(void *data); + static void InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings); + #ifdef OAUTH_ENABLED OBSDataAutoRelease data = nullptr; From c7c3174fad70bb672c2be394725093bf2128a2b9 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 14:40:41 +0200 Subject: [PATCH 59/65] obs-twitch: Add apply encoder settings --- plugins/obs-twitch/twitch-service-info.cpp | 15 +++++++++++++++ plugins/obs-twitch/twitch-service.cpp | 2 ++ plugins/obs-twitch/twitch-service.hpp | 3 +++ 3 files changed, 20 insertions(+) diff --git a/plugins/obs-twitch/twitch-service-info.cpp b/plugins/obs-twitch/twitch-service-info.cpp index 5e7675a5769169..d7927fdba95ab0 100644 --- a/plugins/obs-twitch/twitch-service-info.cpp +++ b/plugins/obs-twitch/twitch-service-info.cpp @@ -109,3 +109,18 @@ bool TwitchService::InfoBandwidthTestEnabled(void *data) ->BandwidthTestEnabled(); return false; } + +void TwitchService::InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings) +{ + if (obs_get_encoder_type(encoderId) == OBS_ENCODER_VIDEO) + obs_data_set_int(encoderSettings, "keyint_sec", 2); + + if (strcmp(encoderId, "obs_x264")) + obs_data_set_string(encoderSettings, "x264opts", "scenecut=0"); + + int maxBitrate = + InfoGetMaxCodecBitrate(data, obs_get_encoder_codec(encoderId)); + if (maxBitrate) + obs_data_set_int(encoderSettings, "bitrate", maxBitrate); +} diff --git a/plugins/obs-twitch/twitch-service.cpp b/plugins/obs-twitch/twitch-service.cpp index f7f7a9319cc3a3..eaceaf575dcc89 100644 --- a/plugins/obs-twitch/twitch-service.cpp +++ b/plugins/obs-twitch/twitch-service.cpp @@ -46,6 +46,8 @@ TwitchService::TwitchService() info.get_max_codec_bitrate = InfoGetMaxCodecBitrate; + info.apply_encoder_settings2 = InfoApplySettings2; + obs_register_service(&info); #ifdef OAUTH_ENABLED diff --git a/plugins/obs-twitch/twitch-service.hpp b/plugins/obs-twitch/twitch-service.hpp index de0ed0d913afca..1c9168cb1ba54b 100644 --- a/plugins/obs-twitch/twitch-service.hpp +++ b/plugins/obs-twitch/twitch-service.hpp @@ -44,6 +44,9 @@ class TwitchService { static void InfoEnableBandwidthTest(void *data, bool enabled); static bool InfoBandwidthTestEnabled(void *data); + static void InfoApplySettings2(void *data, const char *encoderId, + obs_data_t *encoderSettings); + #ifdef OAUTH_ENABLED OBSDataAutoRelease data = nullptr; From 3461d686b1462053afbbe4ec2b102626dccbd258 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 14:46:06 +0200 Subject: [PATCH 60/65] UI: Avoid re-creating the service if it was not changed --- UI/window-basic-settings-stream.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index 206071c3de6d77..a20acf94ae86d0 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -94,17 +94,25 @@ void OBSBasicSettings::LoadStream1Settings() void OBSBasicSettings::SaveStream1Settings() { OBSDataAutoRelease settings = obs_service_get_settings(tempService); - const char *settingsJson = obs_data_get_json(settings); - settings = obs_data_create_from_json(settingsJson); + obs_service_t *service = main->GetService(); - OBSServiceAutoRelease newService = - obs_service_create(obs_service_get_id(tempService), - "default_service", settings, nullptr); + if (ui->service->property("changed").toBool()) { + /* Create a unique obs_data_t for the new service */ + const char *settingsJson = obs_data_get_json(settings); + settings = obs_data_create_from_json(settingsJson); - if (!newService) - return; + OBSServiceAutoRelease newService = obs_service_create( + obs_service_get_id(tempService), "default_service", + settings, nullptr); - main->SetService(newService); + if (!newService) + return; + + main->SetService(newService); + + } else { + obs_service_update(service, settings); + } main->SaveService(); SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); From b0940e3a60f94e5322c53e46e5284269b08c2216 Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 14:50:52 +0200 Subject: [PATCH 61/65] file-updater: Enable use in C++ --- deps/file-updater/file-updater/file-updater.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deps/file-updater/file-updater/file-updater.h b/deps/file-updater/file-updater/file-updater.h index e50b1267195643..2542f70c7a0743 100644 --- a/deps/file-updater/file-updater/file-updater.h +++ b/deps/file-updater/file-updater/file-updater.h @@ -2,6 +2,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + struct update_info; typedef struct update_info update_info_t; @@ -25,3 +29,7 @@ update_info_t *update_info_create_single( const char *log_prefix, const char *user_agent, const char *file_url, confirm_file_callback_t confirm_callback, void *param); void update_info_destroy(update_info_t *info); + +#ifdef __cplusplus +} +#endif From f926d3bf0a30f4d09dff3adca8efde182e3525dd Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 15:50:02 +0200 Subject: [PATCH 62/65] obs-services,json-schema: Add services update --- build-aux/json-schema/package-schema.json | 47 +++++++++++++++++++++++ plugins/obs-services/CMakeLists.txt | 19 ++++++++- plugins/obs-services/json/package.json | 11 ++++++ plugins/obs-services/services-manager.cpp | 40 +++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 build-aux/json-schema/package-schema.json create mode 100644 plugins/obs-services/json/package.json diff --git a/build-aux/json-schema/package-schema.json b/build-aux/json-schema/package-schema.json new file mode 100644 index 00000000000000..58db6698684fca --- /dev/null +++ b/build-aux/json-schema/package-schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "url": { + "$ref": "#/definitions/saneUrl", + "description": "Points to the base URL of hosted package.json and services.json files, used to automatically fetch the latest version." + }, + "version": { + "type": "integer" + }, + "files": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Filename to read,, containing service definitions." + }, + "version": { + "type": "integer", + "description": "This value should be bumped any time the file defined by the 'name' field is changed. This value will be used when determining whether a new version is available." + } + }, + "required": [ + "name", + "version" + ] + }, + "description": "List of files to read, each containing a list of services.", + "additionalProperties": false + } + }, + "required": [ + "url", + "version", + "files" + ], + "definitions": { + "saneUrl": { + "type": "string", + "format": "uri", + "pattern": "^https?://" + } + } +} diff --git a/plugins/obs-services/CMakeLists.txt b/plugins/obs-services/CMakeLists.txt index bdbf02197922c6..1665e892aa8745 100644 --- a/plugins/obs-services/CMakeLists.txt +++ b/plugins/obs-services/CMakeLists.txt @@ -24,13 +24,27 @@ target_sources( services-manager.cpp services-manager.hpp) -target_link_libraries(obs-services PRIVATE OBS::libobs nlohmann_json::nlohmann_json) +target_link_libraries(obs-services PRIVATE OBS::libobs nlohmann_json::nlohmann_json + $<$:OBS::file-updater>) file(STRINGS json/services.json _format_version REGEX "^.*\"format_version\":[ \t]+[0-9]+,$") string(REGEX REPLACE "^.*\"format_version\":[ \t]+([0-9]+),$" "\\1" JSON_FORMAT_VER "${_format_version}") target_compile_definitions(obs-services PRIVATE JSON_FORMAT_VER=${JSON_FORMAT_VER}) +if(ENABLE_SERVICES_UPDATE) + file(STRINGS json/package.json _update_url REGEX "^.*\"url\":[ \t]+\"https?:\/\/.*\/v[0-9]+\",$") + string(REGEX REPLACE "^.*\"url\":[ \t]+\"(https?:\/\/.*\/v[0-9]+)\",$" "\\1" UPDATE_URL "${_update_url}") + + target_compile_definitions(obs-services PRIVATE UPDATE_URL="${UPDATE_URL}") + + string(REGEX REPLACE "^.*\"url\":[ \t]+\"https?:\/\/.*\/v([0-9]+)\",$" "\\1" _pkg_format_version "${_update_url}") + if(NOT _pkg_format_version STREQUAL "${JSON_FORMAT_VER}") + message(FATAL_ERROR "package.json URL version does not match services.json format version") + endif() + unset(_pkg_format_version) +endif() + if(OS_WINDOWS) configure_file(cmake/windows/obs-module.rc.in obs-services.rc) target_sources(obs-services PRIVATE obs-services.rc) @@ -39,9 +53,12 @@ endif() if(OBS_CMAKE_VERSION VERSION_GREATER_EQUAL 3.0.0) target_add_resource(obs-services "${CMAKE_CURRENT_SOURCE_DIR}/json/services.json" "${OBS_DATA_DESTINATION}/obs-plugins/obs-services") + target_add_resource(obs-services "${CMAKE_CURRENT_SOURCE_DIR}/json/package.json" + "${OBS_DATA_DESTINATION}/obs-plugins/obs-services") set_target_properties_obs(obs-services PROPERTIES FOLDER plugins PREFIX "") else() add_target_resource(obs-services json/services.json "obs-plugins/obs-services/") + add_target_resource(obs-services json/package.json "obs-plugins/obs-services/") set_target_properties(obs-services PROPERTIES FOLDER "plugins" PREFIX "") setup_plugin_target(obs-services) endif() diff --git a/plugins/obs-services/json/package.json b/plugins/obs-services/json/package.json new file mode 100644 index 00000000000000..15fd352e5d49ab --- /dev/null +++ b/plugins/obs-services/json/package.json @@ -0,0 +1,11 @@ +{ + "$schema": "schema/package-schema.json", + "url": "https://obsproject.com/obs2_update/obs-services/v1", + "version": 1, + "files": [ + { + "name": "services.json", + "version": 1 + } + ] +} diff --git a/plugins/obs-services/services-manager.cpp b/plugins/obs-services/services-manager.cpp index 77be2e33948a03..3c0ef5cb7770e7 100644 --- a/plugins/obs-services/services-manager.cpp +++ b/plugins/obs-services/services-manager.cpp @@ -7,6 +7,12 @@ #include #include +#ifdef UPDATE_URL +#include + +constexpr const char *OBS_SERVICE_UPDATE_URL = (const char *)UPDATE_URL; +#endif + constexpr int OBS_SERVICES_FORMAT_VER = JSON_FORMAT_VER; std::shared_ptr manager = nullptr; @@ -90,9 +96,43 @@ bool ServicesManager::RegisterServices() return true; } +#ifdef UPDATE_URL +static bool ConfirmServicesFile(void *, file_download_data *file) +{ + std::string name(file->name); + if (name != "services.json") + return false; + + OBSServices::ServicesJson json; + try { + json = nlohmann::json::parse((char *)file->buffer.array); + } catch (nlohmann::json::exception &e) { + blog(LOG_DEBUG, "[obs-services][ConfirmServicesFile] %s", + e.what()); + return false; + } + + return json.formatVersion == OBS_SERVICES_FORMAT_VER; +} +#endif + bool ServicesManager::Initialize() { if (!manager) { +#ifdef UPDATE_URL + BPtr localDir = obs_module_file(""); + BPtr cacheDir = obs_module_config_path(""); + std::string userAgent = "obs-services (libobs "; + userAgent += obs_get_version_string(); + userAgent += ")"; + if (cacheDir) { + update_info_t *update = update_info_create( + "[obs-services][update] ", userAgent.c_str(), + OBS_SERVICE_UPDATE_URL, localDir, cacheDir, + ConfirmServicesFile, nullptr); + update_info_destroy(update); + } +#endif manager = std::make_shared(); return manager->RegisterServices(); } From c0a2cddd0b0b77235fcd99a57c32da3dd3a18b6a Mon Sep 17 00:00:00 2001 From: tytan652 Date: Sat, 8 Jul 2023 16:23:16 +0200 Subject: [PATCH 63/65] UI: Add service migration --- UI/data/services-conversion-matrix.json | 104 ++++++ UI/window-basic-main-service.cpp | 463 +++++++++++++++++++++++- 2 files changed, 555 insertions(+), 12 deletions(-) create mode 100644 UI/data/services-conversion-matrix.json diff --git a/UI/data/services-conversion-matrix.json b/UI/data/services-conversion-matrix.json new file mode 100644 index 00000000000000..64d0de788dcde1 --- /dev/null +++ b/UI/data/services-conversion-matrix.json @@ -0,0 +1,104 @@ +{ + "Twitch": "twitch", + "YouTube - HLS": "youtube", + "YouTube - RTMPS": "youtube", + "YouTube / YouTube Gaming": "youtube", + "YouTube - RTMP": "youtube", + "YouTube - RTMPS (Beta)": "youtube", + "Loola.tv": "loola", + "Lovecast": "lovecast", + "Luzento.com - RTMP": "luzento", + "VIMM": "vimm", + "Web.TV": "web_tv", + "GoodGame.ru": "goodgame", + "YouStreamer": "youstreamer", + "Vaughn Live / iNSTAGIB": "vaughn_live", + "Breakers.TV": "breakers_tv", + "Facebook Live": "facebook", + "Restream.io": "restream", + "Restream.io - RTMP": "restream", + "Restream.io - FTL": "restream", + "Castr.io": "castr", + "Boomstream": "boomstream", + "Meridix Live Sports Platform": "meridix", + "AfreecaTV": "afreecatv", + "아프리카TV": "afreecatv", + "Afreeca.TV": "afreecatv", + "CAM4": "cam4", + "ePlay": "eplay", + "Picarto": "picarto", + "Livestream": "livestream", + "Uscreen": "uscreen", + "Stripchat": "stripchat", + "CamSoda": "camsoda", + "Chaturbate": "chartubate", + "Twitter": "twitter", + "Twitter / Periscope": "twitter", + "Switchboard Live": "switchboard_live", + "Switchboard Live (Joicaster)": "switcherboard_live", + "Looch": "looch", + "Eventials": "eventials", + "EventLive.pro": "eventlive", + "Lahzenegar - StreamG | لحظه‌نگار - استریمجی": "lahzenegar", + "MyLive": "mylive", + "Trovo": "trovo", + "Madcat": "trovo", + "Mixcloud": "mixcloud", + "SermonAudio Cloud": "sermonaudio", + "SermonAudio.com": "sermonaudio", + "Vimeo": "vimeo", + "Aparat": "aparat", + "KakaoTV": "kakaotv", + "Piczel.tv": "piczel_tv", + "STAGE TEN": "stage_ten", + "DLive": "dlive", + "Lightcast.com": "lightcast_com", + "Bongacams": "bongacams", + "Chathostess": "chathostess", + "OnlyFans.com": "onlyfans", + "YouNow": "younow", + "Steam": "steam", + "Konduit.live": "konduit", + "LOCO": "loco", + "niconico, premium member (ニコニコ生放送 プレミアム会員)": "niconico-premium", + "niconico, free member (ニコニコ生放送 一般会員)": "niconico-free", + "WASD.TV": "wasd_tv", + "Nimo TV": "nimo_tv", + "XLoveCam.com": "xlovecam", + "AngelThump": "angelthump", + "api.video": "api_video", + "SHOWROOM": "showroom", + "Mux": "mux", + "Viloud": "viloud", + "MyFreeCams": "myfreecams", + "PolyStreamer.com": "polystreamer", + "OPENREC.tv - Premium member (プレミアム会員)": "openrec_tv", + "nanoStream Cloud / bintu": "nanostream", + "Dacast": "dacast", + "Bilibili Live - RTMP | 哔哩哔哩直播 - RTMP": "bilibili", + "Bilibili Live": "bilibili", + "Volume.com": "volume_com", + "BoxCast": "boxcast", + "Disciple Media": "disciple", + "Jio Games": "jio_games", + "Kuaishou Live": "kuaishou", + "Utreon": "ultreon", + "Autistici.org Live": "autistici_org", + "PhoneLiveStreaming": "phonelivestreaming", + "ManyVids": "manyvids", + "Fantasy.Club": "fantasy_club", + "WpStream": "wpstream", + "Sympla": "sympla", + "Mildom": "mildom", + "Nonolive": "nonolive", + "StreamVi": "streamvi", + "Livepush": "livepush", + "Vindral": "vindral", + "Whowatch (ふわっち)": "whowhatch", + "IRLToolkit": "irltoolkit", + "Bitmovin": "bitmovin", + "Live Streamer Cafe": "live_streamer_cafe", + "Enchant.events": "enchant_events", + "Joystick.TV": "joystick_tv", + "Livepeer Studio": "livepeer_studio" +} diff --git a/UI/window-basic-main-service.cpp b/UI/window-basic-main-service.cpp index bd6e75b47b7d3a..7c4f94b99bf394 100644 --- a/UI/window-basic-main-service.cpp +++ b/UI/window-basic-main-service.cpp @@ -19,7 +19,14 @@ #include -#define SERVICE_PATH "service.json" +#include +#include + +#include + +#include "platform.hpp" + +#define SERVICE_PATH "streamService.json" void OBSBasic::SaveService() { @@ -42,10 +49,394 @@ void OBSBasic::SaveService() blog(LOG_WARNING, "Failed to save service"); } -bool OBSBasic::LoadService() +static bool service_available(const char *service) +{ + const char *val; + size_t i = 0; + + while (obs_enum_service_types(i++, &val)) + if (strcmp(val, service) == 0) + return true; + + return false; +} + +static bool migrate_twitch_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-twitch.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-twitch plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + OBSDataAutoRelease settingsData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "Twitch", "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "Twitch", "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "Twitch", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "Twitch", "ScopeVer")); + + obs_data_set_string(settingsData, "feed_uuid", + config_get_string(basicConfig, "Twitch", "UUID")); + + obs_data_set_int(settings, "chat_addon", + config_get_int(basicConfig, "Twitch", "AddonChoice")); + obs_data_set_bool(settings, "enforce_bwtest", + obs_data_get_bool(settings, "bwtest")); + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + obs_data_set_obj(serviceData, "settings", settingsData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-twitch data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "Twitch", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_restream_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-restream.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-restream plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "Restream", + "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "Restream", + "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "Restream", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "Restream", "ScopeVer")); + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-restream data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "Restream", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_youtube_integration(config_t *basicConfig, + obs_data_t *settings) +{ + BPtr uuid = os_generate_uuid(); + char pluginConfigpath[512]; + + if (GetProfilePath(pluginConfigpath, sizeof(pluginConfigpath), + "obs-youtube.json") <= 0) { + blog(LOG_ERROR, + "Failed to create obs-youtube plugin config path"); + return false; + } + + OBSDataAutoRelease oauthData = obs_data_create(); + OBSDataAutoRelease settingsData = obs_data_create(); + + obs_data_set_string(oauthData, "access_token", + config_get_string(basicConfig, "YouTube", "Token")); + obs_data_set_int(oauthData, "expire_time", + config_get_uint(basicConfig, "YouTube", "ExpireTime")); + obs_data_set_string(oauthData, "refresh_token", + config_get_string(basicConfig, "YouTube", + "RefreshToken")); + obs_data_set_int(oauthData, "scope_version", + config_get_int(basicConfig, "YouTube", "ScopeVer")); + + bool rememberSettings = + config_get_bool(basicConfig, "YouTube", "RememberSettings"); + obs_data_set_bool(settingsData, "remember", rememberSettings); + + if (rememberSettings) { + obs_data_set_string(settingsData, "title", + config_get_string(basicConfig, "YouTube", + "Title")); + + obs_data_set_string(settingsData, "description", + config_get_string(basicConfig, "YouTube", + "Description")); + + std::string priv = + config_get_string(basicConfig, "YouTube", "Privacy"); + int privInt = 0 /* "private" */; + if (priv == "public") + privInt = 1; + else if (priv == "unlisted") + privInt = 2; + obs_data_set_int(settingsData, "privacy", privInt); + + obs_data_set_string(settingsData, "category_id", + config_get_string(basicConfig, "YouTube", + "CategoryID")); + + std::string latency = + config_get_string(basicConfig, "YouTube", "Latency"); + int latencyInt = 0 /* "normal" */; + if (latency == "low") + latencyInt = 1; + else if (latency == "ultraLow") + latencyInt = 2; + obs_data_set_int(settingsData, "latency", latencyInt); + + obs_data_set_bool(settingsData, "dvr", + config_get_bool(basicConfig, "YouTube", + "DVR")); + + obs_data_set_bool(settingsData, "made_for_kids", + config_get_bool(basicConfig, "YouTube", + "MadeForKids")); + + obs_data_set_bool(settingsData, "schedule_for_later", + config_get_bool(basicConfig, "YouTube", + "ScheduleForLater")); + + obs_data_set_bool(settingsData, "auto_start", + config_get_bool(basicConfig, "YouTube", + "AutoStart")); + + obs_data_set_bool(settingsData, "auto_stop", + config_get_bool(basicConfig, "YouTube", + "AutoStop")); + + std::string projection = + config_get_string(basicConfig, "YouTube", "Projection"); + obs_data_set_int(settingsData, "projection", + projection == "360" ? 1 : 0); + + obs_data_set_string(settingsData, "thumbnail_file", + config_get_string(basicConfig, "YouTube", + "ThumbnailFile")); + } + + OBSDataAutoRelease serviceData = obs_data_create(); + obs_data_set_obj(serviceData, "oauth", oauthData); + obs_data_set_obj(serviceData, "settings", settingsData); + + OBSDataAutoRelease pluginData = obs_data_create(); + obs_data_set_obj(pluginData, uuid, serviceData); + obs_data_set_string(settings, "uuid", uuid); + + if (!obs_data_save_json(pluginData, pluginConfigpath)) { + blog(LOG_ERROR, "Failed to save migrated obs-youtube data"); + return false; + } + + const char *dockStateStr = + config_get_string(basicConfig, "YouTube", "DockState"); + config_set_string(basicConfig, "BasicWindow", "DockState", + dockStateStr); + + return true; +} + +static bool migrate_rtmp_common(config_t *basicConfig, obs_data_t *data) +{ + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + std::string matrixFilePath; + if (!GetDataFilePath("services-conversion-matrix.json", + matrixFilePath)) { + blog(LOG_ERROR, "Failed to find service conversion matrix"); + return false; + } + + std::ifstream matrixFile(matrixFilePath); + if (!matrixFile.is_open()) { + blog(LOG_ERROR, "Failed to open service conversion matrix"); + return false; + } + + std::string error; + std::stringstream ss; + ss << matrixFile.rdbuf(); + json11::Json matrix = json11::Json::parse(ss.str(), error); + if (matrix.is_null()) { + blog(LOG_ERROR, "Failed to parse service conversion matrix"); + return false; + } + + std::string service = obs_data_get_string(settings, "service"); + std::string type = matrix[service].string_value(); + if (!service_available(type.c_str())) { + blog(LOG_ERROR, "Service '%s' not found, migration failed", + type.c_str()); + return false; + } + + blog(LOG_INFO, "Migrating 'rtmp_common' service to '%s'", type.c_str()); + obs_data_set_string(data, "type", type.c_str()); + + bool integration = false; + if (config_has_user_value(basicConfig, "Auth", "Type")) { + std::string authType = + config_get_string(basicConfig, "Auth", "Type"); + /* If authType is the same as service name, integration is enabled */ + integration = (service == authType); + } + + if (integration) { + /* Migrate integrations */ + if (type == "twitch") { + if (!migrate_twitch_integration(basicConfig, settings)) + return false; + } else if (type == "restream") { + if (!migrate_restream_integration(basicConfig, + settings)) + return false; + } else if (type == "youtube") { + if (!migrate_youtube_integration(basicConfig, settings)) + return false; + } + } else if (type == "twitch" || type == "restream" || + type == "youtube") { + /* Those service use "stream_key" */ + obs_data_set_string(settings, "stream_key", + obs_data_get_string(data, "key")); + obs_data_erase(settings, "key"); + } else if (type != "dacast" && type != "nimo_tv" && + type != "showroom" && type != "younow") { + /* Services from obs-services use "stream_id" + * other services than integration kept "key" */ + obs_data_set_string(settings, "stream_id", + obs_data_get_string(data, "key")); + obs_data_erase(settings, "key"); + } + obs_data_erase(settings, "bwtest"); + + if (service == "YouNow") { + obs_data_set_string(settings, "protocol", "FTL"); + } else if (service == "YouTube - HLS") { + obs_data_set_string(settings, "protocol", "HLS"); + } else if (type == "twitch" || type == "dacast" || type == "nimo_tv" || + type == "showroom") { + obs_data_set_string(settings, "protocol", "RTMP"); + } + + if (!obs_data_has_user_value(settings, "protocol")) { + std::string url = obs_data_get_string(settings, "server"); + std::string prefix = url.substr(0, url.find("://") + 3); + obs_data_set_string(settings, "protocol", + prefix == "rtmps://" ? "RTMPS" : "RTMP"); + } + + obs_data_set_obj(data, "settings", settings); + + return true; +} + +static bool migrate_rtmp_custom(obs_data_t *data) { - const char *type; + if (!service_available("custom_service")) { + blog(LOG_ERROR, + "Service 'custom_service' not found, migration failed"); + return false; + } + + blog(LOG_INFO, "Migrating 'rtmp_custom' service to 'custom_service'"); + + obs_data_set_string(data, "type", "custom_service"); + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + + obs_data_erase(settings, "use_auth"); + obs_data_erase(settings, "bwtest"); + + std::string url = obs_data_get_string(settings, "server"); + + std::string protocol; + std::string prefix = url.substr(0, url.find("://") + 3); + + if (prefix == "rtmp://") + protocol = "RTMP"; + else if (prefix == "rtmps://") + protocol = "RTMPS"; + else if (prefix == "ftl://") + protocol = "FTL"; + else if (prefix == "srt://") + protocol = "SRT"; + else if (prefix == "rist://") + protocol = "RIST"; + + obs_data_set_string(settings, "protocol", protocol.c_str()); + + /* Migrate encryption passphrase */ + std::string passphrase; + if (protocol == "SRT") { + passphrase = obs_data_get_string(settings, "password"); + obs_data_erase(settings, "username"); + obs_data_erase(settings, "password"); + } else if (protocol == "RIST") { + passphrase = obs_data_get_string(settings, "key"); + + obs_data_erase(settings, "key"); + } + + if (!passphrase.empty()) + obs_data_set_string(settings, "encrypt_passphrase", + passphrase.c_str()); + + /* Migrate stream ID/key */ + if (protocol != "RIST") { + std::string stream_id = obs_data_get_string(settings, "key"); + if (!stream_id.empty()) + obs_data_set_string(settings, "stream_id", + stream_id.c_str()); + obs_data_erase(settings, "key"); + } + + obs_data_set_obj(data, "settings", settings); + + return true; +} + +bool OBSBasic::LoadService() +{ char serviceJsonPath[512]; int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), SERVICE_PATH); @@ -55,24 +446,72 @@ bool OBSBasic::LoadService() OBSDataAutoRelease data = obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); - if (!data) - return false; + if (!data) { + char oldServiceJsonPath[512]; - obs_data_set_default_string(data, "type", "rtmp_common"); - type = obs_data_get_string(data, "type"); + if (GetProfilePath(oldServiceJsonPath, + sizeof(oldServiceJsonPath), + "service.json") <= 0) + return false; - OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); - OBSDataAutoRelease hotkey_data = obs_data_get_obj(data, "hotkeys"); + data = obs_data_create_from_json_file_safe(oldServiceJsonPath, + "bak"); + + if (!data) + return false; + + /* Migration if old rtmp-services and network settings */ + std::string type = obs_data_get_string(data, "type"); + /* rtmp_common was set as a default value so type can be empty */ + if (type == "rtmp_common" || type.empty()) { + if (!migrate_rtmp_common(basicConfig, data)) + return false; + + } else if (type == "rtmp_custom") { + if (!migrate_rtmp_custom(data)) + return false; + } - service = obs_service_create(type, "default_service", settings, - hotkey_data); + if (!obs_data_save_json(data, serviceJsonPath)) + blog(LOG_WARNING, "Failed to save migrated service"); + } + + std::string type = obs_data_get_string(data, "type"); + + if (!service_available(type.c_str())) { + blog(LOG_ERROR, + "Service '%s' not found, fallback to empty custom service", + type.c_str()); + return false; + } + + OBSDataAutoRelease settings = obs_data_get_obj(data, "settings"); + service = obs_service_create(type.c_str(), "default_service", settings, + nullptr); obs_service_release(service); if (!service) return false; + /* Check service returns a protocol */ + const char *protocol = obs_service_get_protocol(service); + if (!protocol) { + blog(LOG_ERROR, + "Service '%s' did not returned a protocol, fallback to empty custom service", + type.c_str()); + return false; + } + + /* Check if the protocol protocol is registered */ + if (!obs_is_output_protocol_registered(protocol)) { + blog(LOG_ERROR, + "Protocol %s is not supported, fallback to empty custom service", + protocol); + return false; + } + /* Enforce Opus on FTL if needed */ - if (strcmp(obs_service_get_protocol(service), "FTL") == 0) { + if (strcmp(protocol, "FTL") == 0) { const char *option = config_get_string( basicConfig, "SimpleOutput", "StreamAudioEncoder"); if (strcmp(option, "opus") != 0) From 5c8b8e438b9c339199989c740ad3c34aba8c205c Mon Sep 17 00:00:00 2001 From: tytan652 Date: Wed, 5 Jul 2023 16:32:10 +0200 Subject: [PATCH 64/65] build-aux: Add script for obs-services JSON parser --- build-aux/README.md | 1 + build-aux/regen-obs-services-json-parser.zsh | 149 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100755 build-aux/regen-obs-services-json-parser.zsh diff --git a/build-aux/README.md b/build-aux/README.md index 66e71d702fdaed..367e515fb77677 100644 --- a/build-aux/README.md +++ b/build-aux/README.md @@ -4,6 +4,7 @@ This folder contains: - The Flatpak manifest used to build OBS Studio - The script `format-manifest.py` which format manifest JSON files - JSON Schemas related to plugins +- The script `regen-obs-services-json-parser.zsh` to regenerate obs-services plugin JSON parser against its schema ## OBS Studio Flatpak Manifest diff --git a/build-aux/regen-obs-services-json-parser.zsh b/build-aux/regen-obs-services-json-parser.zsh new file mode 100755 index 00000000000000..2956da34632d42 --- /dev/null +++ b/build-aux/regen-obs-services-json-parser.zsh @@ -0,0 +1,149 @@ +#!/usr/bin/env zsh + +builtin emulate -L zsh +setopt EXTENDED_GLOB +setopt PUSHD_SILENT +setopt ERR_EXIT +setopt ERR_RETURN +setopt NO_UNSET +setopt PIPE_FAIL +setopt NO_AUTO_PUSHD +setopt NO_PUSHD_IGNORE_DUPS +setopt FUNCTION_ARGZERO + +## Enable for script debugging +# setopt WARN_CREATE_GLOBAL +# setopt WARN_NESTED_VAR +# setopt XTRACE + +autoload -Uz is-at-least && if ! is-at-least 5.2; then + print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade zsh to fix this issue." + exit 1 +fi + +invoke_quicktype() { + local json_parser_file="plugins/obs-services/generated/services-json.hpp" + + if (( ${+commands[quicktype]} )) { + local quicktype_version=($(quicktype --version)) + + if ! is-at-least 23.0.0 ${quicktype_version[3]}; then + log_error "quicktype is not version 23.x.x (found ${quicktype_version[3]}." + exit 2 + fi + + if is-at-least 24.0.0 ${quicktype_version[3]}; then + log_error "quicktype is more recent than version 23.x.x (found ${quicktype_version[3]})." + exit 2 + fi + + } else { + log_error "No viable quicktype version found (required 23.x.x)" + exit 2 + } + + quicktype_args+=( + ## Main schema and its dependencies + -s schema build-aux/json-schema/obs-services.json + -S build-aux/json-schema/service.json + -S build-aux/json-schema/protocolDefs.json + -S build-aux/json-schema/codecDefs.json + ## Language and language options + -l cpp --no-boost + --code-format with-struct + --type-style pascal-case + --member-style camel-case + --enumerator-style upper-underscore-case + # Global include for Nlohmann JSON + --include-location global-include + # Namespace + --namespace OBSServices + # Main struct type name + -t ServicesJson + ) + + if (( check_only )) { + if (( _loglevel > 1 )) log_info "Checking obs-services JSON parser code..." + + if ! quicktype ${quicktype_args} | diff -q "${json_parser_file}" - &> /dev/null; then + log_error "obs-services JSON parser code does not match JSON schemas." + if (( fail_on_error )) return 2; + else + if (( _loglevel > 1 )) log_status "obs-services JSON parser code matches JSON schemas." + fi + } else { + if ! quicktype ${quicktype_args} -o "${json_parser_file}"; then + log_error "An error occured while generating obs-services JSON parser code." + if (( fail_on_error )) return 2; + fi + } +} + +run_quicktype() { + if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} + + typeset -g host_os=${${(L)$(uname -s)}//darwin/macos} + local -i fail_on_error=0 + local -i check_only=0 + local -i verbosity=1 + local -r _version='1.0.0' + + fpath=("${SCRIPT_HOME}/.functions" ${fpath}) + autoload -Uz set_loglevel log_info log_error log_output log_status log_warning + + local -r _usage=" +Usage: %B${functrace[1]%:*}%b