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