diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index 28c07cdb4e8bea..0b817bf2957f88 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -845,19 +845,6 @@ - - - - - 0 - 0 - - - - Basic.AutoConfig.StreamPage.MoreInfo - - - @@ -877,392 +864,6 @@ - - - - - 0 - 0 - - - - 1 - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 170 - 19 - - - - - - - - - - Basic.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 10 - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 170 - 19 - - - - - - - - - - Basic.AutoConfig.StreamPage.UseStreamKey - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - QFormLayout::AllNonFixedFieldsGrow - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - 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.AutoConfig.StreamPage.ConnectAccount - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - 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 - - - 0 - - - 0 - - - - - QLineEdit::Password - - - - - - - Show - - - - - - - - - - - - - Basic.Settings.Stream.TTVAddon - - - twitchAddonDropdown - - - - - - - Basic.Settings.Stream.IgnoreRecommended - - - - - - - - - - - - - - diff --git a/UI/properties-view.cpp b/UI/properties-view.cpp index a92ae79369ecf7..6bc1d900164b38 100644 --- a/UI/properties-view.cpp +++ b/UI/properties-view.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "double-slider.hpp" #include "slider-ignorewheel.hpp" #include "spinbox-ignorewheel.hpp" @@ -1397,6 +1398,51 @@ void OBSPropertiesView::AddGroup(obs_property_t *prop, QFormLayout *layout) connect(groupBox, SIGNAL(toggled(bool)), info, SLOT(ControlChanged())); } +QWidget *OBSPropertiesView::AddOpenUrl(obs_property_t *prop) +{ + const char *name = obs_property_name(prop); + const char *desc = obs_property_description(prop); + const char *val = obs_data_get_string(settings, name); + + QPushButton *button = new QPushButton(QT_UTF8(desc)); + button->setProperty("themeID", "settingsButtons"); + button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum); + QUrl qurl = QString::fromUtf8(val); + connect(button, &QPushButton::clicked, this, + [=]() { this->OpenUrl(qurl); }); + return NewWidget(prop, button, SIGNAL(clicked())); +} + +QWidget *OBSPropertiesView::AddInfo(obs_property_t *prop) +{ + const char *desc = obs_property_description(prop); + + QLabel *label = new QLabel(QT_UTF8(desc)); + return NewWidget(prop, label, SIGNAL(linkActivated(QString))); +} + +QWidget *OBSPropertiesView::AddInfoBitrate(obs_property_t *prop) +{ + const char *name = obs_property_name(prop); + const char *desc = obs_property_description(prop); + int val = obs_data_get_int(settings, name); + + QLabel *label = new QLabel(QT_UTF8(desc) + " " + QString::number(val) + + " kbps"); + return NewWidget(prop, label, SIGNAL(linkActivated(QString))); +} + +QWidget *OBSPropertiesView::AddInfoFPS(obs_property_t *prop) +{ + const char *name = obs_property_name(prop); + const char *desc = obs_property_description(prop); + int val = obs_data_get_int(settings, name); + + QLabel *label = + new QLabel(QT_UTF8(desc) + " " + QString::number(val) + " FPS"); + return NewWidget(prop, label, SIGNAL(linkActivated(QString))); +} + void OBSPropertiesView::AddProperty(obs_property_t *property, QFormLayout *layout) { @@ -1451,13 +1497,27 @@ void OBSPropertiesView::AddProperty(obs_property_t *property, break; case OBS_PROPERTY_COLOR_ALPHA: AddColorAlpha(property, layout, label); + break; + case OBS_PROPERTY_OPEN_URL: + widget = AddOpenUrl(property); + break; + case OBS_PROPERTY_INFO: + widget = AddInfo(property); + break; + case OBS_PROPERTY_INFO_BITRATE: + widget = AddInfoBitrate(property); + break; + case OBS_PROPERTY_INFO_FPS: + widget = AddInfoFPS(property); } if (widget && !obs_property_enabled(property)) widget->setEnabled(false); if (!label && type != OBS_PROPERTY_BOOL && - type != OBS_PROPERTY_BUTTON && type != OBS_PROPERTY_GROUP) + type != OBS_PROPERTY_BUTTON && type != OBS_PROPERTY_GROUP && + type != OBS_PROPERTY_OPEN_URL && type != OBS_PROPERTY_INFO && + type != OBS_PROPERTY_INFO_BITRATE && type != OBS_PROPERTY_INFO_FPS) label = new QLabel(QT_UTF8(obs_property_description(property))); if (warning && label) //TODO: select color based on background color @@ -1529,6 +1589,11 @@ void OBSPropertiesView::SignalChanged() emit Changed(); } +void OBSPropertiesView::OpenUrl(QUrl url) +{ + QDesktopServices::openUrl(url); +} + static bool FrameRateChangedVariant(const QVariant &variant, media_frames_per_second &fps, obs_data_item_t *&obj, @@ -1950,6 +2015,14 @@ void WidgetInfo::ControlChanged() if (!ColorAlphaChanged(setting)) return; break; + case OBS_PROPERTY_OPEN_URL: + return; + case OBS_PROPERTY_INFO: + return; + case OBS_PROPERTY_INFO_BITRATE: + return; + case OBS_PROPERTY_INFO_FPS: + return; } if (!recently_updated) { diff --git a/UI/properties-view.hpp b/UI/properties-view.hpp index fe1461d94462a0..7cf406693181a7 100644 --- a/UI/properties-view.hpp +++ b/UI/properties-view.hpp @@ -134,6 +134,11 @@ class OBSPropertiesView : public VScrollArea { void AddGroup(obs_property_t *prop, QFormLayout *layout); + QWidget *AddOpenUrl(obs_property_t *prop); + QWidget *AddInfo(obs_property_t *prop); + QWidget *AddInfoBitrate(obs_property_t *prop); + QWidget *AddInfoFPS(obs_property_t *prop); + void AddProperty(obs_property_t *property, QFormLayout *layout); void resizeEvent(QResizeEvent *event) override; @@ -145,6 +150,7 @@ public slots: void ReloadProperties(); void RefreshProperties(); void SignalChanged(); + void OpenUrl(QUrl url); signals: void PropertiesResized(); diff --git a/UI/window-basic-settings-stream.cpp b/UI/window-basic-settings-stream.cpp index d59445e3e0491e..3e87c570b7a69d 100644 --- a/UI/window-basic-settings-stream.cpp +++ b/UI/window-basic-settings-stream.cpp @@ -1,14 +1,15 @@ #include -#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 "url-push-button.hpp" +//#include "url-push-button.hpp" -#ifdef BROWSER_AVAILABLE +/*#ifdef BROWSER_AVAILABLE #include #include "auth-oauth.hpp" #endif @@ -17,13 +18,16 @@ struct QCef; struct QCefCookieManager; extern QCef *cef; -extern QCefCookieManager *panel_cookies; +extern QCefCookieManager *panel_cookies;*/ -enum class ListOpt : int { +/*enum class ListOpt : int { ShowAll = 1, Custom, -}; +};*/ + +#define SHOW_ALL "obs_show_all" +/* enum class Section : int { Connect, StreamKey, @@ -32,15 +36,59 @@ enum class Section : int { inline bool OBSBasicSettings::IsCustomService() const { return ui->service->currentData().toInt() == (int)ListOpt::Custom; +}*/ + +static inline bool WidgetChanged(QWidget *widget) +{ + return widget->property("changed").toBool(); +} + +static inline bool SetComboByValue(QComboBox *combo, const char *name) +{ + int idx = combo->findData(QT_UTF8(name)); + if (idx != -1) { + combo->setCurrentIndex(idx); + return true; + } + + return false; +} + +static inline QString GetComboData(QComboBox *combo) +{ + int idx = combo->currentIndex(); + if (idx == -1) + return QString(); + + return combo->itemData(idx).toString(); +} + +static void WriteJsonData(OBSPropertiesView *view, const char *path) +{ + char full_path[512]; + + if (!view || !WidgetChanged(view)) + return; + + int ret = GetProfilePath(full_path, sizeof(full_path), path); + if (ret > 0) { + obs_data_t *settings = view->GetSettings(); + if (settings) { + obs_data_save_json_safe(settings, full_path, "tmp", + "bak"); + } + } } void OBSBasicSettings::InitStreamPage() { + /* ui->connectAccount2->setVisible(false); ui->disconnectAccount->setVisible(false); ui->bandwidthTestEnable->setVisible(false); ui->twitchAddonDropdown->setVisible(false); ui->twitchAddonLabel->setVisible(false); + */ int vertSpacing = ui->topStreamLayout->verticalSpacing(); @@ -48,17 +96,17 @@ void OBSBasicSettings::InitStreamPage() m.setBottom(vertSpacing / 2); ui->topStreamLayout->setContentsMargins(m); - m = ui->loginPageLayout->contentsMargins(); + /*m = ui->loginPageLayout->contentsMargins(); m.setTop(vertSpacing / 2); ui->loginPageLayout->setContentsMargins(m); m = ui->streamkeyPageLayout->contentsMargins(); m.setTop(vertSpacing / 2); - ui->streamkeyPageLayout->setContentsMargins(m); + ui->streamkeyPageLayout->setContentsMargins(m);*/ LoadServices(false); - ui->twitchAddonDropdown->addItem( + /*ui->twitchAddonDropdown->addItem( QTStr("Basic.Settings.Stream.TTVAddon.None")); ui->twitchAddonDropdown->addItem( QTStr("Basic.Settings.Stream.TTVAddon.BTTV")); @@ -87,10 +135,66 @@ void OBSBasicSettings::InitStreamPage() this, SLOT(UpdateKeyLink())); connect(ui->service, SIGNAL(currentIndexChanged(int)), this, SLOT(UpdateMoreInfoLink())); + */ +} + +OBSPropertiesView * +OBSBasicSettings::CreateServicePropertyView(const char *service, + const char *path, bool changed) +{ + obs_data_t *settings = obs_service_defaults(service); + OBSPropertiesView *view; + + if (path) { + char serviceJsonPath[512]; + int ret = GetProfilePath(serviceJsonPath, + sizeof(serviceJsonPath), path); + if (ret > 0) { + obs_data_t *data = obs_data_create_from_json_file_safe( + serviceJsonPath, "bak"); + obs_data_apply(settings, data); + obs_data_release(data); + } + } + + view = new OBSPropertiesView( + settings, service, + (PropertiesReloadCallback)obs_get_service_properties, 170); + view->setFrameShape(QFrame::StyledPanel); + view->setProperty("changed", QVariant(changed)); + QObject::connect(view, SIGNAL(Changed()), this, SLOT(Stream1Changed())); + + obs_data_release(settings); + return view; } void OBSBasicSettings::LoadStream1Settings() { + loading = true; + //TODO: Legacy Service recovering + + //No legacy behaviour + const char *type = + config_has_user_value(main->Config(), "Stream1", "Service") + ? config_get_string(main->Config(), "Stream1", + "Service") + : "custom_service"; + + delete streamServiceProps; + streamServiceProps = CreateServicePropertyView(type, "service.json"); + ui->streamPage->layout()->addWidget(streamServiceProps); + + curStreamService = type; + + if (!SetComboByValue(ui->service, type)) { + const char *name = obs_service_get_display_name(type); + + ui->service->insertItem(0, QT_UTF8(name), QT_UTF8(type)); + SetComboByValue(ui->service, type); + } + + loading = false; + /* bool ignoreRecommended = config_get_bool(main->Config(), "Stream1", "IgnoreRecommended"); @@ -166,10 +270,18 @@ void OBSBasicSettings::LoadStream1Settings() QMetaObject::invokeMethod(this, "UpdateResFPSLimits", Qt::QueuedConnection); + */ } void OBSBasicSettings::SaveStream1Settings() { + curStreamService = GetComboData(ui->service); + + SaveComboData(ui->service, "Stream1", "Service"); + + WriteJsonData(streamServiceProps, "service.json"); + + /* bool customServer = IsCustomService(); const char *service_id = customServer ? "rtmp_custom" : "rtmp_common"; @@ -235,9 +347,10 @@ void OBSBasicSettings::SaveStream1Settings() main->auth->LoadUI(); SaveCheckBox(ui->ignoreRecommended, "Stream1", "IgnoreRecommended"); + */ } -void OBSBasicSettings::UpdateMoreInfoLink() +/*void OBSBasicSettings::UpdateMoreInfoLink() { if (IsCustomService()) { ui->moreInfoButton->hide(); @@ -264,9 +377,9 @@ void OBSBasicSettings::UpdateMoreInfoLink() ui->moreInfoButton->show(); } obs_properties_destroy(props); -} +}*/ -void OBSBasicSettings::UpdateKeyLink() +/*void OBSBasicSettings::UpdateKeyLink() { QString serviceName = ui->service->currentText(); QString customServer = ui->customServer->text(); @@ -314,10 +427,58 @@ void OBSBasicSettings::UpdateKeyLink() ui->getStreamKeyButton->setTargetUrl(QUrl(streamKeyLink)); ui->getStreamKeyButton->show(); } -} +}*/ void OBSBasicSettings::LoadServices(bool showAll) { + QStringList shortListIds; + shortListIds << "custom_service"; + shortListIds << "twitch"; + shortListIds << "twitch-oauth"; + shortListIds << "youtube"; + shortListIds << "facebook"; + shortListIds << "restream"; + shortListIds << "restream-oauth"; + shortListIds << "twitter"; + + const char *id; + size_t idx = 0; + + ui->service->blockSignals(true); + ui->service->clear(); + + while (obs_enum_service_types(idx++, &id)) { + const char *name = obs_service_get_display_name(id); + + QString qName = QT_UTF8(name); + QString qId = QT_UTF8(id); + + if (showAll || shortListIds.contains(qId)) + ui->service->addItem(qName, qId); + } + + if (!showAll) { + ui->service->addItem( + QTStr("Basic.AutoConfig.StreamPage.Service.ShowAll"), + QT_UTF8(SHOW_ALL)); + } else { + QSortFilterProxyModel *model = + new QSortFilterProxyModel(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); + + model->setSortCaseSensitivity(Qt::CaseInsensitive); + + ui->service->setModel(model); + + ui->service->model()->sort(0); + } + + ui->service->blockSignals(false); + + /* obs_properties_t *props = obs_get_service_properties("rtmp_common"); OBSData settings = obs_data_create(); @@ -365,15 +526,34 @@ void OBSBasicSettings::LoadServices(bool showAll) obs_properties_destroy(props); ui->service->blockSignals(false); + */ } -static inline bool is_auth_service(const std::string &service) +/*static inline bool is_auth_service(const std::string &service) { return Auth::AuthType(service) != Auth::Type::None; -} +}*/ void OBSBasicSettings::on_service_currentIndexChanged(int) { + bool showAll = GetComboData(ui->service) == QT_UTF8(SHOW_ALL); + if (showAll) { + LoadServices(true); + return; + } + + QString service = GetComboData(ui->service); + if (!loading) { + bool loadSettings = service == curStreamService; + + delete streamServiceProps; + streamServiceProps = CreateServicePropertyView( + QT_TO_UTF8(service), + loadSettings ? "service.json" : nullptr, true); + ui->streamPage->layout()->addWidget(streamServiceProps); + } + + /* bool showMore = ui->service->currentData().toInt() == (int)ListOpt::ShowAll; if (showMore) @@ -435,9 +615,10 @@ void OBSBasicSettings::on_service_currentIndexChanged(int) OnAuthConnected(); } #endif + */ } -void OBSBasicSettings::UpdateServerList() +/*void OBSBasicSettings::UpdateServerList() { QString serviceName = ui->service->currentText(); bool showMore = ui->service->currentData().toInt() == @@ -472,9 +653,9 @@ void OBSBasicSettings::UpdateServerList() } obs_properties_destroy(props); -} +}*/ -void OBSBasicSettings::on_show_clicked() +/*void OBSBasicSettings::on_show_clicked() { if (ui->key->echoMode() == QLineEdit::Password) { ui->key->setEchoMode(QLineEdit::Normal); @@ -483,9 +664,9 @@ void OBSBasicSettings::on_show_clicked() ui->key->setEchoMode(QLineEdit::Password); ui->show->setText(QTStr("Show")); } -} +}*/ -void OBSBasicSettings::on_authPwShow_clicked() +/*void OBSBasicSettings::on_authPwShow_clicked() { if (ui->authPw->echoMode() == QLineEdit::Password) { ui->authPw->setEchoMode(QLineEdit::Normal); @@ -494,9 +675,9 @@ void OBSBasicSettings::on_authPwShow_clicked() ui->authPw->setEchoMode(QLineEdit::Password); ui->authPwShow->setText(QTStr("Show")); } -} +}*/ -OBSService OBSBasicSettings::SpawnTempService() +/*OBSService OBSBasicSettings::SpawnTempService() { bool custom = IsCustomService(); const char *service_id = custom ? "rtmp_custom" : "rtmp_common"; @@ -521,9 +702,9 @@ OBSService OBSBasicSettings::SpawnTempService() obs_service_release(newService); return newService; -} +}*/ -void OBSBasicSettings::OnOAuthStreamKeyConnected() +/*void OBSBasicSettings::OnOAuthStreamKeyConnected() { #ifdef BROWSER_AVAILABLE OAuthStreamKey *a = reinterpret_cast(auth.get()); @@ -550,9 +731,9 @@ void OBSBasicSettings::OnOAuthStreamKeyConnected() ui->streamStackWidget->setCurrentIndex((int)Section::StreamKey); #endif -} +}*/ -void OBSBasicSettings::OnAuthConnected() +/*void OBSBasicSettings::OnAuthConnected() { std::string service = QT_TO_UTF8(ui->service->currentText()); Auth::Type type = Auth::AuthType(service); @@ -565,9 +746,9 @@ void OBSBasicSettings::OnAuthConnected() stream1Changed = true; EnableApplyButton(true); } -} +}*/ -void OBSBasicSettings::on_connectAccount_clicked() +/*void OBSBasicSettings::on_connectAccount_clicked() { #ifdef BROWSER_AVAILABLE std::string service = QT_TO_UTF8(ui->service->currentText()); @@ -633,17 +814,17 @@ void OBSBasicSettings::on_useAuth_toggled() ui->authUsername->setVisible(use_auth); ui->authPwLabel->setVisible(use_auth); ui->authPwWidget->setVisible(use_auth); -} +}*/ void OBSBasicSettings::UpdateVodTrackSetting() { - bool enableForCustomServer = config_get_bool( - GetGlobalConfig(), "General", "EnableCustomServerVodTrack"); + /*bool enableForCustomServer = config_get_bool( + GetGlobalConfig(), "General", "EnableCustomServerVodTrack");*/ bool enableVodTrack = ui->service->currentText() == "Twitch"; bool wasEnabled = !!vodTrackCheckbox; - if (enableForCustomServer && IsCustomService()) - enableVodTrack = true; + /*if (enableForCustomServer && IsCustomService()) + enableVodTrack = true;*/ if (enableVodTrack == wasEnabled) return; @@ -721,13 +902,13 @@ void OBSBasicSettings::UpdateVodTrackSetting() } } -OBSService OBSBasicSettings::GetStream1Service() +/*OBSService OBSBasicSettings::GetStream1Service() { return stream1Changed ? SpawnTempService() : OBSService(main->GetService()); -} +}*/ -void OBSBasicSettings::UpdateServiceRecommendations() +/*void OBSBasicSettings::UpdateServiceRecommendations() { bool customServer = IsCustomService(); ui->ignoreRecommended->setVisible(!customServer); @@ -786,12 +967,12 @@ void OBSBasicSettings::UpdateServiceRecommendations() #undef ENFORCE_TEXT ui->enforceSettingsLabel->setText(text); -} +}*/ void OBSBasicSettings::DisplayEnforceWarning(bool checked) { - if (IsCustomService()) - return; + /*if (IsCustomService()) + return;*/ if (!checked) { SimpleRecordingEncoderChanged(); @@ -808,9 +989,9 @@ void OBSBasicSettings::DisplayEnforceWarning(bool checked) #undef ENFORCE_WARNING if (button == QMessageBox::No) { - QMetaObject::invokeMethod(ui->ignoreRecommended, "setChecked", + /*QMetaObject::invokeMethod(ui->ignoreRecommended, "setChecked", Qt::QueuedConnection, - Q_ARG(bool, false)); + Q_ARG(bool, false));*/ return; } @@ -887,17 +1068,17 @@ void OBSBasicSettings::UpdateResFPSLimits() if (idx == -1) return; - bool ignoreRecommended = ui->ignoreRecommended->isChecked(); + //bool ignoreRecommended = ui->ignoreRecommended->isChecked(); BPtr res_list; size_t res_count = 0; int max_fps = 0; - if (!IsCustomService() && !ignoreRecommended) { + /*if (!IsCustomService() && !ignoreRecommended) { OBSService service = GetStream1Service(); obs_service_get_supported_resolutions(service, &res_list, &res_count); obs_service_get_max_fps(service, &max_fps); - } + }*/ /* ------------------------------------ */ /* Check for enforced res/FPS */ @@ -959,8 +1140,8 @@ void OBSBasicSettings::UpdateResFPSLimits() /* 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); + /*ui->ignoreRecommended->setChecked(true); + ui->ignoreRecommended->setProperty("changed", true);*/ stream1Changed = true; EnableApplyButton(true); UpdateResFPSLimits(); @@ -992,11 +1173,11 @@ void OBSBasicSettings::UpdateResFPSLimits() Qt::QueuedConnection, Q_ARG(int, lastServiceIdx)); else - QMetaObject::invokeMethod(ui->ignoreRecommended, + /*QMetaObject::invokeMethod(ui->ignoreRecommended, "setChecked", Qt::QueuedConnection, - Q_ARG(bool, true)); - return; + Q_ARG(bool, true));*/ + return; } } @@ -1076,6 +1257,6 @@ void OBSBasicSettings::UpdateResFPSLimits() /* ------------------------------------ */ - lastIgnoreRecommended = (int)ignoreRecommended; + //lastIgnoreRecommended = (int)ignoreRecommended; lastServiceIdx = idx; } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 6062c5f95c22a3..d68fe478e39118 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -417,7 +417,7 @@ 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->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); @@ -425,7 +425,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) 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->ignoreRecommended, CHECK_CHANGED, STREAM1_CHANGED);*/ HookWidget(ui->outputMode, COMBO_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleOutputPath, EDIT_CHANGED, OUTPUTS_CHANGED); HookWidget(ui->simpleNoSpace, CHECK_CHANGED, OUTPUTS_CHANGED); @@ -760,8 +760,8 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) this, SLOT(SimpleRecordingEncoderChanged())); connect(ui->simpleOutAdvanced, SIGNAL(toggled(bool)), this, SLOT(SimpleRecordingEncoderChanged())); - connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this, - SLOT(SimpleRecordingEncoderChanged())); + /*connect(ui->ignoreRecommended, SIGNAL(toggled(bool)), this, + SLOT(SimpleRecordingEncoderChanged()));*/ connect(ui->simpleReplayBuf, SIGNAL(toggled(bool)), this, SLOT(SimpleReplayBufferChanged())); connect(ui->simpleOutputVBitrate, SIGNAL(valueChanged(int)), this, @@ -3661,6 +3661,10 @@ void OBSBasicSettings::SaveSettings() if (advancedChanged) SaveAdvancedSettings(); + if (stream1Changed || outputsChanged || videoChanged) { + //TODO: Check maximum bitrates and fps, supported resolutions and recommended settings + } + if (videoChanged || advancedChanged) main->ResetVideo(); @@ -4690,12 +4694,12 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() { QString qual = ui->simpleOutRecQuality->currentData().toString(); QString warning; - bool enforceBitrate = !ui->ignoreRecommended->isChecked(); - OBSService service = GetStream1Service(); + //bool enforceBitrate = !ui->ignoreRecommended->isChecked(); + //OBSService service = GetStream1Service(); delete simpleOutRecWarning; - if (enforceBitrate && service) { + /*if (enforceBitrate && service) { obs_data_t *videoSettings = obs_data_create(); obs_data_t *audioSettings = obs_data_create(); int oldVBitrate = ui->simpleOutputVBitrate->value(); @@ -4722,7 +4726,7 @@ void OBSBasicSettings::SimpleRecordingEncoderChanged() obs_data_release(videoSettings); obs_data_release(audioSettings); - } + }*/ if (qual == "Lossless") { if (!warning.isEmpty()) diff --git a/UI/window-basic-settings.hpp b/UI/window-basic-settings.hpp index 4b5d352f20c42a..b9051d95a40c67 100644 --- a/UI/window-basic-settings.hpp +++ b/UI/window-basic-settings.hpp @@ -128,7 +128,7 @@ class OBSBasicSettings : public QDialog { OBSFFFormatDesc formats; - OBSPropertiesView *streamProperties = nullptr; + OBSPropertiesView *streamServiceProps = nullptr; OBSPropertiesView *streamEncoderProps = nullptr; OBSPropertiesView *recordEncoderProps = nullptr; @@ -140,6 +140,7 @@ class OBSBasicSettings : public QDialog { QString curNVENCPreset; QString curAMDPreset; + QString curStreamService; QString curAdvStreamEncoder; QString curAdvRecordEncoder; @@ -243,28 +244,32 @@ class OBSBasicSettings : public QDialog { /* stream */ void InitStreamPage(); - inline bool IsCustomService() const; + //inline bool IsCustomService() const; void LoadServices(bool showAll); - void OnOAuthStreamKeyConnected(); - void OnAuthConnected(); + //void OnOAuthStreamKeyConnected(); + //void OnAuthConnected(); QString lastService; int prevLangIndex; bool prevBrowserAccel; + + OBSPropertiesView *CreateServicePropertyView(const char *service, + const char *path, + bool changed = false); private slots: - void UpdateServerList(); - void UpdateKeyLink(); + //void UpdateServerList(); + //void UpdateKeyLink(); void UpdateVodTrackSetting(); - void UpdateServiceRecommendations(); + //void UpdateServiceRecommendations(); void RecreateOutputResolutionWidget(); void UpdateResFPSLimits(); - void UpdateMoreInfoLink(); + //void UpdateMoreInfoLink(); 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_show_clicked(); + //void on_authPwShow_clicked(); + //void on_connectAccount_clicked(); + //void on_disconnectAccount_clicked(); + //void on_useStreamKey_clicked(); + //void on_useAuth_toggled(); private: /* output */ @@ -330,7 +335,7 @@ private slots: int CurrentFLVTrack(); - OBSService GetStream1Service(); + //OBSService GetStream1Service(); private slots: void on_theme_activated(int idx); @@ -389,7 +394,7 @@ private slots: void SimpleStreamingEncoderChanged(); - OBSService SpawnTempService(); + //OBSService SpawnTempService(); void SetGeneralIcon(const QIcon &icon); void SetStreamIcon(const QIcon &icon); diff --git a/docs/sphinx/reference-properties.rst b/docs/sphinx/reference-properties.rst index b7d8c8d9071de8..78fb6d8d7c2ccd 100644 --- a/docs/sphinx/reference-properties.rst +++ b/docs/sphinx/reference-properties.rst @@ -47,7 +47,7 @@ General Functions :param flags: 0 or a bitwise OR combination of one of the following values: - + - OBS_PROPERTIES_DEFER_UPDATE - A hint that tells the front-end to defers updating the settings until the user has finished editing all properties rather than @@ -266,7 +266,7 @@ Property Object Functions :param name: Setting identifier string :param description: Localized name shown to user :param type: Can be one of the following values: - + - **OBS_EDITABLE_LIST_TYPE_STRINGS** - An editable list of strings. - **OBS_EDITABLE_LIST_TYPE_FILES** - An @@ -320,6 +320,59 @@ Property Object Functions --------------------- +.. function:: obs_property_t *obs_properties_add_open_url(obs_properties_t *props, const char *name, const char *description) + + Adds a button which open a URL. This property does not actually + store any settings; it's used to implement a button in user + interface if the properties are used to generate user interface. + + :param name: Setting identifier string + :param description: Localized name shown to user + + :return: The property + +--------------------- + +.. function:: obs_property_t *obs_properties_add_info(obs_properties_t *props, const char *name, const char *description) + + Adds a label. This property does not actually store any settings; + it's used to implement a label in user interface if the properties + are used to generate user interface. + + :param name: Setting identifier string + :param description: Localized name shown to user + + :return: The property + +--------------------- + +.. function:: obs_property_t *obs_properties_add_info_bitrate(obs_properties_t *props, const char *name, const char *description) + + Adds a label with a bitrate in kbps. This property does not + actually store any settings; it's used to implement a label in + user interface if the properties are used to generate user + interface. + + :param name: Setting identifier string + :param description: Localized name shown to user + + :return: The property + +--------------------- + +.. function:: obs_property_t *obs_properties_add_info_fps(obs_properties_t *props, const char *name, const char *description) + + Adds a label with a frequency in FPS. This property does not + actually store any settings; it's used to implement a label in + user interface if the properties are used to generate user + interface. + + :param name: Setting identifier string + :param description: Localized name shown to user + + :return: The property + +--------------------- Property Enumeration Functions ------------------------------ diff --git a/docs/sphinx/reference-services.rst b/docs/sphinx/reference-services.rst index fdfb85e419c4e9..c97343dccc259e 100644 --- a/docs/sphinx/reference-services.rst +++ b/docs/sphinx/reference-services.rst @@ -90,6 +90,10 @@ Service Definition Structure :return: *true* to allow the output to start up, *false* to prevent output from starting up +.. member:: const char *(*obs_service_info.get_protocol)(void *data) + + :return: The stream protocol + .. member:: const char *(*obs_service_info.get_url)(void *data) :return: The stream URL @@ -163,7 +167,7 @@ General Service Functions .. function:: obs_service_t *obs_service_create(const char *id, const char *name, obs_data_t *settings, obs_data_t *hotkey_data) Creates a service with the specified settings. - + The "service" context is used for encoding video/audio data. Use obs_service_release to release it. @@ -267,7 +271,7 @@ General Service Functions .. function:: void obs_service_apply_encoder_settings(obs_service_t *service, obs_data_t *video_encoder_settings, obs_data_t *audio_encoder_settings) Applies service-specific video encoder settings. - + :param video_encoder_settings: Video encoder settings. Can be *NULL* :param audio_encoder_settings: Audio encoder settings. Can be *NULL* diff --git a/libobs/obs-properties.c b/libobs/obs-properties.c index de1705daf9270c..81bde284d81264 100644 --- a/libobs/obs-properties.c +++ b/libobs/obs-properties.c @@ -440,6 +440,14 @@ static inline size_t get_property_size(enum obs_property_type type) return sizeof(struct group_data); case OBS_PROPERTY_COLOR_ALPHA: return 0; + case OBS_PROPERTY_OPEN_URL: + return 0; + case OBS_PROPERTY_INFO: + return 0; + case OBS_PROPERTY_INFO_BITRATE: + return 0; + case OBS_PROPERTY_INFO_FPS: + return 0; } return 0; @@ -806,6 +814,42 @@ obs_property_t *obs_properties_add_group(obs_properties_t *props, return p; } +obs_property_t *obs_properties_add_open_url(obs_properties_t *props, + const char *name, const char *desc) +{ + if (!props || has_prop(props, name)) + return NULL; + + struct obs_property *p = + new_prop(props, name, desc, OBS_PROPERTY_OPEN_URL); + return p; +} + +obs_property_t *obs_properties_add_info(obs_properties_t *props, + const char *name, const char *desc) +{ + if (!props || has_prop(props, name)) + return NULL; + return new_prop(props, name, desc, OBS_PROPERTY_INFO); +} + +obs_property_t *obs_properties_add_info_bitrate(obs_properties_t *props, + const char *name, + const char *desc) +{ + if (!props || has_prop(props, name)) + return NULL; + return new_prop(props, name, desc, OBS_PROPERTY_INFO_BITRATE); +} + +obs_property_t *obs_properties_add_info_fps(obs_properties_t *props, + const char *name, const char *desc) +{ + if (!props || has_prop(props, name)) + return NULL; + return new_prop(props, name, desc, OBS_PROPERTY_INFO_FPS); +} + /* ------------------------------------------------------------------------- */ static inline bool is_combo(struct obs_property *p) diff --git a/libobs/obs-properties.h b/libobs/obs-properties.h index 92790120760f3f..b268a226f513e7 100644 --- a/libobs/obs-properties.h +++ b/libobs/obs-properties.h @@ -57,6 +57,10 @@ enum obs_property_type { OBS_PROPERTY_FRAME_RATE, OBS_PROPERTY_GROUP, OBS_PROPERTY_COLOR_ALPHA, + OBS_PROPERTY_OPEN_URL, + OBS_PROPERTY_INFO, + OBS_PROPERTY_INFO_BITRATE, + OBS_PROPERTY_INFO_FPS, }; enum obs_combo_format { @@ -267,6 +271,22 @@ EXPORT obs_property_t *obs_properties_add_group(obs_properties_t *props, enum obs_group_type type, obs_properties_t *group); +EXPORT obs_property_t *obs_properties_add_open_url(obs_properties_t *props, + const char *name, + const char *desc); + +EXPORT obs_property_t *obs_properties_add_info(obs_properties_t *props, + const char *name, + const char *desc); + +EXPORT obs_property_t *obs_properties_add_info_bitrate(obs_properties_t *props, + const char *name, + const char *desc); + +EXPORT obs_property_t *obs_properties_add_info_fps(obs_properties_t *props, + const char *name, + const char *desc); + /* ------------------------------------------------------------------------- */ /** diff --git a/libobs/obs-service.c b/libobs/obs-service.c index e2fb0e8ec6e20b..91f72b6cd7e921 100644 --- a/libobs/obs-service.c +++ b/libobs/obs-service.c @@ -126,6 +126,9 @@ static inline obs_data_t *get_defaults(const struct obs_service_info *info) obs_data_t *settings = obs_data_create(); if (info->get_defaults) info->get_defaults(settings); + if (info->get_defaults2) { + info->get_defaults2(settings, info->type_data); + } return settings; } @@ -138,11 +141,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; + obs_properties_t *properties = NULL; + + if (info->get_properties2) { + properties = + info->get_properties2(NULL, info->type_data); + } else if (info->get_properties) { + properties = info->get_properties(NULL); + } - properties = info->get_properties(NULL); obs_properties_apply_settings(properties, defaults); obs_data_release(defaults); return properties; @@ -155,6 +164,14 @@ 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_properties2) { + obs_properties_t *props; + props = service->info.get_properties2(service->context.data, + service->info.type_data); + obs_properties_apply_settings(props, service->context.settings); + return props; + } + if (service->info.get_properties) { obs_properties_t *props; props = service->info.get_properties(service->context.data); @@ -207,6 +224,16 @@ proc_handler_t *obs_service_get_proc_handler(const obs_service_t *service) : NULL; } +const char *obs_service_get_protocol(const obs_service_t *service) +{ + if (!obs_service_valid(service, "obs_service_get_protocol")) + return NULL; + + if (!service->info.get_protocol) + return NULL; + return service->info.get_protocol(service->context.data); +} + const char *obs_service_get_url(const obs_service_t *service) { if (!obs_service_valid(service, "obs_service_get_url")) diff --git a/libobs/obs-service.h b/libobs/obs-service.h index 697ccb39e7f8d9..e199471e9fa7b3 100644 --- a/libobs/obs-service.h +++ b/libobs/obs-service.h @@ -34,21 +34,63 @@ struct obs_service_resolution { }; struct obs_service_info { - /* required */ + /* ----------------------------------------------------------------- */ + /* Required implementation*/ + + /** Specifies the named identifier of this service */ const char *id; + /** + * Gets the full translated name of this service + * + * @param type_data The type_data variable of this structure + * @return Translated name of the service + */ const char *(*get_name)(void *type_data); + + /** + * Creates the service with the specified settings + * + * @param settings Settings for the service + * @param service OBS service context + * @return Data associated with this service context, or + * NULL if initialization failed. + */ void *(*create)(obs_data_t *settings, obs_service_t *service); + + /** + * Destroys the service data + * + * @param data Data associated with this service context + */ void (*destroy)(void *data); - /* optional */ + /* ----------------------------------------------------------------- */ + /* Optional implementation */ + void (*activate)(void *data, obs_data_t *settings); void (*deactivate)(void *data); + /** + * Updates the settings for this service + * + * @param data Data associated with this service context + * @param settings New settings for this service + */ void (*update)(void *data, obs_data_t *settings); + /** + * Gets the default settings for this service + * + * @param[out] settings Data to assign default settings to + */ void (*get_defaults)(obs_data_t *settings); + /** + * Gets the property information of this service + * + * @return The properties data + */ obs_properties_t *(*get_properties)(void *data); /** @@ -62,6 +104,7 @@ struct obs_service_info { */ bool (*initialize)(void *data, obs_output_t *output); + const char *(*get_protocol)(void *data); const char *(*get_url)(void *data); const char *(*get_key)(void *data); @@ -86,11 +129,37 @@ struct obs_service_info { void (*get_max_bitrate)(void *data, int *video_bitrate, int *audio_bitrate); + + /** + * Gets the default settings for this service + * + * If get_defaults is also defined both will be called, and the first + * call will be to get_defaults, then to get_defaults2. + * + * @param[out] settings Data to assign default settings to + * @param[in] typedata Type Data + */ + void (*get_defaults2)(obs_data_t *settings, void *type_data); + + /** + * Gets the property information of this service + * + * @param[in] data Pointer from create (or null) + * @param[in] typedata Type Data + * @return The properties data + */ + obs_properties_t *(*get_properties2)(void *data, void *type_data); }; EXPORT void obs_register_service_s(const struct obs_service_info *info, size_t size); +/** + * Register an service definition to the current obs context. This should be + * used in obs_module_load. + * + * @param info Pointer to the source definition structure. + */ #define obs_register_service(info) \ obs_register_service_s(info, sizeof(struct obs_service_info)) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a39f357719ef42..7043f7a88f3f32 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -87,6 +87,7 @@ add_subdirectory(obs-libfdk) add_subdirectory(obs-ffmpeg) add_subdirectory(obs-outputs) add_subdirectory(obs-filters) +add_subdirectory(obs-services) add_subdirectory(obs-transitions) add_subdirectory(obs-text) add_subdirectory(rtmp-services) diff --git a/plugins/obs-outputs/CMakeLists.txt b/plugins/obs-outputs/CMakeLists.txt index 3ec627396e75ae..e19146bbbe7b68 100644 --- a/plugins/obs-outputs/CMakeLists.txt +++ b/plugins/obs-outputs/CMakeLists.txt @@ -21,8 +21,10 @@ if (WITH_RTMPS) find_package(ZLIB REQUIRED) add_definitions(-DCRYPTO -DUSE_MBEDTLS) include_directories(${MBEDTLS_INCLUDE_DIRS} ${ZLIB_INCLUDE_DIRS}) + set(RTMPS_NOT_AVAILABLE OFF CACHE BOOL "Internal global cmake variable" FORCE) else() add_definitions(-DNO_CRYPTO) + set(RTMPS_NOT_AVAILABLE ON CACHE BOOL "Internal global cmake variable" FORCE) endif() set(COMPILE_FTL FALSE) @@ -92,6 +94,12 @@ elseif (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ftl-sdk/CMakeLists.txt") set(COMPILE_FTL TRUE) endif() +if (COMPILE_FTL) + set(FTL_NOT_AVAILABLE OFF CACHE BOOL "Internal global cmake variable" FORCE) +else() + set(FTL_NOT_AVAILABLE ON CACHE BOOL "Internal global cmake variable" FORCE) +endif() + configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/obs-outputs-config.h.in" "${CMAKE_BINARY_DIR}/plugins/obs-outputs/config/obs-outputs-config.h") diff --git a/plugins/obs-services/CMakeLists.txt b/plugins/obs-services/CMakeLists.txt new file mode 100644 index 00000000000000..d430b4b48a303a --- /dev/null +++ b/plugins/obs-services/CMakeLists.txt @@ -0,0 +1,52 @@ +project(obs-services) + +include_directories(${OBS_JANSSON_INCLUDE_DIRS}) + +set(RTMPS_DISABLED FALSE) +set(FTL_DISABLED FALSE) + +if(RTMPS_NOT_AVAILABLE) + set(RTMPS_DISABLED TRUE) +endif() + +if(FTL_NOT_AVAILABLE) + set(FTL_DISABLED TRUE) +endif() + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/protocols.hpp.in" + "${CMAKE_BINARY_DIR}/plugins/obs-services/config/protocols.hpp") + +include_directories("${CMAKE_BINARY_DIR}/plugins/obs-services/config") + +set(obs-services_SOURCES + service-instance.cpp + service-factory.cpp + service-manager.cpp + plugin-main.cpp) +set(obs-services_HEADERS + service-instance.cpp + service-factory.cpp + service-manager.cpp + json-format-ver.hpp + "${CMAKE_BINARY_DIR}/plugins/obs-services/config/protocols.hpp" + plugin.hpp) + +if(WIN32) + set(MODULE_DESCRIPTION "OBS Core Services") + configure_file(${CMAKE_SOURCE_DIR}/cmake/winrc/obs-module.rc.in obs-services.rc) + list(APPEND obs-services_SOURCES + obs-services.rc) +endif() + +add_library(obs-services MODULE + ${obs-services_SOURCES} + ${obs-services_HEADERS}) + +target_link_libraries(obs-services + libobs + ${OBS_JANSSON_IMPORT}) + +set_target_properties(obs-services PROPERTIES FOLDER "plugins") + +install_obs_plugin_with_data(obs-services data) 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..479b25b1050c82 --- /dev/null +++ b/plugins/obs-services/data/locale/en-US.ini @@ -0,0 +1,11 @@ +MoreInfo="More Info" +Protocol="Protocol" +Server="Server" +StreamKey="Stream key" +StreamKeyLink="Get Stream Key" +MaxVideoBitrate="Maximum Video Bitrate:" +MaxAudioBitrate="Maximum Audio Bitrate:" +MaxFPS="Maximum FPS:" +IgnoreMaximum="Ignore streaming service maximum limits" +SupportedResolutions="Supported resolution(s):" +IgnoreSupportedResolutions="Ignore supported resolutions" diff --git a/plugins/obs-services/data/services.json b/plugins/obs-services/data/services.json new file mode 100644 index 00000000000000..36467ff7e2dc15 --- /dev/null +++ b/plugins/obs-services/data/services.json @@ -0,0 +1,2133 @@ +{ + "format_version": 4, + "services": [ + { + "id": "youtube", + "name": "YouTube", + "more_info_link": "https://developers.google.com/youtube/v3/live/guides/ingestion-protocol-comparison", + "stream_key_link": "https://www.youtube.com/live_dashboard", + "available_protocols": ["HLS", "RTMPS", "RTMP"], + "servers": [ + { + "protocol": "HLS", + "name": "Primary YouTube ingest server (HLS)", + "url": "https://a.upload.youtube.com/http_upload_hls?cid={stream_key}©=0&file=out.m3u8" + }, + { + "protocol": "HLS", + "name": "Backup YouTube ingest server (HLS)", + "url": "https://b.upload.youtube.com/http_upload_hls?cid={stream_key}©=1&file=out.m3u8" + }, + { + "protocol": "RTMPS", + "name": "Primary YouTube ingest server (RTMPS)", + "url": "rtmps://a.rtmps.youtube.com:443/live2" + }, + { + "protocol": "RTMPS", + "name": "Backup YouTube ingest server (RTMPS)", + "url": "rtmps://b.rtmps.youtube.com:443/live2?backup=1" + }, + { + "protocol": "RTMP", + "name": "Primary YouTube ingest server (legacy RTMP)", + "url": "rtmp://a.rtmp.youtube.com/live2" + }, + { + "protocol": "RTMP", + "name": "Backup YouTube ingest server (legacy RTMP)", + "url": "rtmp://b.rtmp.youtube.com/live2?backup=1" + } + ], + "maximum": [ + { + "protocol": "HLS", + "video_bitrate": 51000, + "audio_bitrate": 160 + }, + { + "protocol": "RTMPS", + "video_bitrate": 51000, + "audio_bitrate": 160 + }, + { + "protocol": "RTMP", + "video_bitrate": 51000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "loola_tv", + "name": "Loola.tv", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US East: Virginia", + "url": "rtmp://rtmp.loola.tv/push" + }, + { + "name": "EU Central: Germany", + "url": "rtmp://rtmp-eu.loola.tv/push" + }, + { + "name": "South America: Brazil", + "url": "rtmp://rtmp-sa.loola.tv/push" + }, + { + "name": "Asia/Pacific: Singapore", + "url": "rtmp://rtmp-sg.loola.tv/push" + }, + { + "name": "Middle East: Bahrain", + "url": "rtmp://rtmp-me.loola.tv/push" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 2500, + "audio_bitrate": 160 + } + ] + }, + { + "id": "luzento", + "name": "Luzento.com", + "stream_key_link": "https://cms.luzento.com/dashboard/stream-key?from=OBS", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://ingest.luzento.com/live" + }, + { + "name": "Primary (Test)", + "url": "rtmp://ingest.luzento.com/test" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 256 + } + ] + }, + { + "id": "vimm", + "name": "VIMM", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Europe: Frankfurt", + "url": "rtmp://eu.vimm.tv/live" + }, + { + "name": "North America: Montreal", + "url": "rtmp://us.vimm.tv/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 8000, + "audio_bitrate": 320 + } + ] + }, + { + "id": "mobcrush", + "name": "Mobcrush", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://live.mobcrush.net/mob" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "web_dot_tv", + "name": "Web.TV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://live3.origins.web.tv/liveext" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3500, + "audio_bitrate": 160 + } + ] + }, + { + "id": "googgame_ru", + "name": "GoodGame.ru", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Моscow", + "url": "rtmp://msk.goodgame.ru:1940/live" + } + ] + }, + { + "id": "youstreamer", + "name": "YouStreamer", + "stream_key_link": "https://app.youstreamer.com/stream/", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Moscow", + "url": "rtmp://push.youstreamer.com/in/" + } + ] + }, + { + "id": "vaughn", + "name": "Vaughn Live / iNSTAGIB", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US: Chicago, IL", + "url": "rtmp://live-ord.vaughnsoft.net/live" + }, + { + "name": "US: Vint Hill, VA", + "url": "rtmp://live-iad.vaughnsoft.net/live" + }, + { + "name": "US: Denver, CO", + "url": "rtmp://live-den.vaughnsoft.net/live" + }, + { + "name": "US: New York, NY", + "url": "rtmp://live-nyc.vaughnsoft.net/live" + }, + { + "name": "US: Miami, FL", + "url": "rtmp://live-mia.vaughnsoft.net/live" + }, + { + "name": "US: Seattle, WA", + "url": "rtmp://live-sea.vaughnsoft.net/live" + }, + { + "name": "EU: Amsterdam, NL", + "url": "rtmp://live-ams.vaughnsoft.net/live" + }, + { + "name": "EU: London, UK", + "url": "rtmp://live-lhr.vaughnsoft.net/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 15000, + "audio_bitrate": 320 + } + ] + }, + { + "id": "breakers", + "name": "Breakers.TV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US: Chicago, IL", + "url": "rtmp://live-ord.vaughnsoft.net/live" + }, + { + "name": "US: Vint Hill, VA", + "url": "rtmp://live-iad.vaughnsoft.net/live" + }, + { + "name": "US: Denver, CO", + "url": "rtmp://live-den.vaughnsoft.net/live" + }, + { + "name": "US: New York, NY", + "url": "rtmp://live-nyc.vaughnsoft.net/live" + }, + { + "name": "US: Miami, FL", + "url": "rtmp://live-mia.vaughnsoft.net/live" + }, + { + "name": "US: Seattle, WA", + "url": "rtmp://live-sea.vaughnsoft.net/live" + }, + { + "name": "EU: Amsterdam, NL", + "url": "rtmp://live-ams.vaughnsoft.net/live" + }, + { + "name": "EU: London, UK", + "url": "rtmp://live-lhr.vaughnsoft.net/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 15000, + "audio_bitrate": 320 + } + ] + }, + { + "id": "facebook", + "name": "Facebook Live", + "stream_key_link": "https://www.facebook.com/live/producer?ref=OBS", + "available_protocols": ["RTMPS"], + "servers": [ + { + "protocol": "RTMPS", + "name": "Default", + "url": "rtmps://rtmp-api.facebook.com:443/rtmp/" + } + ], + "maximum": [ + { + "protocol": "RTMPS", + "video_bitrate": 6000, + "audio_bitrate": 128, + "fps": 30 + } + ], + "supported_resolutions": [ + "1280x720", + "852x480", + "480x360" + ] + }, + { + "id": "restream_io", + "name": "Restream.io", + "stream_key_link": "https://restream.io/settings/streaming-setup?from=OBS", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Autodetect", + "url": "rtmp://live.restream.io/live" + }, + { + "name": "EU-West (London, GB)", + "url": "rtmp://london.restream.io/live" + }, + { + "name": "EU-West (Amsterdam, NL)", + "url": "rtmp://amsterdam.restream.io/live" + }, + { + "name": "EU-West (Luxembourg)", + "url": "rtmp://luxembourg.restream.io/live" + }, + { + "name": "EU-West (Paris, FR)", + "url": "rtmp://paris.restream.io/live" + }, + { + "name": "EU-West (Milan, IT)", + "url": "rtmp://milan.restream.io/live" + }, + { + "name": "EU-Central (Frankfurt, DE)", + "url": "rtmp://frankfurt.restream.io/live" + }, + { + "name": "EU-East (Falkenstein, DE)", + "url": "rtmp://falkenstein.restream.io/live" + }, + { + "name": "EU-East (Prague, Czech)", + "url": "rtmp://prague.restream.io/live" + }, + { + "name": "EU-South (Madrid, Spain)", + "url": "rtmp://madrid.restream.io/live" + }, + { + "name": "Russia (Moscow)", + "url": "rtmp://moscow.restream.io/live" + }, + { + "name": "Turkey (Istanbul)", + "url": "rtmp://istanbul.restream.io/live" + }, + { + "name": "Israel (Tel Aviv)", + "url": "rtmp://telaviv.restream.io/live" + }, + { + "name": "US-West (Seattle, WA)", + "url": "rtmp://seattle.restream.io/live" + }, + { + "name": "US-West (San Jose, CA)", + "url": "rtmp://sanjose.restream.io/live" + }, + { + "name": "US-Central (Dallas, TX)", + "url": "rtmp://dallas.restream.io/live" + }, + { + "name": "US-East (Washington, DC)", + "url": "rtmp://washington.restream.io/live" + }, + { + "name": "US-East (Miami, FL)", + "url": "rtmp://miami.restream.io/live" + }, + { + "name": "US-East (Chicago, IL)", + "url": "rtmp://chicago.restream.io/live" + }, + { + "name": "NA-East (Toronto, Canada)", + "url": "rtmp://toronto.restream.io/live" + }, + { + "name": "SA (Saint Paul, Brazil)", + "url": "rtmp://saopaulo.restream.io/live" + }, + { + "name": "India (Bangalore)", + "url": "rtmp://bangalore.restream.io/live" + }, + { + "name": "Asia (Singapore)", + "url": "rtmp://singapore.restream.io/live" + }, + { + "name": "Asia (Seoul, South Korea)", + "url": "rtmp://seoul.restream.io/live" + }, + { + "name": "Asia (Tokyo, Japan)", + "url": "rtmp://tokyo.restream.io/live" + }, + { + "name": "Australia (Sydney)", + "url": "rtmp://sydney.restream.io/live" + } + ] + }, + { + "id": "nood", + "name": "Nood", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Global: Fastest (Recommended)", + "url": "rtmp://stream.nood.tv/live_source" + }, + { + "name": "NA East: Ashburn, VA, USA", + "url": "rtmp://us-east-1.stream.nood.tv/live_source" + }, + { + "name": "NA East: Columbus, OH, USA", + "url": "rtmp://us-east-2.stream.nood.tv/live_source" + }, + { + "name": "NA East: Montreal, QC, CAN", + "url": "rtmp://ca-central-1.stream.nood.tv/live_source" + }, + { + "name": "NA West: San Francisco, CA, USA", + "url": "rtmp://us-west-1.stream.nood.tv/live_source" + }, + { + "name": "NA West: Portland, OR, USA", + "url": "rtmp://us-west-2.stream.nood.tv/live_source" + }, + { + "name": "SA East: Sao Paulo, BRA", + "url": "rtmp://sa-east-1.stream.nood.tv/live_source" + }, + { + "name": "EU West: Dublin, IRL", + "url": "rtmp://eu-west-1.stream.nood.tv/live_source" + }, + { + "name": "EU West: London, GBR", + "url": "rtmp://eu-west-2.stream.nood.tv/live_source" + }, + { + "name": "EU West: Paris, FRA", + "url": "rtmp://eu-west-3.stream.nood.tv/live_source" + }, + { + "name": "EU West: Frankfurt, DEU", + "url": "rtmp://eu-central-1.stream.nood.tv/live_source" + }, + { + "name": "Asia North-East: Tokyo, JPN", + "url": "rtmp://ap-northeast-1.stream.nood.tv/live_source" + }, + { + "name": "Asia North-East: Seoul, KOR", + "url": "rtmp://ap-northeast-2.stream.nood.tv/live_source" + }, + { + "name": "Asia South-East: Singapore, SGP", + "url": "rtmp://ap-southeast-1.stream.nood.tv/live_source" + }, + { + "name": "Asia South-East: Sydney, AUS", + "url": "rtmp://ap-southeast-2.stream.nood.tv/live_source" + }, + { + "name": "Asia South: Mumbai, IND", + "url": "rtmp://ap-south-1.stream.nood.tv/live_source" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 25000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "castr_io", + "name": "Castr.io", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US-East (Chicago, IL)", + "url": "rtmp://cg.castr.io/static" + }, + { + "name": "US-East (New York, NY)", + "url": "rtmp://ny.castr.io/static" + }, + { + "name": "US-East (Miami, FL)", + "url": "rtmp://mi.castr.io/static" + }, + { + "name": "US-West (Seattle, WA)", + "url": "rtmp://se.castr.io/static" + }, + { + "name": "US-West (Los Angeles, CA)", + "url": "rtmp://la.castr.io/static" + }, + { + "name": "US-Central (Dallas, TX)", + "url": "rtmp://da.castr.io/static" + }, + { + "name": "NA-East (Toronto, CA)", + "url": "rtmp://qc.castr.io/static" + }, + { + "name": "SA (Sao Paulo, BR)", + "url": "rtmp://br.castr.io/static" + }, + { + "name": "EU-West (London, UK)", + "url": "rtmp://uk.castr.io/static" + }, + { + "name": "EU-Central (Frankfurt, DE)", + "url": "rtmp://fr.castr.io/static" + }, + { + "name": "Russia (Moscow)", + "url": "rtmp://ru.castr.io/static" + }, + { + "name": "Asia (Singapore)", + "url": "rtmp://sg.castr.io/static" + }, + { + "name": "Asia (India)", + "url": "rtmp://in.castr.io/static" + }, + { + "name": "Australia (Sydney)", + "url": "rtmp://au.castr.io/static" + }, + { + "name": "US Central", + "url": "rtmp://us-central.castr.io/static" + }, + { + "name": "US West", + "url": "rtmp://us-west.castr.io/static" + }, + { + "name": "US East", + "url": "rtmp://us-east.castr.io/static" + }, + { + "name": "US South", + "url": "rtmp://us-south.castr.io/static" + }, + { + "name": "South America", + "url": "rtmp://south-am.castr.io/static" + }, + { + "name": "EU Central", + "url": "rtmp://eu-central.castr.io/static" + }, + { + "name": "Singapore", + "url": "rtmp://sg-central.castr.io/static" + } + ] + }, + { + "id": "boomstream", + "name": "Boomstream", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://live.boomstream.com/live" + } + ] + }, + { + "id": "meridix", + "name": "Meridix Live Sports Platform", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://publish.meridix.com/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3500 + } + ] + }, + { + "id": "afreeca", + "name": "AfreecaTV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Asia : Korea", + "url": "rtmp://rtmpmanager-freecat.afreeca.tv/app" + }, + { + "name": "North America : US East", + "url": "rtmp://rtmp-esu.afreecatv.com/app" + }, + { + "name": "North America : US West", + "url": "rtmp://rtmp-wsu.afreecatv.com/app" + }, + { + "name": "Europe : UK", + "url": "rtmp://rtmp-uk.afreecatv.com/app" + }, + { + "name": "Asia : Singapore", + "url": "rtmp://rtmp-sgp.afreecatv.com/app" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 8000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "cam4", + "name": "CAM4", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "CAM4", + "url": "rtmp://origin.cam4.com/cam4-origin-live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "eplay", + "name": "ePlay", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "ePlay Primary", + "url": "rtmp://live.eplay.link/origin" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 7500, + "audio_bitrate": 192 + } + ] + }, + { + "id": "picarto", + "name": "Picarto", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US East (Chicago, USA)", + "url": "rtmp://live.us-east1.picarto.tv/golive" + }, + { + "name": "US West (Los Angeles, USA)", + "url": "rtmp://live.us-west1.picarto.tv/golive" + }, + { + "name": "EU West (Düsseldorf, Germany)", + "url": "rtmp://live.eu-west1.picarto.tv/golive" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3500 + } + ] + }, + { + "id": "pandora_tv", + "name": "Pandora TV Korea", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://plive.pandora.tv:80/mediaHub" + } + ] + }, + { + "id": "livestream", + "name": "Livestream", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://rtmpin.livestreamingest.com/rtmpin" + } + ] + }, + { + "id": "uscreen", + "name": "Uscreen", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://global-live.uscreen.app:5222/app" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 8000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "stripchat", + "name": "Stripchat", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Auto", + "url": "rtmp://s-sd.stripst.com/ext" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "camsoda", + "name": "CamSoda", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "North America", + "url": "rtmp://obs-ingest-na.camsoda.com/cam_obs" + }, + { + "name": "South America", + "url": "rtmp://obs-ingest-sa.camsoda.com/cam_obs" + }, + { + "name": "Asia", + "url": "rtmp://obs-ingest-as.camsoda.com/cam_obs" + }, + { + "name": "Europe", + "url": "rtmp://obs-ingest-eu.camsoda.com/cam_obs" + }, + { + "name": "Oceania", + "url": "rtmp://obs-ingest-oc.camsoda.com/cam_obs" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 160, + "fps": 30 + } + ], + "supported_resolutions": [ + "1920x1080", + "1280x720", + "852x480", + "480x360" + ] + }, + { + "id": "chaturbate", + "name": "Chaturbate", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Global Main Fastest - Recommended", + "url": "rtmp://live.stream.highwebmedia.com/live-origin" + }, + { + "name": "Global Backup", + "url": "rtmp://live-backup.stream.highwebmedia.com/live-origin" + }, + { + "name": "US West: Seattle, WA", + "url": "rtmp://live-sea.stream.highwebmedia.com/live-origin" + }, + { + "name": "US West: Phoenix, AZ", + "url": "rtmp://live-phx.stream.highwebmedia.com/live-origin" + }, + { + "name": "US Central: Salt Lake City, UT", + "url": "rtmp://live-slc.stream.highwebmedia.com/live-origin" + }, + { + "name": "US Central: Chicago, IL", + "url": "rtmp://live-chi.stream.highwebmedia.com/live-origin" + }, + { + "name": "US East: Atlanta, GA", + "url": "rtmp://live-atl.stream.highwebmedia.com/live-origin" + }, + { + "name": "US East: Ashburn, VA", + "url": "rtmp://live-ash.stream.highwebmedia.com/live-origin" + }, + { + "name": "South America: Sao Paulo, Brazil", + "url": "rtmp://live-gru.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Amsterdam, NL", + "url": "rtmp://live-nld.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Alblasserdam, NL", + "url": "rtmp://live-alb.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Frankfurt, DE", + "url": "rtmp://live-fra.stream.highwebmedia.com/live-origin" + }, + { + "name": "EU: Belgrade, Serbia", + "url": "rtmp://live-srb.stream.highwebmedia.com/live-origin" + }, + { + "name": "Asia: Singapore", + "url": "rtmp://live-sin.stream.highwebmedia.com/live-origin" + }, + { + "name": "Asia: Tokyo, Japan", + "url": "rtmp://live-nrt.stream.highwebmedia.com/live-origin" + }, + { + "name": "Australia: Sydney", + "url": "rtmp://live-syd.stream.highwebmedia.com/live-origin" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 50000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "twitter", + "name": "Twitter", + "stream_key_link": "https://studio.twitter.com/producer/sources", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "US West: California", + "url": "rtmp://ca.pscp.tv:80/x" + }, + { + "name": "US West: Oregon", + "url": "rtmp://or.pscp.tv:80/x" + }, + { + "name": "US East: Virginia", + "url": "rtmp://va.pscp.tv:80/x" + }, + { + "name": "South America: Brazil", + "url": "rtmp://br.pscp.tv:80/x" + }, + { + "name": "EU West: France", + "url": "rtmp://fr.pscp.tv:80/x" + }, + { + "name": "EU West: Ireland", + "url": "rtmp://ie.pscp.tv:80/x" + }, + { + "name": "EU Central: Germany", + "url": "rtmp://de.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Australia", + "url": "rtmp://au.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: India", + "url": "rtmp://in.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Japan", + "url": "rtmp://jp.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Korea", + "url": "rtmp://kr.pscp.tv:80/x" + }, + { + "name": "Asia/Pacific: Singapore", + "url": "rtmp://sg.pscp.tv:80/x" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 12000, + "audio_bitrate": 128, + "fps": 60 + } + ] + }, + { + "id": "switcherboard", + "name": "Switchboard Live", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Global Zone (geo based)", + "url": "rtmp://ingest-global-a.switchboard.zone/live" + }, + { + "name": "US Zone (geo based)", + "url": "rtmp://ingest-us.switchboard.zone/live" + }, + { + "name": "US West 1 (South)", + "url": "rtmp://ingest-us-west.a.switchboard.zone/live" + }, + { + "name": "US West 2 (North)", + "url": "rtmp://ingest-us-west.b.switchboard.zone/live" + }, + { + "name": "US East 1 (North)", + "url": "rtmp://ingest-us-east.a.switchboard.zone/live" + }, + { + "name": "US East 2 (South)", + "url": "rtmp://ingest-us-east.b.switchboard.zone/live" + }, + { + "name": "US Central (North)", + "url": "rtmp://ingest-us-central.a.switchboard.zone/live" + }, + { + "name": "South America East (São Paulo, BR)", + "url": "rtmp://ingest-sa-east.a.switchboard.zone/live" + }, + { + "name": "Europe West (London, UK)", + "url": "rtmp://ingest-eu-west.a.switchboard.zone/live" + }, + { + "name": "Europe North (Hamina, FI)", + "url": "rtmp://ingest-eu-north.a.switchboard.zone/live" + }, + { + "name": "Australia Southeast (Sydney, AU)", + "url": "rtmp://ingest-au-southeast.a.switchboard.zone/live" + }, + { + "name": "Asia East (Changhua County, TW)", + "url": "rtmp://ingest-as-east.a.switchboard.zone/live" + }, + { + "name": "Asia Northeast (Tokyo, JP)", + "url": "rtmp://ingest-as-northeast.a.switchboard.zone/live" + }, + { + "name": "Asia South (Mumbai, IN)", + "url": "rtmp://ingest-as-south.a.switchboard.zone/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 10000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "looch", + "name": "Looch", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary Looch ingest server", + "url": "rtmp://ingest.looch.tv/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "eventials", + "name": "Eventials", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://live.eventials.com/eventialsLiveOrigin" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 900, + "audio_bitrate": 192 + } + ] + }, + { + "id": "eventlive", + "name": "EventLive.pro", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://go.eventlive.pro/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3000, + "audio_bitrate": 192, + "fps": 30 + } + ], + "supported_resolutions": [ + "1920x1080", + "1280x720" + ] + }, + { + "id": "lahzenegar", + "name": "Lahzenegar - StreamG | لحظه‌نگار - استریمجی", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://rtmp.lahzecdn.com/pro" + }, + { + "name": "Iran", + "url": "rtmp://rtmp-iran.lahzecdn.com/pro" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 4000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "mylive", + "name": "MyLive", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://stream.mylive.in.th/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 7000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "trovo", + "name": "Trovo", + "stream_key_link": "https://studio.trovo.live/mychannel/stream", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://livepush.trovo.live/live/" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 9000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "mixcloud", + "name": "Mixcloud", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.mixcloud.com/broadcast" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 320, + "fps": 30 + } + ], + "supported_resolutions": [ + "1280x720", + "852x480", + "480x360" + ] + }, + { + "id": "sermonaudio", + "name": "SermonAudio Cloud", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Primary", + "url": "rtmp://webcast.sermonaudio.com/sa" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 2000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "vimeo", + "name": "Vimeo", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.cloud.vimeo.com/live" + } + ] + }, + { + "id": "aparat", + "name": "Aparat", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.cdn.asset.aparat.com:443/event" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 320 + } + ] + }, + { + "id": "gametips", + "name": "GameTips.TV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Iran - Tehran | AsiaTech", + "url": "rtmp://rtmp.s2.gametips.tv:1935/live" + }, + { + "name": "Netherlands - Amsterdam | Serverius", + "url": "rtmp://rtmp.s3.gametips.tv:1935/live" + }, + { + "name": "Iran - Tehran | ParsOnline", + "url": "rtmp://rtmp.s4.gametips.tv:1935/live" + }, + { + "name": "Iran - Tehran | AfraNet", + "url": "rtmp://rtmp.s5.gametips.tv:1935/live" + } + ] + }, + { + "id": "kakao", + "name": "KakaoTV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.play.kakao.com/kakaotv" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 8000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "piczel_tv", + "name": "Piczel.tv", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://piczel.tv:1935/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 2500, + "audio_bitrate": 256 + } + ] + }, + { + "id": "stage_ten", + "name": "STAGE TEN", + "available_protocols": ["RTMPS"], + "servers": [ + { + "protocol": "RTMPS", + "name": "STAGE TEN", + "url": "rtmps://app-rtmp.stageten.tv:443/stageten" + } + ], + "maximum": [ + { + "protocol": "RTMPS", + "video_bitrate": 4000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "dlive", + "name": "DLive", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://stream.dlive.tv/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "lightcast", + "name": "Lightcast.com", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "North America / East", + "url": "rtmp://us-east.live.lightcast.com/202E1F/default" + }, + { + "name": "North America / West", + "url": "rtmp://us-west.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Amsterdam", + "url": "rtmp://europe.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Frankfurt", + "url": "rtmp://europe-fra.live.lightcast.com/202E1F/default" + }, + { + "name": "Europe / Stockholm", + "url": "rtmp://europe-sto.live.lightcast.com/202E1F/default" + }, + { + "name": "Asia / Hong Kong", + "url": "rtmp://asia.live.lightcast.com/202E1F/default" + }, + { + "name": "Australia / Sydney", + "url": "rtmp://australia.live.lightcast.com/202E1F/default" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "bongacams", + "name": "Bongacams", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Automatic / Default", + "url": "rtmp://auto.origin.gnsbc.com:1934/live" + }, + { + "name": "Automatic / Backup", + "url": "rtmp://origin.bcvidorigin.com:1934/live" + }, + { + "name": "Europe", + "url": "rtmp://z-eu.origin.gnsbc.com:1934/live" + }, + { + "name": "North America", + "url": "rtmp://z-us.origin.gnsbc.com:1934/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "showit_tv", + "name": "show-it.tv", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://stream-1.show-it.tv:1935/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 6000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "chathostness", + "name": "Chathostess", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Chathostess - Default", + "url": "rtmp://wowza01.foobarweb.com/cmschatsys_video" + }, + { + "name": "Chathostess - Backup", + "url": "rtmp://wowza05.foobarweb.com/cmschatsys_video" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3600, + "audio_bitrate": 128 + } + ] + }, + { + "id": "camplace", + "name": "Camplace", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Camplace - Default", + "url": "rtmp://rtmp.camplace.com" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "onlyfans", + "name": "OnlyFans.com", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "USA", + "url": "rtmp://route0.onlyfans.com/live" + }, + { + "name": "Europe", + "url": "rtmp://route0-dc2.onlyfans.com/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 2500, + "audio_bitrate": 192 + } + ] + }, + { + "id": "steam", + "name": "Steam", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://ingest-rtmp.broadcast.steamcontent.com/app" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 7000, + "audio_bitrate": 128 + } + ] + }, + { + "id": "stars_avn", + "name": "Stars.AVN.com", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://alpha.gateway.stars.avn.com/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 2500, + "audio_bitrate": 192 + } + ] + }, + { + "id": "konduit", + "name": "Konduit.live", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.konduit.live/live" + } + ] + }, + { + "id": "uncanny", + "name": "Uncanny.gg", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://stream.uncanny.gg/fortnite" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 10000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "whalebone", + "name": "Whalebone.tv", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Automatic", + "url": "rtmp://live.whalebone.tv/live" + }, + { + "name": "Tokyo, Japan", + "url": "rtmp://ap-northeast.live.whalebone.tv/live" + }, + { + "name": "Frankfurt, Germany", + "url": "rtmp://eu-central.live.whalebone.tv/live" + }, + { + "name": "London, United Kingdom", + "url": "rtmp://eu-west.live.whalebone.tv/live" + }, + { + "name": "São Paulo, Brazil", + "url": "rtmp://sa-east.live.whalebone.tv/live" + }, + { + "name": "North Virgina, United States", + "url": "rtmp://us-east.live.whalebone.tv/live" + }, + { + "name": "Oregon, United States", + "url": "rtmp://us-west.live.whalebone.tv/live" + } + ] + }, + { + "id": "loco", + "name": "LOCO", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://ivory-ingest.getloconow.com:1935/stream" + } + ] + }, + { + "id": "niconico-premium", + "name": "niconico, premium member (ニコニコ生放送 プレミアム会員)", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://aliveorigin.dmc.nico/named_input" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 5808, + "audio_bitrate": 192 + } + ] + }, + { + "id": "niconico-free", + "name": "niconico, free member (ニコニコ生放送 一般会員)", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://aliveorigin.dmc.nico/named_input" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 904, + "audio_bitrate": 96 + } + ] + }, + { + "id": "wasd_tv", + "name": "WASD.TV", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Automatic", + "url": "rtmp://push.rtmp.wasd.tv/live" + }, + { + "name": "Russia, Moscow", + "url": "rtmp://ru-moscow.rtmp.wasd.tv/live" + }, + { + "name": "Germany, Frankfurt", + "url": "rtmp://de-frankfurt.rtmp.wasd.tv/live" + }, + { + "name": "Finland, Helsinki", + "url": "rtmp://fi-helsinki.rtmp.wasd.tv/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 10000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "virtwish", + "name": "VirtWish", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://rtmp.virtwish.com/live" + } + ] + }, + { + "id": "xlovecam", + "name": "XLoveCam.com", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Europe(main)", + "url": "rtmp://nl.eu.stream.xlove.com/performer-origin" + }, + { + "name": "Europe(Romania)", + "url": "rtmp://ro.eu.stream.xlove.com/performer-origin" + }, + { + "name": "Europe(Russia)", + "url": "rtmp://ru.eu.stream.xlove.com/performer-origin" + }, + { + "name": "North America(US East)", + "url": "rtmp://usec.na.stream.xlove.com/performer-origin" + }, + { + "name": "North America(US West)", + "url": "rtmp://uswc.na.stream.xlove.com/performer-origin" + }, + { + "name": "North America(Canada)", + "url": "rtmp://ca.na.stream.xlove.com/performer-origin" + }, + { + "name": "South America", + "url": "rtmp://co.sa.stream.xlove.com/performer-origin" + }, + { + "name": "Asia", + "url": "rtmp://sg.as.stream.xlove.com/performer-origin" + } + ] + }, + { + "id": "angelthump", + "name": "AngelThump", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Auto", + "url": "rtmp://ingest.angelthump.com/live" + }, + { + "name": "New York 3", + "url": "rtmp://nyc-ingest.angelthump.com:1935/live" + }, + { + "name": "San Francisco 2", + "url": "rtmp://sfo-ingest.angelthump.com:1935/live" + }, + { + "name": "Singapore 1", + "url": "rtmp://sgp-ingest.angelthump.com:1935/live" + }, + { + "name": "London 1", + "url": "rtmp://lon-ingest.angelthump.com:1935/live" + }, + { + "name": "Frankfurt 1", + "url": "rtmp://fra-ingest.angelthump.com:1935/live" + }, + { + "name": "Toronto 1", + "url": "rtmp://tor-ingest.angelthump.com:1935/live" + }, + { + "name": "Bangalore 1", + "url": "rtmp://blr-ingest.angelthump.com:1935/live" + }, + { + "name": "Amsterdam 3", + "url": "rtmp://ams-ingest.angelthump.com:1935/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 3500, + "audio_bitrate": 160 + } + ] + }, + { + "id": "apachat", + "name": "Taryana - Apachat | تاریانا - آپاچت", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Global: Fastest (Recommended)", + "url": "rtmp://cdn.apachat.com:443/multistream" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 4000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "api_video", + "name": "api.video", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://broadcast.api.video/s" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 20000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "mux", + "name": "Mux", + "available_protocols": ["RTMPS", "RTMP"], + "servers": [ + { + "protocol": "RTMPS", + "name": "Global (RTMPS)", + "url": "rtmps://global-live.mux.com:443/app" + }, + { + "name": "Global (RTMP)", + "url": "rtmp://global-live.mux.com:5222/app" + } + ], + "maximum": [ + { + "protocol": "RTMPS", + "video_bitrate": 5000, + "audio_bitrate": 160 + }, + { + "protocol": "RTMP", + "video_bitrate": 5000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "viloud", + "name": "Viloud", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://live.viloud.tv:5222/app" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 5000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "myfreecams", + "name": "MyFreeCams", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Automatic", + "url": "rtmp://publish.myfreecams.com/NxServer" + }, + { + "name": "Australia", + "url": "rtmp://publish-syd.myfreecams.com/NxServer" + }, + { + "name": "East Asia", + "url": "rtmp://publish-tyo.myfreecams.com/NxServer" + }, + { + "name": "Europe (East)", + "url": "rtmp://publish-buh.myfreecams.com/NxServer" + }, + { + "name": "Europe (West)", + "url": "rtmp://publish-ams.myfreecams.com/NxServer" + }, + { + "name": "North America (East Coast)", + "url": "rtmp://publish-ord.myfreecams.com/NxServer" + }, + { + "name": "North America (West Coast)", + "url": "rtmp://publish-tuk.myfreecams.com/NxServer" + }, + { + "name": "South America", + "url": "rtmp://publish-sao.myfreecams.com/NxServer" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 10000, + "audio_bitrate": 192, + "fps": 60 + } + ] + }, + { + "id": "polystreamer", + "name": "PolyStreamer.com", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Auto-select closest server", + "url": "rtmp://live.polystreamer.com/live" + }, + { + "name": "United States - West", + "url": "rtmp://us-west.live.polystreamer.com/live" + }, + { + "name": "United States - East", + "url": "rtmp://us-east.live.polystreamer.com/live" + }, + { + "name": "Australia", + "url": "rtmp://aus.live.polystreamer.com/live" + }, + { + "name": "India", + "url": "rtmp://ind.live.polystreamer.com/live" + }, + { + "name": "Germany", + "url": "rtmp://deu.live.polystreamer.com/live" + }, + { + "name": "Japan", + "url": "rtmp://jpn.live.polystreamer.com/live" + }, + { + "name": "Singapore", + "url": "rtmp://sgp.live.polystreamer.com/live" + } + ] + }, + { + "id": "glimesh", + "name": "Glimesh", + "stream_key_link": "https://glimesh.tv/users/settings/stream", + "available_protocols": ["FTL"], + "servers": [ + { + "protocol": "FTL", + "name": "North America - Chicago, United States", + "url": "ingest.kord.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "North America - New York, United States", + "url": "ingest.kjfk.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "North America - San Francisco, United States", + "url": "ingest.ksfo.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "North America - Toronto, Canada", + "url": "ingest.cyyz.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "Europe - Amsterdam, Netherlands", + "url": "ingest.eham.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "Europe - Frankfurt, Germany", + "url": "ingest.eddf.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "Europe - London, United Kingdom", + "url": "ingest.egll.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "Asia - Bangalore, India", + "url": "ingest.vobl.live.glimesh.tv" + }, + { + "protocol": "FTL", + "name": "Asia - Singapore", + "url": "ingest.wsss.live.glimesh.tv" + } + ], + "maximum": [ + { + "protocol": "FTL", + "video_bitrate": 6000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "openrec", + "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", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "Default", + "url": "rtmp://a.station.openrec.tv:1935/live1" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 5000, + "audio_bitrate": 160 + } + ] + }, + { + "id": "nanistream", + "name": "nanoStream Cloud / bintu", + "available_protocols": ["RTMPS", "RTMP"], + "servers": [ + { + "name": "bintu-stream global ingest (rtmp)", + "url": "rtmp://bintu-stream.nanocosmos.de/live" + }, + { + "protocol": "RTMPS", + "name": "bintu-stream global ingest (rtmps)", + "url": "rtmps://bintu-stream.nanocosmos.de:1937/live" + }, + { + "name": "bintu-vtrans global ingest with transcoding/ABR (rtmp)", + "url": "rtmp://bintu-stream.nanocosmos.de/live" + }, + { + "protocol": "RTMPS", + "name": "bintu-vtrans global ingest with transcoding/ABR (rtmps)", + "url": "rtmps://bintu-stream.nanocosmos.de:1937/live" + }, + { + "name": "bintu-stream Europe (EU)", + "url": "rtmp://bintu-stream-eu.nanocosmos.de/live" + }, + { + "name": "bintu-stream USA West (USW)", + "url": "rtmp://bintu-stream-usw.nanocosmos.de/live" + }, + { + "name": "bintu-stream US East (USE)", + "url": "rtmp://bintu-stream-use.nanocosmos.de/live" + }, + { + "name": "bintu-stream Asia South (ASS)", + "url": "rtmp://bintu-stream-ass.nanocosmos.de/live" + }, + { + "name": "bintu-stream Australia (AU)", + "url": "rtmp://bintu-stream-au.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Europe (EU)", + "url": "rtmp://bintu-vtrans-eu.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans USA West (USW)", + "url": "rtmp://bintu-vtrans-usw.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans US East (USE)", + "url": "rtmp://bintu-vtrans-use.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Asia South (ASS)", + "url": "rtmp://bintu-vtrans-ass.nanocosmos.de/live" + }, + { + "name": "bintu-vtrans Australia (AU)", + "url": "rtmp://bintu-vtrans-au.nanocosmos.de/live" + } + ], + "maximum": [ + { + "protocol": "RTMPS", + "video_bitrate": 5000, + "audio_bitrate": 192 + }, + { + "protocol": "RTMP", + "video_bitrate": 5000, + "audio_bitrate": 192 + } + ] + }, + { + "id": "brime_live", + "name": "Brime Live", + "stream_key_link": "https://brimelive.com/obs-stream-key-link", + "available_protocols": ["RTMP"], + "servers": [ + { + "name": "North America - Ashburn, VA", + "url": "rtmp://ingest-us-ashburn.brimelive.com/live" + }, + { + "name": "North America - San Jose, CA", + "url": "rtmp://ingest-us-sanjose.brimelive.com/live" + }, + { + "name": "North America - Atlanta, GA", + "url": "rtmp://ingest-us-atlanta.brimelive.com/live" + }, + { + "name": "North America - Dallas, TX", + "url": "rtmp://ingest-us-dallas.brimelive.com/live" + }, + { + "name": "North America - Chicago, IL", + "url": "rtmp://ingest-us-chicago.brimelive.com/live" + }, + { + "name": "Canada Southeast - Montreal", + "url": "rtmp://ingest-ca-montreal.brimelive.com/live" + }, + { + "name": "Latin America - Brazil East (Sao Paulo)", + "url": "rtmp://ingest-la-saopaulo.brimelive.com/live" + }, + { + "name": "Europe / EMEA - Germany (Frankfurt)", + "url": "rtmp://ingest-eu-frankfurt.brimelive.com/live" + }, + { + "name": "Europe / EMEA - UK South (London)", + "url": "rtmp://ingest-eu-london.brimelive.com/live" + }, + { + "name": "Europe / EMEA - Russia (Moscow)", + "url": "rtmp://ingest-eu-moscow.brimelive.com/live" + }, + { + "name": "APAC - Japan East (Tokyo)", + "url": "rtmp://ingest-apac-tokyo.brimelive.com/live" + }, + { + "name": "APAC - Australia East (Sydney)", + "url": "rtmp://ingest-apac-sydney.brimelive.com/live" + } + ], + "maximum": [ + { + "protocol": "RTMP", + "video_bitrate": 20000, + "audio_bitrate": 320 + } + ] + } + ] +} diff --git a/plugins/obs-services/json-format-ver.hpp b/plugins/obs-services/json-format-ver.hpp new file mode 100644 index 00000000000000..ba6f1f9610dd2e --- /dev/null +++ b/plugins/obs-services/json-format-ver.hpp @@ -0,0 +1,3 @@ +#pragma once + +#define SERVICES_FORMAT_VERSION 4 diff --git a/plugins/obs-services/plugin-main.cpp b/plugins/obs-services/plugin-main.cpp new file mode 100644 index 00000000000000..6910e0ef073f10 --- /dev/null +++ b/plugins/obs-services/plugin-main.cpp @@ -0,0 +1,21 @@ +#include + +#include "service-manager.hpp" + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-services", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "OBS Core Stream Services"; +} + +bool obs_module_load() +{ + service_manager::initialize(); + return true; +} + +void obs_module_unload() +{ + service_manager::finalize(); +} diff --git a/plugins/obs-services/plugin.hpp b/plugins/obs-services/plugin.hpp new file mode 100644 index 00000000000000..54946ca03dab0b --- /dev/null +++ b/plugins/obs-services/plugin.hpp @@ -0,0 +1,7 @@ +#pragma once + +extern "C" { +#include +} + +#define blog(level, msg, ...) blog(level, "[obs-services] " msg, ##__VA_ARGS__) diff --git a/plugins/obs-services/protocols.hpp.in b/plugins/obs-services/protocols.hpp.in new file mode 100644 index 00000000000000..a74e729f3b35c6 --- /dev/null +++ b/plugins/obs-services/protocols.hpp.in @@ -0,0 +1,12 @@ +#pragma once + +#ifndef TRUE +#define TRUE 1 +#endif + +#ifndef FALSE +#define FALSE 0 +#endif + +#define RTMPS_DISABLED @RTMPS_DISABLED@ +#define FTL_DISABLED @FTL_DISABLED@ diff --git a/plugins/obs-services/service-factory.cpp b/plugins/obs-services/service-factory.cpp new file mode 100644 index 00000000000000..7cd519b8978894 --- /dev/null +++ b/plugins/obs-services/service-factory.cpp @@ -0,0 +1,670 @@ +#include "service-factory.hpp" + +#include "protocols.hpp" +#include "plugin.hpp" +#include "service-instance.hpp" + +extern "C" { +#include +} + +// XXX: Add the server list for each protocols +void service_factory::create_server_lists(obs_properties_t *props) +{ + obs_property_t *rtmp, *hls; + rtmp = obs_properties_add_list(props, "server_rtmp", + obs_module_text("Server"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); +#if !RTMPS_DISABLED + obs_property_t *rtmps = obs_properties_add_list( + props, "server_rtmps", obs_module_text("Server"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); +#endif + hls = obs_properties_add_list(props, "server_hls", + obs_module_text("Server"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); +#if !FTL_DISABLED + obs_property_t *ftl = obs_properties_add_list(props, "server_ftl", + obs_module_text("Server"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); +#endif + + for (size_t idx = 0; idx < servers.size(); idx++) { + if (strcmp(servers[idx].protocol, "RTMP") == 0) + obs_property_list_add_string( + rtmp, obs_module_text(servers[idx].name), + servers[idx].url); + +#if !RTMPS_DISABLED + if (strcmp(servers[idx].protocol, "RTMPS") == 0) + obs_property_list_add_string( + rtmps, obs_module_text(servers[idx].name), + servers[idx].url); +#endif + + if (strcmp(servers[idx].protocol, "HLS") == 0) + obs_property_list_add_string( + hls, obs_module_text(servers[idx].name), + servers[idx].url); + +#if !FTL_DISABLED + if (strcmp(servers[idx].protocol, "FTL") == 0) + obs_property_list_add_string( + ftl, obs_module_text(servers[idx].name), + servers[idx].url); +#endif + } +} + +/* ----------------------------------------------------------------- */ + +// XXX: Add maximum default settings for each protocols +void service_factory::add_maximum_defaults(obs_data_t *settings) +{ + obs_data_set_default_int(settings, "max_video_bitrate_rtmp", + maximum["RTMP"].video_bitrate); + obs_data_set_default_int(settings, "max_audio_bitrate_rtmp", + maximum["RTMP"].audio_bitrate); + obs_data_set_default_int(settings, "max_fps_rtmp", maximum["RTMP"].fps); + +#if !RTMPS_DISABLED + obs_data_set_default_int(settings, "max_video_bitrate_rtmps", + maximum["RTMPS"].video_bitrate); + obs_data_set_default_int(settings, "max_audio_bitrate_rtmps", + maximum["RTMPS"].audio_bitrate); + obs_data_set_default_int(settings, "max_fps_rtmps", + maximum["RTMPS"].fps); +#endif + + obs_data_set_default_int(settings, "max_video_bitrate_hls", + maximum["HLS"].video_bitrate); + obs_data_set_default_int(settings, "max_audio_bitrate_hls", + maximum["HLS"].audio_bitrate); + obs_data_set_default_int(settings, "max_fps_hls", maximum["HLS"].fps); + +#if !FTL_DISABLED + obs_data_set_default_int(settings, "max_video_bitrate_ftl", + maximum["FTL"].video_bitrate); + obs_data_set_default_int(settings, "max_audio_bitrate_ftl", + maximum["FTL"].audio_bitrate); + obs_data_set_default_int(settings, "max_fps_ftl", maximum["FTL"].fps); +#endif + + obs_data_set_default_bool(settings, "ignore_maximum", false); +} + +// XXX: Add maximum settings for each protocols +void service_factory::add_maximum_infos(obs_properties_t *props) +{ + obs_properties_add_info_bitrate(props, "max_video_bitrate_rtmp", + obs_module_text("MaxVideoBitrate")); + obs_properties_add_info_bitrate(props, "max_audio_bitrate_rtmp", + obs_module_text("MaxAudioBitrate")); + obs_properties_add_info_fps(props, "max_fps_rtmp", + obs_module_text("MaxFPS")); + +#if !RTMPS_DISABLED + obs_properties_add_info_bitrate(props, "max_video_bitrate_rtmps", + obs_module_text("MaxVideoBitrate")); + obs_properties_add_info_bitrate(props, "max_audio_bitrate_rtmps", + obs_module_text("MaxAudioBitrate")); + obs_properties_add_info_fps(props, "max_fps_rtmps", + obs_module_text("MaxFPS")); +#endif + + obs_properties_add_info_bitrate(props, "max_video_bitrate_hls", + obs_module_text("MaxVideoBitrate")); + obs_properties_add_info_bitrate(props, "max_audio_bitrate_hls", + obs_module_text("MaxAudioBitrate")); + obs_properties_add_info_fps(props, "max_fps_hls", + obs_module_text("MaxFPS")); + +#if !FTL_DISABLED + obs_properties_add_info_bitrate(props, "max_video_bitrate_ftl", + obs_module_text("MaxVideoBitrate")); + obs_properties_add_info_bitrate(props, "max_audio_bitrate_ftl", + obs_module_text("MaxAudioBitrate")); + obs_properties_add_info_fps(props, "max_fps_ftl", + obs_module_text("MaxFPS")); +#endif + + obs_properties_add_bool(props, "ignore_maximum", + obs_module_text("IgnoreMaximum")); +} + +/* ----------------------------------------------------------------- */ + +static inline int get_int_val(json_t *service, const char *key) +{ + json_t *integer_val = json_object_get(service, key); + if (!integer_val || !json_is_integer(integer_val)) + return -1; + + return (int)json_integer_value(integer_val); +} + +static inline const char *get_string_val(json_t *service, const char *key) +{ + json_t *str_val = json_object_get(service, key); + if (!str_val || !json_is_string(str_val)) + return NULL; + + return json_string_value(str_val); +} + +const char *service_factory::_get_name(void *type_data) noexcept +try { + if (type_data) + return reinterpret_cast(type_data) + ->get_name(); + return nullptr; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +void *service_factory::_create(obs_data_t *settings, + obs_service_t *service) noexcept +try { + service_factory *fac = reinterpret_cast( + obs_service_get_type_data(service)); + return fac->create(settings, service); +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +void service_factory::_destroy(void *data) noexcept +try { + if (data) + delete reinterpret_cast(data); +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); +} + +void service_factory::_update(void *data, obs_data_t *settings) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) { + priv->update(settings); + } + +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); +} + +void service_factory::_get_defaults2(obs_data_t *settings, + void *type_data) noexcept +try { + if (type_data) + reinterpret_cast(type_data)->get_defaults2( + settings); +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); +} + +obs_properties_t *service_factory::_get_properties2(void *data, + void *type_data) noexcept +try { + if (type_data) + return reinterpret_cast(type_data) + ->get_properties2(data); + return nullptr; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +const char *service_factory::_get_protocol(void *data) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) + return priv->get_protocol(); + return nullptr; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +const char *service_factory::_get_url(void *data) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) + return priv->get_url(); + return nullptr; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +const char *service_factory::_get_key(void *data) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) + return priv->get_key(); + return nullptr; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return nullptr; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return nullptr; +} + +void service_factory::_get_max_fps(void *data, int *fps) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) + priv->get_max_fps(fps); +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); +} + +void service_factory::_get_max_bitrate(void *data, int *video, + int *audio) noexcept +try { + service_instance *priv = reinterpret_cast(data); + if (priv) + priv->get_max_bitrate(video, audio); +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); +} + +/* ----------------------------------------------------------------- */ + +service_factory::service_factory(json_t *service) +{ + // XXX: Initialize maximum settings for each protocols + // Clang disabled to keep it readable a a list + /* clang-format off */ + maximum.insert({ + {"RTMP", {}}, +#if !RTMPS_DISABLED + {"RTMPS", {}}, +#endif + {"HLS", {}}, +#if !FTL_DISABLED + {"FTL", {}}, +#endif + }); + /* clang-format on */ + + /** JSON data extraction **/ + + _id = get_string_val(service, "id"); + _name = get_string_val(service, "name"); + + const char *info_link = get_string_val(service, "more_info_link"); + if (info_link != NULL) + more_info_link = info_link; + + const char *key_link = get_string_val(service, "stream_key_link"); + if (key_link != NULL) + stream_key_link = key_link; + + json_t *object; + size_t idx; + json_t *element; + + /* Available protocols extraction */ + + object = json_object_get(service, "available_protocols"); + //If not provided set only RTMP by default + if (!object) + protocols.push_back("RTMP"); + else { + json_incref(object); + + json_array_foreach (object, idx, element) { + const char *prtcl = json_string_value(element); + if (prtcl != NULL) + protocols.push_back(prtcl); + } + + json_decref(object); + } + + /* Servers extraction */ + + object = json_object_get(service, "servers"); + if (object) { + json_incref(object); + + json_array_foreach (object, idx, element) { + const char *prtcl = get_string_val(element, "protocol"); + const char *url = get_string_val(element, "url"); + const char *name = get_string_val(element, "name"); + + if ((url != NULL) && (name != NULL)) { + //If not provided set RTMP by default + if (prtcl != NULL) + servers.push_back({strdup(prtcl), + strdup(url), + strdup(name)}); + else + servers.push_back({"RTMP", strdup(url), + strdup(name)}); + } + } + + json_decref(object); + } + + /* Maximum output settings extraction */ + + object = json_object_get(service, "maximum"); + if (object) { + json_incref(object); + + json_array_foreach (object, idx, element) { + const char *prtcl = get_string_val(element, "protocol"); + int video_bitrate = + get_int_val(element, "video_bitrate"); + int audio_bitrate = + get_int_val(element, "audio_bitrate"); + int fps = get_int_val(element, "fps"); + + if (prtcl != NULL) { + if (video_bitrate != -1) + maximum[prtcl].video_bitrate = + video_bitrate; + if (audio_bitrate != -1) + maximum[prtcl].audio_bitrate = + audio_bitrate; + if (fps != -1) + maximum[prtcl].fps = fps; + } + } + + json_decref(object); + } + + /* Supported resolutions extraction */ + + object = json_object_get(service, "supported_resolutions"); + if (object) { + json_incref(object); + + json_array_foreach (object, idx, element) { + const char *res_str = json_string_value(element); + obs_service_resolution res; + + if (res_str != NULL) { + if (sscanf(res_str, "%dx%d", &res.cx, + &res.cy) == 2) { + supported_resolutions_str.push_back( + res_str); + supported_resolutions.push_back(res); + } + } + } + + json_decref(object); + } + + /** Service implementation **/ + + _info.type_data = this; + _info.id = _id.c_str(); + + _info.get_name = _get_name; + + _info.create = _create; + _info.destroy = _destroy; + + _info.update = _update; + + _info.get_defaults2 = _get_defaults2; + + _info.get_properties2 = _get_properties2; + + _info.get_protocol = _get_protocol; + _info.get_url = _get_url; + _info.get_key = _get_key; + + _info.get_max_fps = _get_max_fps; + _info.get_max_bitrate = _get_max_bitrate; + +#if RTMPS_DISABLED + if ((protocols.size() == 1) && (protocols[0].compare("RTMPS") == 0)) + return; +#endif + +#if FTL_DISABLED + if ((protocols.size() == 1) && (protocols[0].compare("FTL") == 0)) + return; +#endif + + obs_register_service(&_info); +} + +service_factory::~service_factory() +{ + protocols.clear(); + servers.clear(); + maximum.clear(); + supported_resolutions_str.clear(); + supported_resolutions.clear(); +} + +const char *service_factory::get_name() +{ + return _name.c_str(); +} + +void *service_factory::create(obs_data_t *settings, obs_service_t *service) +{ + return reinterpret_cast( + new service_instance(settings, service)); +} + +void service_factory::get_defaults2(obs_data_t *settings) +{ + if (!more_info_link.empty()) + obs_data_set_default_string(settings, "info_link", + more_info_link.c_str()); + + if (!stream_key_link.empty()) + obs_data_set_default_string(settings, "key_link", + stream_key_link.c_str()); + + obs_data_set_default_string(settings, "protocol", protocols[0].c_str()); + + add_maximum_defaults(settings); + + obs_data_set_default_bool(settings, "ignore_supported_resolutions", + false); +} + +static inline void set_visible_maximum(obs_properties_t *props, + obs_data_t *settings, const char *name, + bool prtcl) +{ + obs_property_set_visible(obs_properties_get(props, name), + prtcl && (obs_data_get_int(settings, name) != + -1)); +} + +static inline void +set_visible_ignore_maximum(obs_properties_t *props, obs_data_t *settings, + const char *max1, const char *max2, const char *max3) +{ + obs_property_set_visible( + obs_properties_get(props, "ignore_maximum"), + (obs_data_get_int(settings, max1) != -1) || + (obs_data_get_int(settings, max2) != -1) || + (obs_data_get_int(settings, max3) != -1)); +} + +// XXX: Set the visibility of server lists and maximum settings for each protocols +static bool modified_protocol(obs_properties_t *props, obs_property_t *, + obs_data_t *settings) noexcept +try { + const char *protocol = obs_data_get_string(settings, "protocol"); + + bool rtmp = strcmp(protocol, "RTMP") == 0; + obs_property_set_visible(obs_properties_get(props, "server_rtmp"), + rtmp); + set_visible_maximum(props, settings, "max_video_bitrate_rtmp", rtmp); + set_visible_maximum(props, settings, "max_audio_bitrate_rtmp", rtmp); + set_visible_maximum(props, settings, "max_fps_rtmp", rtmp); + +#if !RTMPS_DISABLED + bool rtmps = strcmp(protocol, "RTMPS") == 0; + obs_property_set_visible(obs_properties_get(props, "server_rtmps"), + rtmps); + set_visible_maximum(props, settings, "max_video_bitrate_rtmps", rtmps); + set_visible_maximum(props, settings, "max_audio_bitrate_rtmps", rtmps); + set_visible_maximum(props, settings, "max_fps_rtmps", rtmps); +#endif + + bool hls = strcmp(protocol, "HLS") == 0; + obs_property_set_visible(obs_properties_get(props, "server_hls"), hls); + set_visible_maximum(props, settings, "max_video_bitrate_hls", hls); + set_visible_maximum(props, settings, "max_audio_bitrate_hls", hls); + set_visible_maximum(props, settings, "max_fps_hls", hls); + +#if !FTL_DISABLED + bool ftl = strcmp(protocol, "FTL") == 0; + obs_property_set_visible(obs_properties_get(props, "server_ftl"), ftl); + set_visible_maximum(props, settings, "max_video_bitrate_ftl", ftl); + set_visible_maximum(props, settings, "max_audio_bitrate_ftl", ftl); + set_visible_maximum(props, settings, "max_fps_ftl", ftl); +#endif + + //Ignore Max Toggle + if (rtmp) + set_visible_ignore_maximum(props, settings, + "max_video_bitrate_rtmp", + "max_audio_bitrate_rtmp", + "max_fps_rtmp"); +#if !RTMPS_DISABLED + if (rtmps) + set_visible_ignore_maximum(props, settings, + "max_video_bitrate_rtmps", + "max_audio_bitrate_rtmps", + "max_fps_rtmps"); +#endif + if (hls) + set_visible_ignore_maximum(props, settings, + "max_video_bitrate_hls", + "max_audio_bitrate_hls", + "max_fps_hls"); +#if !FTL_DISABLED + if (ftl) + set_visible_ignore_maximum(props, settings, + "max_video_bitrate_ftl", + "max_audio_bitrate_ftl", + "max_fps_ftl"); +#endif + + return true; +} catch (const std::exception &ex) { + blog(LOG_ERROR, "Unexpected exception in function %s: %s", __func__, + ex.what()); + return false; +} catch (...) { + blog(LOG_ERROR, "Unexpected exception in function %s", __func__); + return false; +} + +obs_properties_t *service_factory::get_properties2(void *data) +{ + UNUSED_PARAMETER(data); + + obs_properties_t *props = obs_properties_create(); + obs_property_t *p; + + if (!more_info_link.empty()) + obs_properties_add_open_url(props, "info_link", + obs_module_text("MoreInfo")); + + p = obs_properties_add_list(props, "protocol", + obs_module_text("Protocol"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + obs_property_set_modified_callback(p, modified_protocol); + + for (size_t idx = 0; idx < protocols.size(); idx++) { +#if RTMPS_DISABLED + if (protocols[idx].compare("RTMPS") == 0) + continue; +#endif +#if FTL_DISABLED + if (protocols[idx].compare("FTL") == 0) + continue; +#endif + obs_property_list_add_string(p, protocols[idx].c_str(), + protocols[idx].c_str()); + } + + create_server_lists(props); + + obs_properties_add_text(props, "key", obs_module_text("StreamKey"), + OBS_TEXT_PASSWORD); + + if (!stream_key_link.empty()) + obs_properties_add_open_url(props, "key_link", + obs_module_text("StreamKeyLink")); + + add_maximum_infos(props); + + if (!supported_resolutions_str.empty()) { + std::string label = obs_module_text("SupportedResolutions"); + label += " "; + + for (size_t idx = 0; idx < supported_resolutions_str.size(); + idx++) { + label += supported_resolutions_str[idx]; + if ((supported_resolutions_str.size() - idx) != 1) + label += ", "; + } + + obs_properties_add_info(props, "resolutions", label.c_str()); + obs_properties_add_bool( + props, "ignore_supported_resolutions", + obs_module_text("IgnoreSupportedResolutions")); + } + + return props; +} diff --git a/plugins/obs-services/service-factory.hpp b/plugins/obs-services/service-factory.hpp new file mode 100644 index 00000000000000..737200a0276df1 --- /dev/null +++ b/plugins/obs-services/service-factory.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +extern "C" { +#include +#include +} + +struct server_t { + const char *protocol; + const char *url; + const char *name; +}; + +struct maximum_t { + int video_bitrate = -1; + int audio_bitrate = -1; + int fps = -1; +}; + +class service_factory { + obs_service_info _info = {}; + + std::string _id; + std::string _name; + std::string more_info_link; + std::string stream_key_link; + std::vector protocols; + std::vector servers; + std::map maximum; + std::vector supported_resolutions_str; + std::vector supported_resolutions; + + void create_server_lists(obs_properties_t *props); + + void add_maximum_defaults(obs_data_t *settings); + void add_maximum_infos(obs_properties_t *props); + + static const char *_get_name(void *type_data) noexcept; + + static void *_create(obs_data_t *settings, + obs_service_t *service) noexcept; + static void _destroy(void *data) noexcept; + + static void _update(void *data, obs_data_t *settings) noexcept; + + static void _get_defaults2(obs_data_t *settings, + void *type_data) noexcept; + + static obs_properties_t *_get_properties2(void *data, + void *type_data) noexcept; + + static const char *_get_protocol(void *data) noexcept; + static const char *_get_url(void *data) noexcept; + static const char *_get_key(void *data) noexcept; + + static void _get_max_fps(void *data, int *fps) noexcept; + static void _get_max_bitrate(void *data, int *video, + int *audio) noexcept; + +public: + service_factory(json_t *service); + ~service_factory(); + + const char *get_name(); + + virtual void *create(obs_data_t *settings, obs_service_t *service); + + virtual void get_defaults2(obs_data_t *settings); + + virtual obs_properties_t *get_properties2(void *data); +}; diff --git a/plugins/obs-services/service-instance.cpp b/plugins/obs-services/service-instance.cpp new file mode 100644 index 00000000000000..deaa667d963016 --- /dev/null +++ b/plugins/obs-services/service-instance.cpp @@ -0,0 +1,80 @@ +#include "service-instance.hpp" + +#include + +service_instance::service_instance(obs_data_t *settings, obs_service_t *self) + : _factory(reinterpret_cast( + obs_service_get_type_data(self))) +{ + update(settings); +} + +// XXX: Add server updater for each protocols +void service_instance::update(obs_data_t *settings) +{ + protocol = obs_data_get_string(settings, "protocol"); + + if (protocol.compare("RTMP") == 0) { + server = obs_data_get_string(settings, "server_rtmp"); + max_fps = obs_data_get_int(settings, "max_fps_rtmp"); + max_video_bitrate = + obs_data_get_int(settings, "max_video_bitrate_rtmp"); + max_audio_bitrate = + obs_data_get_int(settings, "max_audio_bitrate_rtmp"); + } + if (protocol.compare("RTMPS") == 0) { + server = obs_data_get_string(settings, "server_rtmps"); + max_fps = obs_data_get_int(settings, "max_fps_rtmps"); + max_video_bitrate = + obs_data_get_int(settings, "max_video_bitrate_rtmps"); + max_audio_bitrate = + obs_data_get_int(settings, "max_audio_bitrate_rtmps"); + } + if (protocol.compare("HLS") == 0) { + server = obs_data_get_string(settings, "server_hls"); + max_fps = obs_data_get_int(settings, "max_fps_hls"); + max_video_bitrate = + obs_data_get_int(settings, "max_video_bitrate_hls"); + max_audio_bitrate = + obs_data_get_int(settings, "max_audio_bitrate_hls"); + } + if (protocol.compare("FTL") == 0) { + server = obs_data_get_string(settings, "server_ftl"); + max_fps = obs_data_get_int(settings, "max_fps_ftl"); + max_video_bitrate = + obs_data_get_int(settings, "max_video_bitrate_ftl"); + max_audio_bitrate = + obs_data_get_int(settings, "max_audio_bitrate_ftl"); + } + + key = obs_data_get_string(settings, "key"); +} + +const char *service_instance::get_protocol() +{ + return protocol.c_str(); +} + +const char *service_instance::get_url() +{ + return server.c_str(); +} + +const char *service_instance::get_key() +{ + return key.c_str(); +} + +void service_instance::get_max_fps(int *fps) +{ + if (max_fps != -1) + *fps = max_fps; +} + +void service_instance::get_max_bitrate(int *video, int *audio) +{ + if (max_video_bitrate != -1) + *video = max_video_bitrate; + if (max_audio_bitrate != -1) + *audio = max_audio_bitrate; +} diff --git a/plugins/obs-services/service-instance.hpp b/plugins/obs-services/service-instance.hpp new file mode 100644 index 00000000000000..70d8093ea0ad59 --- /dev/null +++ b/plugins/obs-services/service-instance.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "service-factory.hpp" + +extern "C" { +#include +#include +} + +class service_instance { + service_factory *_factory; + + std::string protocol; + std::string server; + std::string key; + int max_fps; + int max_video_bitrate; + int max_audio_bitrate; + +public: + service_instance(obs_data_t *settings, obs_service_t *self); + virtual ~service_instance(){}; + + void update(obs_data_t *settings); + + const char *get_protocol(); + const char *get_url(); + const char *get_key(); + + void get_max_fps(int *fps); + void get_max_bitrate(int *video, int *audio); +}; diff --git a/plugins/obs-services/service-manager.cpp b/plugins/obs-services/service-manager.cpp new file mode 100644 index 00000000000000..923dfdbaf3d8d4 --- /dev/null +++ b/plugins/obs-services/service-manager.cpp @@ -0,0 +1,140 @@ +#include "service-manager.hpp" + +#include "plugin.hpp" + +extern "C" { +#include +#include +#include +#include +} + +#include "json-format-ver.hpp" + +static inline int get_int_val(json_t *service, const char *key) +{ + json_t *integer_val = json_object_get(service, key); + if (!integer_val || !json_is_integer(integer_val)) + return 0; + + return (int)json_integer_value(integer_val); +} + +static inline const char *get_string_val(json_t *service, const char *key) +{ + json_t *str_val = json_object_get(service, key); + if (!str_val || !json_is_string(str_val)) + return NULL; + + return json_string_value(str_val); +} + +static json_t *open_services_list(const char *file) +{ + char *file_data = os_quick_read_utf8_file(file); + json_error_t error; + json_t *root; + json_t *list; + int format_ver; + + if (!file_data) + return NULL; + + root = json_loads(file_data, JSON_REJECT_DUPLICATES, &error); + bfree(file_data); + + if (!root) { + blog(LOG_WARNING, "%s: Error reading JSON file (%d): %s", + __func__, error.line, error.text); + return NULL; + } + + format_ver = get_int_val(root, "format_version"); + + if (format_ver != SERVICES_FORMAT_VERSION) { + blog(LOG_DEBUG, "%s: Wrong format version (%d), expected %d", + __func__, format_ver, SERVICES_FORMAT_VERSION); + json_decref(root); + return NULL; + } + + list = json_object_get(root, "services"); + if (list) + json_incref(list); + json_decref(root); + + if (!list) { + blog(LOG_WARNING, "%s: No services list", __func__); + return NULL; + } + + return list; +} + +static json_t *get_services_list(void) +{ + char *file; + json_t *list = NULL; + + file = obs_module_config_path("services.json"); + if (file) { + list = open_services_list(file); + bfree(file); + } + + if (!list) { + file = obs_module_file("services.json"); + if (file) { + list = open_services_list(file); + bfree(file); + } + } + + return list; +} + +service_manager::~service_manager() +{ + _factories.clear(); +} + +void service_manager::register_services() +{ + json_t *services_list = get_services_list(); + json_t *service; + size_t idx; + + json_array_foreach (services_list, idx, service) { + const char *id = get_string_val(service, "id"); + + if (id != NULL) { + blog(LOG_DEBUG, "%s: Loading service with id \"%s\"", + __func__, id); + _factories.emplace( + id, std::make_shared(service)); + blog(LOG_DEBUG, "%s: Service with id \"%s\" was loaded", + __func__, id); + } else + blog(LOG_ERROR, + "%s: Unable to load as service JSON object n°%zu", + __func__, idx); + } + + json_decref(service); + json_decref(services_list); +} + +std::shared_ptr _service_manager_instance = nullptr; + +void service_manager::initialize() +{ + if (!_service_manager_instance) { + _service_manager_instance = std::make_shared(); + _service_manager_instance->register_services(); + } +} + +void service_manager::finalize() +{ + _service_manager_instance.reset(); +} diff --git a/plugins/obs-services/service-manager.hpp b/plugins/obs-services/service-manager.hpp new file mode 100644 index 00000000000000..c262e927f8784a --- /dev/null +++ b/plugins/obs-services/service-manager.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#include "service-factory.hpp" + +class service_manager { + std::map> _factories; + +public: + service_manager(){}; + ~service_manager(); + + void register_services(); + + static void initialize(); + + static void finalize(); +};