From 005c583304fbe695dfd99f859f915900fed54bbf Mon Sep 17 00:00:00 2001 From: Emil Santurio Date: Thu, 11 Dec 2025 17:15:20 +0100 Subject: [PATCH 1/2] frontend: Auto-discovery of registered services --- frontend/settings/OBSBasicSettings.hpp | 35 ++++ frontend/settings/OBSBasicSettings_Stream.cpp | 188 +++++++++++++++--- 2 files changed, 193 insertions(+), 30 deletions(-) diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp index 38412505398bf6..27d3f4caf88eee 100644 --- a/frontend/settings/OBSBasicSettings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -37,6 +37,39 @@ struct OBSTheme; std::string DeserializeConfigText(const char *value); +// Structured data for service dropdown items +struct ServiceItemData { + enum class Type { + Custom, // rtmp_custom service + ShowAll, // "Show All" option in dropdown + RtmpCommon, // Standard rtmp_common service (Twitch, YouTube, etc.) + CustomServiceType // Custom service types (WHIP, MoQ, etc.) + }; + + Type type; + QString serviceId; // Service ID: for RtmpCommon this is the service name, + // for CustomServiceType this is the service type ID (e.g., "whip_custom") + QString displayName; // Human-readable display name + + ServiceItemData() : type(Type::Custom) {} + + ServiceItemData(Type t, const QString &id = QString(), const QString &name = QString()) + : type(t), + serviceId(id), + displayName(name) + { + } + + // Helper methods for easy type checking + bool isCustom() const { return type == Type::Custom; } + bool isShowAll() const { return type == Type::ShowAll; } + bool isRtmpCommon() const { return type == Type::RtmpCommon; } + bool isCustomServiceType() const { return type == Type::CustomServiceType; } +}; + +// Register with Qt's meta-type system so it can be stored in QVariant +Q_DECLARE_METATYPE(ServiceItemData) + class OBSBasicSettings : public QDialog { Q_OBJECT Q_PROPERTY(QIcon generalIcon READ GetGeneralIcon WRITE SetGeneralIcon DESIGNABLE true) @@ -204,6 +237,8 @@ class OBSBasicSettings : public QDialog { /* stream */ void InitStreamPage(); bool IsCustomService() const; + inline bool IsCustomServiceType() const; + inline QString GetCustomServiceTypeId() const; inline bool IsWHIP() const; void LoadServices(bool showAll); void OnOAuthStreamKeyConnected(); diff --git a/frontend/settings/OBSBasicSettings_Stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp index ce74a1cb1babf4..b0a4913e7d8185 100644 --- a/frontend/settings/OBSBasicSettings_Stream.cpp +++ b/frontend/settings/OBSBasicSettings_Stream.cpp @@ -39,12 +39,43 @@ enum class Section : int { bool OBSBasicSettings::IsCustomService() const { - return ui->service->currentData().toInt() == (int)ListOpt::Custom; + QVariant data = ui->service->currentData(); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + return serviceData.isCustom(); + } + // Fallback for backward compatibility during migration + return data.toInt() == (int)ListOpt::Custom; +} + +inline bool OBSBasicSettings::IsCustomServiceType() const +{ + QVariant data = ui->service->currentData(); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + return serviceData.isCustomServiceType(); + } + // Fallback for backward compatibility during migration + return data.canConvert() && !data.toString().isEmpty(); +} + +inline QString OBSBasicSettings::GetCustomServiceTypeId() const +{ + QVariant data = ui->service->currentData(); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (serviceData.isCustomServiceType()) + return serviceData.serviceId; + } + // Fallback for backward compatibility during migration + if (data.canConvert()) + return data.toString(); + return QString(); } inline bool OBSBasicSettings::IsWHIP() const { - return ui->service->currentData().toInt() == (int)ListOpt::WHIP; + return GetCustomServiceTypeId() == "whip_custom"; } void OBSBasicSettings::InitStreamPage() @@ -113,12 +144,26 @@ void OBSBasicSettings::LoadStream1Settings() 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) + if (!is_rtmp_common) ui->customServer->setText(server); if (is_rtmp_custom) { - ui->service->setCurrentIndex(0); - lastServiceIdx = 0; + // Find the "Custom" option by looking for ServiceItemData with Type::Custom + int idx = -1; + for (int i = 0; i < ui->service->count(); i++) { + QVariant data = ui->service->itemData(i); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (serviceData.isCustom()) { + idx = i; + break; + } + } + } + if (idx == -1) + idx = 0; + ui->service->setCurrentIndex(idx); + lastServiceIdx = idx; lastCustomServer = ui->customServer->text(); bool use_auth = obs_data_get_bool(settings, "use_auth"); @@ -127,11 +172,26 @@ void OBSBasicSettings::LoadStream1Settings() ui->authUsername->setText(QT_UTF8(username)); ui->authPw->setText(QT_UTF8(password)); ui->useAuth->setChecked(use_auth); - } else { - int idx = ui->service->findText(service); + } else if (is_rtmp_common) { + // For rtmp_common services, find by service ID in ServiceItemData + int idx = -1; + for (int i = 0; i < ui->service->count(); i++) { + QVariant data = ui->service->itemData(i); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (serviceData.isRtmpCommon() && serviceData.serviceId == service) { + idx = i; + break; + } + } + } + if (idx == -1) { - if (service && *service) - ui->service->insertItem(1, service); + // Service not found, add it + if (service && *service) { + ServiceItemData newService(ServiceItemData::Type::RtmpCommon, service, service); + ui->service->insertItem(1, service, QVariant::fromValue(newService)); + } idx = 1; } ui->service->setCurrentIndex(idx); @@ -142,6 +202,33 @@ void OBSBasicSettings::LoadStream1Settings() idx = config_get_int(main->Config(), "Twitch", "AddonChoice"); ui->twitchAddonDropdown->setCurrentIndex(idx); + } else { + // For custom service types (WHIP, MoQ, etc.), find by service type ID in ServiceItemData + int idx = -1; + for (int i = 0; i < ui->service->count(); i++) { + QVariant data = ui->service->itemData(i); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (serviceData.isCustomServiceType() && serviceData.serviceId == type) { + idx = i; + break; + } + } + } + + if (idx == -1) { + // Service type not found in dropdown, try to add it + const char *display_name = obs_service_get_display_name(type); + if (!display_name) + display_name = type; + ServiceItemData newService(ServiceItemData::Type::CustomServiceType, QString(type), + QT_UTF8(display_name)); + ui->service->insertItem(1, QT_UTF8(display_name), QVariant::fromValue(newService)); + idx = 1; + } + + ui->service->setCurrentIndex(idx); + lastServiceIdx = idx; } ui->enableMultitrackVideo->setChecked(config_get_bool(main->Config(), "Stream1", "EnableMultitrackVideo")); @@ -255,13 +342,15 @@ void OBSBasicSettings::SwapMultiTrack(const char *protocol) void OBSBasicSettings::SaveStream1Settings() { bool customServer = IsCustomService(); - bool whip = IsWHIP(); + QString customServiceTypeId = GetCustomServiceTypeId(); const char *service_id = "rtmp_common"; + QByteArray serviceIdUtf8; if (customServer) { service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; + } else if (!customServiceTypeId.isEmpty()) { + serviceIdUtf8 = customServiceTypeId.toUtf8(); + service_id = serviceIdUtf8.constData(); } obs_service_t *oldService = main->GetService(); @@ -269,7 +358,7 @@ void OBSBasicSettings::SaveStream1Settings() OBSDataAutoRelease settings = obs_data_create(); - if (!customServer && !whip) { + if (!customServer && customServiceTypeId.isEmpty()) { obs_data_set_string(settings, "service", QT_TO_UTF8(ui->service->currentText())); obs_data_set_string(settings, "protocol", QT_TO_UTF8(protocol)); if (ui->server->currentData() == CustomServerUUID()) { @@ -303,8 +392,7 @@ void OBSBasicSettings::SaveStream1Settings() obs_data_set_bool(settings, "bwtest", false); } - if (whip) { - obs_data_set_string(settings, "service", "WHIP"); + if (IsWHIP()) { 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())); @@ -363,7 +451,7 @@ void OBSBasicSettings::SaveStream1Settings() void OBSBasicSettings::UpdateMoreInfoLink() { - if (IsCustomService() || IsWHIP()) { + if (IsCustomService() || IsCustomServiceType()) { ui->moreInfoButton->hide(); return; } @@ -469,19 +557,47 @@ void OBSBasicSettings::LoadServices(bool showAll) if (showAll) names.sort(Qt::CaseInsensitive); - for (QString &name : names) - ui->service->addItem(name); + // Add rtmp_common services with ServiceItemData + for (QString &name : names) { + ServiceItemData data(ServiceItemData::Type::RtmpCommon, name, name); + ui->service->addItem(name, QVariant::fromValue(data)); + } - if (obs_is_output_protocol_registered("WHIP")) { - ui->service->addItem(QTStr("WHIP"), QVariant((int)ListOpt::WHIP)); + // Enumerate all available service types loaded in OBS + size_t idx = 0; + const char *service_id; + while (obs_enum_service_types(idx++, &service_id)) { + // Skip rtmp_common and rtmp_custom as they're handled separately + if (strcmp(service_id, "rtmp_common") == 0 || strcmp(service_id, "rtmp_custom") == 0) + continue; + + // Get the display name for the service + const char *display_name = obs_service_get_display_name(service_id); + if (!display_name) + display_name = service_id; + + // Check if the service has a registered protocol + OBSServiceAutoRelease temp_service = obs_service_create(service_id, "temp", nullptr, nullptr); + if (temp_service) { + const char *protocol = obs_service_get_protocol(temp_service); + if (protocol && obs_is_output_protocol_registered(protocol)) { + // Add the service to the dropdown with ServiceItemData + ServiceItemData data(ServiceItemData::Type::CustomServiceType, QString(service_id), + QT_UTF8(display_name)); + ui->service->addItem(QT_UTF8(display_name), QVariant::fromValue(data)); + } + } } if (!showAll) { + ServiceItemData showAllData(ServiceItemData::Type::ShowAll); ui->service->addItem(QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), - QVariant((int)ListOpt::ShowAll)); + QVariant::fromValue(showAllData)); } - ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), QVariant((int)ListOpt::Custom)); + ServiceItemData customData(ServiceItemData::Type::Custom); + ui->service->insertItem(0, QTStr("Basic.AutoConfig.StreamPage.Service.Custom"), + QVariant::fromValue(customData)); if (!lastService.isEmpty()) { int idx = ui->service->findText(lastService); @@ -558,7 +674,17 @@ void OBSBasicSettings::UseStreamKeyAdvClicked() void OBSBasicSettings::on_service_currentIndexChanged(int idx) { - if (ui->service->currentData().toInt() == (int)ListOpt::ShowAll) { + // Check if "Show All" was selected + QVariant data = ui->service->currentData(); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (serviceData.isShowAll()) { + LoadServices(true); + ui->service->showPopup(); + return; + } + } else if (data.toInt() == (int)ListOpt::ShowAll) { + // Fallback for backward compatibility LoadServices(true); ui->service->showPopup(); return; @@ -608,7 +734,7 @@ void OBSBasicSettings::ServiceChanged(bool resetFields) { std::string service = QT_TO_UTF8(ui->service->currentText()); bool custom = IsCustomService(); - bool whip = IsWHIP(); + bool customServiceType = IsCustomServiceType(); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); @@ -629,7 +755,7 @@ void OBSBasicSettings::ServiceChanged(bool resetFields) ui->authPwLabel->setVisible(custom); ui->authPwWidget->setVisible(custom); - if (custom || whip) { + if (custom || customServiceType) { ui->destinationLayout->insertRow(1, ui->serverLabel, ui->serverStackedWidget); ui->serverStackedWidget->setCurrentIndex(1); @@ -751,25 +877,27 @@ void OBSBasicSettings::on_authPwShow_clicked() OBSService OBSBasicSettings::SpawnTempService() { bool custom = IsCustomService(); - bool whip = IsWHIP(); + QString customServiceTypeId = GetCustomServiceTypeId(); const char *service_id = "rtmp_common"; + QByteArray serviceIdUtf8; if (custom) { service_id = "rtmp_custom"; - } else if (whip) { - service_id = "whip_custom"; + } else if (!customServiceTypeId.isEmpty()) { + serviceIdUtf8 = customServiceTypeId.toUtf8(); + service_id = serviceIdUtf8.constData(); } OBSDataAutoRelease settings = obs_data_create(); - if (!custom && !whip) { + if (!custom && customServiceTypeId.isEmpty()) { 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())); } - if (whip) + if (IsWHIP()) 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())); From 70b50de06b316319d388462f20b09a0a6ed3afd0 Mon Sep 17 00:00:00 2001 From: Emil Santurio Date: Wed, 14 Jan 2026 19:41:38 +0100 Subject: [PATCH 2/2] frontend: Remove ListOpt and refactor duplicated code OBSBasicSettings --- frontend/settings/OBSBasicSettings.hpp | 5 +- frontend/settings/OBSBasicSettings_Stream.cpp | 107 ++++++------------ 2 files changed, 35 insertions(+), 77 deletions(-) diff --git a/frontend/settings/OBSBasicSettings.hpp b/frontend/settings/OBSBasicSettings.hpp index 27d3f4caf88eee..4c90e472757d87 100644 --- a/frontend/settings/OBSBasicSettings.hpp +++ b/frontend/settings/OBSBasicSettings.hpp @@ -237,9 +237,10 @@ class OBSBasicSettings : public QDialog { /* stream */ void InitStreamPage(); bool IsCustomService() const; - inline bool IsCustomServiceType() const; - inline QString GetCustomServiceTypeId() const; + bool IsCustomServiceType() const; + QString GetCustomServiceTypeId() const; inline bool IsWHIP() const; + int FindService(const std::function &predicate); void LoadServices(bool showAll); void OnOAuthStreamKeyConnected(); void OnAuthConnected(); diff --git a/frontend/settings/OBSBasicSettings_Stream.cpp b/frontend/settings/OBSBasicSettings_Stream.cpp index b0a4913e7d8185..b714f59ce66f70 100644 --- a/frontend/settings/OBSBasicSettings_Stream.cpp +++ b/frontend/settings/OBSBasicSettings_Stream.cpp @@ -26,12 +26,6 @@ extern QCef *cef; extern QCefCookieManager *panel_cookies; extern bool cef_js_avail; -enum class ListOpt : int { - ShowAll = 1, - Custom, - WHIP, -}; - enum class Section : int { Connect, StreamKey, @@ -40,36 +34,23 @@ enum class Section : int { bool OBSBasicSettings::IsCustomService() const { QVariant data = ui->service->currentData(); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - return serviceData.isCustom(); - } - // Fallback for backward compatibility during migration - return data.toInt() == (int)ListOpt::Custom; + ServiceItemData serviceData = data.value(); + return serviceData.isCustom(); } -inline bool OBSBasicSettings::IsCustomServiceType() const +bool OBSBasicSettings::IsCustomServiceType() const { QVariant data = ui->service->currentData(); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - return serviceData.isCustomServiceType(); - } - // Fallback for backward compatibility during migration - return data.canConvert() && !data.toString().isEmpty(); + ServiceItemData serviceData = data.value(); + return serviceData.isCustomServiceType(); } -inline QString OBSBasicSettings::GetCustomServiceTypeId() const +QString OBSBasicSettings::GetCustomServiceTypeId() const { QVariant data = ui->service->currentData(); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - if (serviceData.isCustomServiceType()) - return serviceData.serviceId; - } - // Fallback for backward compatibility during migration - if (data.canConvert()) - return data.toString(); + ServiceItemData serviceData = data.value(); + if (serviceData.isCustomServiceType()) + return serviceData.serviceId; return QString(); } @@ -78,6 +59,20 @@ inline bool OBSBasicSettings::IsWHIP() const return GetCustomServiceTypeId() == "whip_custom"; } +int OBSBasicSettings::FindService(const std::function &predicate) +{ + for (int i = 0; i < ui->service->count(); i++) { + QVariant data = ui->service->itemData(i); + if (data.canConvert()) { + ServiceItemData serviceData = data.value(); + if (predicate(serviceData)) { + return i; + } + } + } + return -1; +} + void OBSBasicSettings::InitStreamPage() { ui->connectAccount2->setVisible(false); @@ -148,18 +143,7 @@ void OBSBasicSettings::LoadStream1Settings() ui->customServer->setText(server); if (is_rtmp_custom) { - // Find the "Custom" option by looking for ServiceItemData with Type::Custom - int idx = -1; - for (int i = 0; i < ui->service->count(); i++) { - QVariant data = ui->service->itemData(i); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - if (serviceData.isCustom()) { - idx = i; - break; - } - } - } + auto idx = FindService([](const ServiceItemData &serviceData) { return serviceData.isCustom(); }); if (idx == -1) idx = 0; ui->service->setCurrentIndex(idx); @@ -173,21 +157,11 @@ void OBSBasicSettings::LoadStream1Settings() ui->authPw->setText(QT_UTF8(password)); ui->useAuth->setChecked(use_auth); } else if (is_rtmp_common) { - // For rtmp_common services, find by service ID in ServiceItemData - int idx = -1; - for (int i = 0; i < ui->service->count(); i++) { - QVariant data = ui->service->itemData(i); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - if (serviceData.isRtmpCommon() && serviceData.serviceId == service) { - idx = i; - break; - } - } - } + auto idx = FindService([&](const ServiceItemData &serviceData) { + return serviceData.isRtmpCommon() && serviceData.serviceId == service; + }); if (idx == -1) { - // Service not found, add it if (service && *service) { ServiceItemData newService(ServiceItemData::Type::RtmpCommon, service, service); ui->service->insertItem(1, service, QVariant::fromValue(newService)); @@ -203,21 +177,11 @@ void OBSBasicSettings::LoadStream1Settings() idx = config_get_int(main->Config(), "Twitch", "AddonChoice"); ui->twitchAddonDropdown->setCurrentIndex(idx); } else { - // For custom service types (WHIP, MoQ, etc.), find by service type ID in ServiceItemData - int idx = -1; - for (int i = 0; i < ui->service->count(); i++) { - QVariant data = ui->service->itemData(i); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - if (serviceData.isCustomServiceType() && serviceData.serviceId == type) { - idx = i; - break; - } - } - } + auto idx = FindService([&](const ServiceItemData &serviceData) { + return serviceData.isCustomServiceType() && serviceData.serviceId == type; + }); if (idx == -1) { - // Service type not found in dropdown, try to add it const char *display_name = obs_service_get_display_name(type); if (!display_name) display_name = type; @@ -676,15 +640,8 @@ void OBSBasicSettings::on_service_currentIndexChanged(int idx) { // Check if "Show All" was selected QVariant data = ui->service->currentData(); - if (data.canConvert()) { - ServiceItemData serviceData = data.value(); - if (serviceData.isShowAll()) { - LoadServices(true); - ui->service->showPopup(); - return; - } - } else if (data.toInt() == (int)ListOpt::ShowAll) { - // Fallback for backward compatibility + ServiceItemData serviceData = data.value(); + if (serviceData.isShowAll()) { LoadServices(true); ui->service->showPopup(); return;