#include "multistream.hpp" #include "obs-module.h" #include "version.h" #include #include #include #include #include #include #include #include #include #include #include #include "config-utils.hpp" extern "C" { #include "file-updater.h" } OBS_DECLARE_MODULE() OBS_MODULE_AUTHOR("Aitum"); OBS_MODULE_USE_DEFAULT_LOCALE("aitum-multistream", "en-US") static MultistreamDock *multistream_dock = nullptr; update_info_t *version_update_info = nullptr; bool version_info_downloaded(void *param, struct file_download_data *file) { UNUSED_PARAMETER(param); if (!file || !file->buffer.num) return true; auto d = obs_data_create_from_json((const char *)file->buffer.array); if (!d) return true; auto data = obs_data_get_obj(d, "data"); obs_data_release(d); if (!data) return true; auto version = QString::fromUtf8(obs_data_get_string(data, "version")); QStringList pieces = version.split("."); if (pieces.count() > 2) { auto major = pieces[0].toInt(); auto minor = pieces[1].toInt(); auto patch = pieces[2].toInt(); auto sv = MAKE_SEMANTIC_VERSION(major, minor, patch); if (sv > MAKE_SEMANTIC_VERSION(PROJECT_VERSION_MAJOR, PROJECT_VERSION_MINOR, PROJECT_VERSION_PATCH)) { QMetaObject::invokeMethod(multistream_dock, "NewerVersionAvailable", Q_ARG(QString, version)); } } obs_data_release(data); return true; } bool obs_module_load(void) { blog(LOG_INFO, "[Aitum-Multistream] loaded version %s", PROJECT_VERSION); const auto main_window = static_cast(obs_frontend_get_main_window()); multistream_dock = new MultistreamDock(main_window); obs_frontend_add_dock_by_id("AitumMultistreamDock", obs_module_text("AitumMultistream"), multistream_dock); version_update_info = update_info_create_single("[Aitum Multistream]", "OBS", "https://api.aitum.tv/multi", version_info_downloaded, nullptr); return true; } void obs_module_post_load() { if (multistream_dock) multistream_dock->LoadVerticalOutputs(true); } void obs_module_unload() { update_info_destroy(version_update_info); if (multistream_dock) { delete multistream_dock; } } void RemoveWidget(QWidget *widget); void RemoveLayoutItem(QLayoutItem *item) { if (!item) return; RemoveWidget(item->widget()); if (item->layout()) { while (QLayoutItem *item2 = item->layout()->takeAt(0)) RemoveLayoutItem(item2); } delete item; } void RemoveWidget(QWidget *widget) { if (!widget) return; if (widget->layout()) { auto l = widget->layout(); QLayoutItem *item; while (l->count() > 0 && (item = l->takeAt(0))) { RemoveLayoutItem(item); } delete l; } delete widget; } // Output button styling void MultistreamDock::outputButtonStyle(QPushButton *button) { button->setMinimumHeight(24); std::string baseStyles = "min-width: 30px; padding: 2px 10px; border-width: 2px;"; button->setStyleSheet(QString::fromUtf8(baseStyles + (button->isChecked() ? "background: rgb(0,210,153);" : ""))); button->setIcon(button->isChecked() ? streamActiveIcon : streamInactiveIcon); } // Common styling things here auto canvasGroupStyle = QString("padding: 0px 0px 0px 0px;"); // Main Canvas, Vertical Canvas auto canvasGroupHeaderStyle = QString("padding: 0px 0px 0px 0px; font-weight: bold;"); // header of each group auto outputTitleStyle = QString("QLabel{}"); // "Built -in stream" auto outputGroupStyle = QString("background-color: %1; padding: 0px;") .arg(QPalette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)); // wrapper around above auto outputPlatformIconSize = 36; // For showing warning for no vertical integration void showVerticalWarning(QVBoxLayout *verticalLayout) { auto verticalWarning = new QWidget; verticalWarning->setContentsMargins(0, 0, 0, 0); auto verticalWarningLayout = new QVBoxLayout; verticalWarningLayout->setContentsMargins(0, 0, 0, 0); auto label = new QLabel(QString::fromUtf8(obs_module_text("NoVerticalWarning"))); label->setStyleSheet(QString("padding: 0px;")); label->setWordWrap(true); label->setTextFormat(Qt::RichText); label->setOpenExternalLinks(true); verticalWarningLayout->addWidget(label); verticalWarning->setLayout(verticalWarningLayout); verticalLayout->addWidget(verticalWarning); } #if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 0, 0) static config_t *(*get_user_config_func)(void) = nullptr; #endif config_t *get_user_config(void) { #if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 0, 0) if (!get_user_config_func) { if (obs_get_version() < MAKE_SEMANTIC_VERSION(31, 0, 0)) { get_user_config_func = obs_frontend_get_global_config; blog(LOG_INFO, "[Aitum Multistream] use global config"); } else { auto handle = os_dlopen("obs-frontend-api"); if (handle) { get_user_config_func = (config_t * (*)(void)) os_dlsym(handle, "obs_frontend_get_user_config"); os_dlclose(handle); if (get_user_config_func) blog(LOG_INFO, "[Aitum Multistream] use user config"); } } } if (get_user_config_func) return get_user_config_func(); return obs_frontend_get_global_config(); #else return obs_frontend_get_user_config(); #endif } MultistreamDock::MultistreamDock(QWidget *parent) : QFrame(parent) { // Main layout auto mainLayout = new QVBoxLayout; mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->setSpacing(0); setLayout(mainLayout); auto t = new QWidget; auto tl = new QVBoxLayout; tl->setSpacing(8); // between canvas groups tl->setContentsMargins(0, 0, 0, 0); t->setStyleSheet(QString("padding: 0px; margin:0px;")); t->setLayout(tl); // Group for built in canvas auto mainCanvasGroup = new QGroupBox; mainCanvasGroup->setStyleSheet(canvasGroupStyle); mainCanvasLayout = new QVBoxLayout; mainCanvasLayout->setSpacing(4); // between outputs on main canvas // Layout for header row auto mainCanvasTitleRowLayout = new QHBoxLayout; auto mainCanvasLabel = new QLabel(QString::fromUtf8(obs_module_text("MainCanvas"))); mainCanvasLabel->setStyleSheet(canvasGroupHeaderStyle); mainCanvasTitleRowLayout->addWidget(mainCanvasLabel); mainCanvasLayout->addLayout(mainCanvasTitleRowLayout); // We store the actual outputs here mainCanvasOutputLayout = new QVBoxLayout; mainCanvasOutputLayout->setSpacing(4); // between outputs on main canvas auto mainStreamGroup = new QGroupBox; mainStreamGroup->setStyleSheet(outputGroupStyle); auto mainStreamLayout = new QVBoxLayout; auto l2 = new QHBoxLayout; // Label for built in stream auto bisHeaderLabel = new QLabel(QString::fromUtf8(obs_module_text("BuiltinStream"))); bisHeaderLabel->setStyleSheet(outputTitleStyle); // blank because we're pulling settings through from bis later mainPlatformIconLabel = new QLabel; auto platformIcon = ConfigUtils::getPlatformIconFromEndpoint(QString::fromUtf8("")); mainPlatformIconLabel->setPixmap(platformIcon.pixmap(outputPlatformIconSize, outputPlatformIconSize)); l2->addWidget(mainPlatformIconLabel); l2->addWidget(bisHeaderLabel, 1); mainStreamButton = new QPushButton; mainStreamButton->setObjectName(QStringLiteral("canvasStream")); mainStreamButton->setIcon(streamInactiveIcon); mainStreamButton->setCheckable(true); mainStreamButton->setChecked(false); outputButtonStyle(mainStreamButton); connect(mainStreamButton, &QPushButton::clicked, [this] { const auto config = get_user_config(); if (obs_frontend_streaming_active()) { bool stop = true; bool warnBeforeStreamStop = config_get_bool(config, "BasicWindow", "WarnBeforeStoppingStream"); if (warnBeforeStreamStop && isVisible()) { auto button = QMessageBox::question( this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) stop = false; } if (stop) { obs_frontend_streaming_stop(); mainStreamButton->setChecked(false); } else { mainStreamButton->setChecked(true); } } else { bool warnBeforeStreamStart = config_get_bool(config, "BasicWindow", "WarnBeforeStartingStream"); if (warnBeforeStreamStart && isVisible()) { auto button = QMessageBox::question( this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) { mainStreamButton->setChecked(false); } else { obs_frontend_streaming_start(); } } else { obs_frontend_streaming_start(); } mainStreamButton->setChecked(true); } outputButtonStyle(mainStreamButton); }); //streamButton->setSizePolicy(sp2); mainStreamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream"))); l2->addWidget(mainStreamButton); mainStreamLayout->addLayout(l2); mainStreamGroup->setLayout(mainStreamLayout); mainCanvasOutputLayout->addWidget(mainStreamGroup); mainCanvasLayout->addLayout(mainCanvasOutputLayout); mainCanvasGroup->setLayout(mainCanvasLayout); tl->addWidget(mainCanvasGroup); // VERTICAL auto verticalCanvasGroup = new QGroupBox; verticalCanvasGroup->setStyleSheet(canvasGroupStyle); verticalCanvasLayout = new QVBoxLayout; verticalCanvasGroup->setLayout(verticalCanvasLayout); tl->addWidget(verticalCanvasGroup); tl->addStretch(1); // Layout for header row auto verticalCanvasTitleRowLayout = new QHBoxLayout; auto verticalCanvasLabel = new QLabel(QString::fromUtf8(obs_module_text("VerticalCanvas"))); verticalCanvasLabel->setStyleSheet(canvasGroupHeaderStyle); verticalCanvasTitleRowLayout->addWidget(verticalCanvasLabel); verticalCanvasLayout->addLayout(verticalCanvasTitleRowLayout); // We store the actual outputs here verticalCanvasOutputLayout = new QVBoxLayout; verticalCanvasOutputLayout->setSpacing(4); // between outputs on vertical canvas verticalCanvasLayout->addLayout(verticalCanvasOutputLayout); // Add output layout to parent //tl->addWidget(verticalCanvasGroup); QScrollArea *scrollArea = new QScrollArea; scrollArea->setWidget(t); scrollArea->setWidgetResizable(true); scrollArea->setLineWidth(0); scrollArea->setFrameShape(QFrame::NoFrame); scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); mainLayout->addWidget(scrollArea, 1); // Bottom Button Row auto buttonRow = new QHBoxLayout; buttonRow->setContentsMargins(8, 6, 8, 4); buttonRow->setSpacing(8); // Config Button configButton = new QPushButton; configButton->setMinimumHeight(30); configButton->setProperty("themeID", "configIconSmall"); configButton->setProperty("class", "icon-gear"); configButton->setFlat(true); configButton->setAutoDefault(false); //configButton->setSizePolicy(sp2); configButton->setToolTip(QString::fromUtf8(obs_module_text("AitumMultistreamSettings"))); QPushButton::connect(configButton, &QPushButton::clicked, [this] { if (!configDialog) configDialog = new OBSBasicSettings((QMainWindow *)obs_frontend_get_main_window()); auto settings = obs_data_create(); if (current_config) obs_data_apply(settings, current_config); configDialog->LoadSettings(settings); configDialog->LoadVerticalSettings(true); configDialog->LoadOutputStats(&oldVideo); configDialog->SetNewerVersion(newer_version_available); configDialog->setResult(QDialog::Rejected); if (configDialog->exec() == QDialog::Accepted) { if (current_config) { obs_data_apply(current_config, settings); obs_data_release(settings); SaveSettings(); LoadSettings(); configDialog->SaveVerticalSettings(); LoadVerticalOutputs(false); } else { current_config = settings; } } else { obs_data_release(settings); } }); buttonRow->addWidget(configButton); // Contribute Button auto contributeButton = new QPushButton; contributeButton->setMinimumHeight(30); contributeButton->setIcon(ConfigUtils::generateEmojiQIcon("❤️")); contributeButton->setToolTip(QString::fromUtf8(obs_module_text("AitumMultistreamDonate"))); QPushButton::connect(contributeButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://aitum.tv/contribute")); }); buttonRow->addWidget(contributeButton); // Aitum Button auto aitumButton = new QPushButton; aitumButton->setMinimumHeight(30); //aitumButton->setSizePolicy(sp2); aitumButton->setIcon(QIcon(":/aitum/media/aitum.png")); aitumButton->setToolTip(QString::fromUtf8("https://aitum.tv")); QPushButton::connect(aitumButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://aitum.tv")); }); buttonRow->addWidget(aitumButton); mainLayout->addLayout(buttonRow); obs_frontend_add_event_callback(frontend_event, this); mainVideo = obs_get_video(); connect(&videoCheckTimer, &QTimer::timeout, [this] { if (obs_get_video() != mainVideo) { oldVideo.push_back(mainVideo); mainVideo = obs_get_video(); for (auto it = outputs.begin(); it != outputs.end(); it++) { auto venc = obs_output_get_video_encoder(std::get(*it)); if (venc && !obs_encoder_active(venc)) obs_encoder_set_video(venc, mainVideo); } } auto service = obs_frontend_get_streaming_service(); auto url = QString::fromUtf8(service ? obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_SERVER_URL) : ""); if (url != mainPlatformUrl) { mainPlatformUrl = url; mainPlatformIconLabel->setPixmap(ConfigUtils::getPlatformIconFromEndpoint(url).pixmap(outputPlatformIconSize, outputPlatformIconSize)); } int idx = 0; while (auto item = mainCanvasOutputLayout->itemAt(idx++)) { auto streamGroup = item->widget(); if (idx == 1) { auto active = obs_frontend_streaming_active(); foreach(QObject * c, streamGroup->children()) { std::string cn = c->metaObject()->className(); if (cn == "QPushButton") { auto pb = (QPushButton *)c; if (pb->isChecked() != active) { pb->setChecked(active); outputButtonStyle(pb); } } } continue; } std::string name = streamGroup->objectName().toUtf8().constData(); if (name.empty()) continue; for (auto it = outputs.begin(); it != outputs.end(); it++) { if (std::get(*it) != name) continue; auto active = obs_output_active(std::get(*it)); foreach(QObject * c, streamGroup->children()) { std::string cn = c->metaObject()->className(); if (cn == "QPushButton") { auto pb = (QPushButton *)c; if (pb->isChecked() != active) { pb->setChecked(active); outputButtonStyle(pb); } } } } } auto ph = obs_get_proc_handler(); struct calldata cd; calldata_init(&cd); idx = 0; while (auto item = verticalCanvasOutputLayout->itemAt(idx++)) { auto streamGroup = item->widget(); std::string name = streamGroup->objectName().toUtf8().constData(); if (name.empty()) continue; obs_output_t *output = nullptr; calldata_set_string(&cd, "name", name.c_str()); if (proc_handler_call(ph, "aitum_vertical_get_stream_output", &cd)) { output = (obs_output_t *)calldata_ptr(&cd, "output"); } bool active = obs_output_active(output); obs_output_release(output); foreach(QObject * c, streamGroup->children()) { std::string cn = c->metaObject()->className(); if (cn == "QPushButton") { auto pb = (QPushButton *)c; if (pb->isChecked() != active) { pb->setChecked(active); outputButtonStyle(pb); } } } } calldata_free(&cd); }); videoCheckTimer.start(500); } MultistreamDock::~MultistreamDock() { for (auto it = outputs.begin(); it != outputs.end(); it++) { auto old = std::get(*it); signal_handler_t *signal = obs_output_get_signal_handler(old); signal_handler_disconnect(signal, "start", stream_output_start, this); signal_handler_disconnect(signal, "stop", stream_output_stop, this); auto service = obs_output_get_service(old); if (obs_output_active(old)) { obs_output_force_stop(old); } if (!exiting) obs_output_release(old); obs_service_release(service); } outputs.clear(); obs_data_array_release(vertical_outputs); obs_data_release(current_config); obs_frontend_remove_event_callback(frontend_event, this); multistream_dock = nullptr; } void MultistreamDock::frontend_event(enum obs_frontend_event event, void *private_data) { auto md = (MultistreamDock *)private_data; if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED || event == OBS_FRONTEND_EVENT_FINISHED_LOADING) { md->LoadSettingsFile(); } else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGING || event == OBS_FRONTEND_EVENT_PROFILE_RENAMED) { md->SaveSettings(); } else if (event == OBS_FRONTEND_EVENT_EXIT) { md->SaveSettings(); md->exiting = true; } else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING || event == OBS_FRONTEND_EVENT_STREAMING_STARTED) { md->mainStreamButton->setChecked(true); md->outputButtonStyle(md->mainStreamButton); md->mainStreamButton->setIcon(md->streamActiveIcon); md->storeMainStreamEncoders(); } else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING || event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) { md->mainStreamButton->setChecked(false); md->outputButtonStyle(md->mainStreamButton); } } void MultistreamDock::LoadSettingsFile() { obs_data_release(current_config); current_config = nullptr; char *path = obs_module_config_path("config.json"); if (!path) return; obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak"); bfree(path); if (!config) { config = obs_data_create(); blog(LOG_WARNING, "[Aitum Multistream] No configuration file loaded"); } else { blog(LOG_INFO, "[Aitum Multistream] Loaded configuration file"); } char *profile = obs_frontend_get_current_profile(); auto profiles = obs_data_get_array(config, "profiles"); auto pc = obs_data_array_count(profiles); obs_data_t *pd = nullptr; for (size_t i = 0; i < pc; i++) { obs_data_t *t = obs_data_array_item(profiles, i); if (!t) continue; auto name = obs_data_get_string(t, "name"); if (strcmp(profile, name) == 0) { pd = t; break; } obs_data_release(t); } obs_data_array_release(profiles); obs_data_release(config); if (!pd) { current_config = obs_data_create(); obs_data_set_string(current_config, "name", profile); bfree(profile); blog(LOG_INFO, "[Aitum Multistream] profile not found"); LoadSettings(); return; } bfree(profile); current_config = pd; LoadSettings(); } void MultistreamDock::LoadSettings() { auto outputs2 = obs_data_get_array(current_config, "outputs"); int idx = 1; while (auto item = mainCanvasOutputLayout->itemAt(idx)) { auto streamGroup = item->widget(); mainCanvasOutputLayout->removeWidget(streamGroup); RemoveWidget(streamGroup); } obs_data_array_enum( outputs2, [](obs_data_t *data2, void *param) { auto d = (MultistreamDock *)param; d->LoadOutput(data2, false); }, this); obs_data_array_release(outputs2); } void MultistreamDock::LoadOutput(obs_data_t *output_data, bool vertical) { auto nameChars = obs_data_get_string(output_data, "name"); auto name = QString::fromUtf8(nameChars); if (vertical) { for (int i = 0; i < verticalCanvasOutputLayout->count(); i++) { auto item = verticalCanvasOutputLayout->itemAt(i); auto oName = item->widget()->objectName(); if (oName == name) { return; } } } else { for (int i = 1; i < mainCanvasOutputLayout->count(); i++) { auto item = mainCanvasOutputLayout->itemAt(i); auto oName = item->widget()->objectName(); if (oName == name) { return; } } } auto streamButton = new QPushButton; for (auto it = outputs.begin(); it != outputs.end(); it++) { if (std::get(*it) != nameChars) continue; if (obs_data_get_bool(output_data, "advanced")) { auto output = std::get(*it); auto video_encoder = obs_output_get_video_encoder(output); if (video_encoder && strcmp(obs_encoder_get_id(video_encoder), obs_data_get_string(output_data, "video_encoder")) == 0) { auto ves = obs_data_get_obj(output_data, "video_encoder_settings"); obs_encoder_update(video_encoder, ves); obs_data_release(ves); } } std::get(*it) = streamButton; } auto streamGroup = new QGroupBox; streamGroup->setStyleSheet(outputGroupStyle); streamGroup->setObjectName(name); auto streamLayout = new QVBoxLayout; auto l2 = new QHBoxLayout; auto endpoint = QString::fromUtf8(obs_data_get_string(output_data, "stream_server")); auto platformIconLabel = new QLabel; auto platformIcon = ConfigUtils::getPlatformIconFromEndpoint(endpoint); platformIconLabel->setPixmap(platformIcon.pixmap(outputPlatformIconSize, outputPlatformIconSize)); l2->addWidget(platformIconLabel); l2->addWidget(new QLabel(name), 1); streamButton->setMinimumHeight(30); streamButton->setObjectName(QStringLiteral("canvasStream")); streamButton->setIcon(streamInactiveIcon); streamButton->setCheckable(true); streamButton->setChecked(false); outputButtonStyle(streamButton); if (vertical) { std::string output_name = obs_data_get_string(output_data, "name"); connect(streamButton, &QPushButton::clicked, [this, streamButton, output_name] { auto ph = obs_get_proc_handler(); struct calldata cd; calldata_init(&cd); calldata_set_string(&cd, "name", output_name.c_str()); auto config = get_user_config(); if (streamButton->isChecked()) { bool start = true; bool warnBeforeStreamStart = config_get_bool(config, "BasicWindow", "WarnBeforeStartingStream"); if (warnBeforeStreamStart && isVisible()) { auto button = QMessageBox::question( this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) start = false; } if (!start || !proc_handler_call(ph, "aitum_vertical_start_stream_output", &cd)) streamButton->setChecked(false); } else { bool stop = true; bool warnBeforeStreamStop = config_get_bool(config, "BasicWindow", "WarnBeforeStoppingStream"); if (warnBeforeStreamStop && isVisible()) { auto button = QMessageBox::question( this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) stop = false; } if (stop) { proc_handler_call(ph, "aitum_vertical_stop_stream_output", &cd); } else { streamButton->setChecked(true); } } calldata_free(&cd); outputButtonStyle(streamButton); }); } else { connect(streamButton, &QPushButton::clicked, [this, streamButton, output_data] { if (streamButton->isChecked()) { blog(LOG_INFO, "[Aitum Multistream] start stream clicked '%s'", obs_data_get_string(output_data, "name")); if (!StartOutput(output_data, streamButton)) streamButton->setChecked(false); } else { bool stop = true; bool warnBeforeStreamStop = config_get_bool(get_user_config(), "BasicWindow", "WarnBeforeStoppingStream"); if (warnBeforeStreamStop && isVisible()) { auto button = QMessageBox::question( this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStop.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) stop = false; } if (stop) { blog(LOG_INFO, "[Aitum Multistream] stop stream clicked '%s'", obs_data_get_string(output_data, "name")); const char *name2 = obs_data_get_string(output_data, "name"); for (auto it = outputs.begin(); it != outputs.end(); it++) { if (std::get(*it) != name2) continue; obs_queue_task( OBS_TASK_GRAPHICS, [](void *param) { obs_output_stop((obs_output_t *)param); }, std::get(*it), false); } } else { streamButton->setChecked(true); } } outputButtonStyle(streamButton); }); } //streamButton->setSizePolicy(sp2); streamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream"))); l2->addWidget(streamButton); streamLayout->addLayout(l2); streamGroup->setLayout(streamLayout); if (vertical) verticalCanvasOutputLayout->addWidget(streamGroup); else mainCanvasOutputLayout->addWidget(streamGroup); } static void ensure_directory(char *path) { #ifdef _WIN32 char *backslash = strrchr(path, '\\'); if (backslash) *backslash = '/'; #endif char *slash = strrchr(path, '/'); if (slash) { *slash = 0; os_mkdirs(path); *slash = '/'; } #ifdef _WIN32 if (backslash) *backslash = '\\'; #endif } void MultistreamDock::SaveSettings() { char *path = obs_module_config_path("config.json"); if (!path) return; obs_data_t *config = obs_data_create_from_json_file_safe(path, "bak"); if (!config) { ensure_directory(path); config = obs_data_create(); blog(LOG_WARNING, "[Aitum Multistream] New configuration file"); } auto profiles = obs_data_get_array(config, "profiles"); if (!profiles) { profiles = obs_data_array_create(); obs_data_set_array(config, "profiles", profiles); } obs_data_t *pd = nullptr; if (current_config) { auto old_name = obs_data_get_string(current_config, "name"); auto pc = obs_data_array_count(profiles); for (size_t i = 0; i < pc; i++) { obs_data_t *t = obs_data_array_item(profiles, i); if (!t) continue; auto name = obs_data_get_string(t, "name"); if (strcmp(old_name, name) == 0) { pd = t; break; } obs_data_release(t); } } if (!pd) { pd = obs_data_create(); obs_data_array_push_back(profiles, pd); } obs_data_array_release(profiles); char *profile = obs_frontend_get_current_profile(); obs_data_set_string(pd, "name", profile); bfree(profile); if (current_config) obs_data_apply(pd, current_config); obs_data_release(pd); if (obs_data_save_json_safe(config, path, "tmp", "bak")) { blog(LOG_INFO, "[Aitum Multistream] Saved settings"); } else { blog(LOG_ERROR, "[Aitum Multistream] Failed saving settings"); } obs_data_release(config); bfree(path); } bool MultistreamDock::StartOutput(obs_data_t *settings, QPushButton *streamButton) { if (!settings) return false; bool warnBeforeStreamStart = config_get_bool(get_user_config(), "BasicWindow", "WarnBeforeStartingStream"); if (warnBeforeStreamStart && isVisible()) { auto button = QMessageBox::question(this, QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Title")), QString::fromUtf8(obs_frontend_get_locale_string("ConfirmStart.Text")), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (button == QMessageBox::No) return false; } const char *name = obs_data_get_string(settings, "name"); for (auto it = outputs.begin(); it != outputs.end(); it++) { if (std::get(*it) != name) continue; auto old = std::get(*it); auto service = obs_output_get_service(old); if (obs_output_active(old)) { obs_output_force_stop(old); } obs_output_release(old); obs_service_release(service); outputs.erase(it); break; } obs_encoder_t *venc = nullptr; obs_encoder_t *aenc = nullptr; auto advanced = obs_data_get_bool(settings, "advanced"); if (advanced) { auto venc_name = obs_data_get_string(settings, "video_encoder"); if (!venc_name || venc_name[0] == '\0') { //use main encoder auto main_output = obs_frontend_get_streaming_output(); if (!obs_output_active(main_output)) { obs_output_release(main_output); blog(LOG_WARNING, "[Aitum Multistream] failed to start stream '%s' because main was not started", obs_data_get_string(settings, "name")); QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")), QString::fromUtf8(obs_module_text("MainOutputNotActive"))); return false; } auto vei = (int)obs_data_get_int(settings, "video_encoder_index"); venc = obs_output_get_video_encoder2(main_output, vei); obs_output_release(main_output); if (!venc) { blog(LOG_WARNING, "[Aitum Multistream] failed to start stream '%s' because encoder index %d was not found", obs_data_get_string(settings, "name"), vei); QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputEncoderIndexNotFound")), QString::fromUtf8(obs_module_text("MainOutputEncoderIndexNotFound"))); return false; } } else { obs_data_t *s = nullptr; auto ves = obs_data_get_obj(settings, "video_encoder_settings"); if (ves) { s = obs_data_create(); obs_data_apply(s, ves); obs_data_release(ves); } std::string video_encoder_name = "aitum_multi_video_encoder_"; video_encoder_name += name; venc = obs_video_encoder_create(venc_name, video_encoder_name.c_str(), s, nullptr); obs_data_release(s); obs_encoder_set_video(venc, obs_get_video()); auto divisor = obs_data_get_int(settings, "frame_rate_divisor"); if (divisor > 1) obs_encoder_set_frame_rate_divisor(venc, (uint32_t)divisor); bool scale = obs_data_get_bool(settings, "scale"); if (scale) { obs_encoder_set_scaled_size(venc, (uint32_t)obs_data_get_int(settings, "width"), (uint32_t)obs_data_get_int(settings, "height")); obs_encoder_set_gpu_scale_type(venc, (obs_scale_type)obs_data_get_int(settings, "scale_type")); } } auto aenc_name = obs_data_get_string(settings, "audio_encoder"); if (!aenc_name || aenc_name[0] == '\0') { //use main encoder auto main_output = obs_frontend_get_streaming_output(); if (!obs_output_active(main_output)) { obs_output_release(main_output); blog(LOG_WARNING, "[Aitum Multistream] failed to start stream '%s' because main was not started", obs_data_get_string(settings, "name")); QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")), QString::fromUtf8(obs_module_text("MainOutputNotActive"))); return false; } auto aei = (int)obs_data_get_int(settings, "audio_encoder_index"); aenc = obs_output_get_audio_encoder(main_output, aei); obs_output_release(main_output); if (!aenc) { blog(LOG_WARNING, "[Aitum Multistream] failed to start stream '%s' because encoder index %d was not found", obs_data_get_string(settings, "name"), aei); QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputEncoderIndexNotFound")), QString::fromUtf8(obs_module_text("MainOutputEncoderIndexNotFound"))); return false; } } else { obs_data_t *s = nullptr; auto aes = obs_data_get_obj(settings, "audio_encoder_settings"); if (aes) { s = obs_data_create(); obs_data_apply(s, aes); obs_data_release(aes); } std::string audio_encoder_name = "aitum_multi_audio_encoder_"; audio_encoder_name += name; aenc = obs_audio_encoder_create(aenc_name, audio_encoder_name.c_str(), s, obs_data_get_int(settings, "audio_track"), nullptr); obs_data_release(s); obs_encoder_set_audio(aenc, obs_get_audio()); } } else { auto main_output = obs_frontend_get_streaming_output(); venc = main_output ? obs_output_get_video_encoder(main_output) : nullptr; if (!venc || !obs_output_active(main_output)) { obs_output_release(main_output); blog(LOG_WARNING, "[Aitum Multistream] failed to start stream '%s' because main was not started", obs_data_get_string(settings, "name")); QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")), QString::fromUtf8(obs_module_text("MainOutputNotActive"))); return false; } aenc = obs_output_get_audio_encoder(main_output, 0); obs_output_release(main_output); } if (!aenc || !venc) { return false; } auto server = obs_data_get_string(settings, "stream_server"); if (!server || !strlen(server)) { server = obs_data_get_string(settings, "server"); if (server && strlen(server)) obs_data_set_string(settings, "stream_server", server); } bool whip = strstr(server, "whip") != nullptr; auto s = obs_data_create(); obs_data_set_string(s, "server", server); auto key = obs_data_get_string(settings, "stream_key"); if (!key || !strlen(key)) { key = obs_data_get_string(settings, "key"); if (key && strlen(key)) obs_data_set_string(settings, "stream_key", key); } if (whip) { obs_data_set_string(s, "bearer_token", key); } else { obs_data_set_string(s, "key", key); } //use_auth //username //password std::string service_name = "aitum_multi_service_"; service_name += name; auto service = obs_service_create(whip ? "whip_custom" : "rtmp_custom", service_name.c_str(), s, nullptr); obs_data_release(s); const char *type = obs_service_get_preferred_output_type(service); if (!type) { type = "rtmp_output"; if (strncmp(server, "ftl", 3) == 0) { type = "ftl_output"; } else if (strncmp(server, "rtmp", 4) != 0) { type = "ffmpeg_mpegts_muxer"; } } std::string output_name = "aitum_multi_output_"; output_name += name; auto output = obs_output_create(type, output_name.c_str(), nullptr, nullptr); obs_output_set_service(output, service); config_t *config = obs_frontend_get_profile_config(); if (config) { obs_data_t *output_settings = obs_data_create(); obs_data_set_string(output_settings, "bind_ip", config_get_string(config, "Output", "BindIP")); obs_data_set_string(output_settings, "ip_family", config_get_string(config, "Output", "IPFamily")); obs_output_update(output, output_settings); obs_data_release(output_settings); bool useDelay = config_get_bool(config, "Output", "DelayEnable"); auto delaySec = (uint32_t)config_get_int(config, "Output", "DelaySec"); bool preserveDelay = config_get_bool(config, "Output", "DelayPreserve"); obs_output_set_delay(output, useDelay ? delaySec : 0, preserveDelay ? OBS_OUTPUT_DELAY_PRESERVE : 0); } signal_handler_t *signal = obs_output_get_signal_handler(output); signal_handler_disconnect(signal, "start", stream_output_start, this); signal_handler_disconnect(signal, "stop", stream_output_stop, this); signal_handler_connect(signal, "start", stream_output_start, this); signal_handler_connect(signal, "stop", stream_output_stop, this); //for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) { //auto venc = obs_output_get_video_encoder2(main_output, 0); //for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) { //obs_output_get_audio_encoder(main_output, 0); obs_output_set_video_encoder(output, venc); obs_output_set_audio_encoder(output, aenc, 0); obs_output_start(output); outputs.push_back({obs_data_get_string(settings, "name"), output, streamButton}); return true; } void MultistreamDock::stream_output_start(void *data, calldata_t *calldata) { auto md = (MultistreamDock *)data; auto output = (obs_output_t *)calldata_ptr(calldata, "output"); for (auto it = md->outputs.begin(); it != md->outputs.end(); it++) { if (std::get(*it) != output) continue; auto button = std::get(*it); if (!button->isChecked()) { QMetaObject::invokeMethod( button, [button, md] { button->setChecked(true); md->outputButtonStyle(button); }, Qt::QueuedConnection); } } } void MultistreamDock::stream_output_stop(void *data, calldata_t *calldata) { auto md = (MultistreamDock *)data; auto output = (obs_output_t *)calldata_ptr(calldata, "output"); for (auto it = md->outputs.begin(); it != md->outputs.end(); it++) { if (std::get(*it) != output) continue; auto button = std::get(*it); if (button->isChecked()) { QMetaObject::invokeMethod( button, [button, md] { button->setChecked(false); md->outputButtonStyle(button); }, Qt::QueuedConnection); } if (!md->exiting) QMetaObject::invokeMethod( button, [output] { obs_output_release(output); }, Qt::QueuedConnection); md->outputs.erase(it); break; } //const char *last_error = (const char *)calldata_ptr(calldata, "last_error"); } void MultistreamDock::NewerVersionAvailable(QString version) { newer_version_available = version; configButton->setStyleSheet(QString::fromUtf8("background: rgb(192,128,0);")); } void MultistreamDock::LoadVerticalOutputs(bool firstLoad) { auto ph = obs_get_proc_handler(); struct calldata cd; calldata_init(&cd); if (!proc_handler_call(ph, "aitum_vertical_get_stream_settings", &cd)) { if (firstLoad) { // only display warning on first load showVerticalWarning(verticalCanvasOutputLayout); // show warning } calldata_free(&cd); return; } if (vertical_outputs) obs_data_array_release(vertical_outputs); vertical_outputs = (obs_data_array_t *)calldata_ptr(&cd, "outputs"); calldata_free(&cd); int idx = 0; while (auto item = verticalCanvasOutputLayout->itemAt(idx)) { auto streamGroup = item->widget(); verticalCanvasOutputLayout->removeWidget(streamGroup); RemoveWidget(streamGroup); } obs_data_array_enum( vertical_outputs, [](obs_data_t *data2, void *param) { auto d = (MultistreamDock *)param; d->LoadOutput(data2, true); }, this); } void MultistreamDock::storeMainStreamEncoders() { if (!current_config) return; struct obs_video_info ovi = {0}; obs_get_video_info(&ovi); double fps = ovi.fps_den > 0 ? (double)ovi.fps_num / (double)ovi.fps_den : 0.0; auto output = obs_frontend_get_streaming_output(); bool found = false; for (auto i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) { auto encoder = obs_output_get_video_encoder2(output, i); QString settingName = QString::fromUtf8("video_encoder_description") + QString::number(i); if (encoder) { found = true; auto mainEncoderDescription = QString::number(obs_encoder_get_width(encoder)) + "x" + QString::number(obs_encoder_get_height(encoder)); auto divisor = obs_encoder_get_frame_rate_divisor(encoder); if (divisor > 0) mainEncoderDescription += QString::fromUtf8(" ") + QString::number(fps / divisor, 'g', 4) + QString::fromUtf8("fps"); auto settings = obs_encoder_get_settings(encoder); auto bitrate = settings ? obs_data_get_int(settings, "bitrate") : 0; if (bitrate > 0) mainEncoderDescription += QString::fromUtf8(" ") + QString::number(bitrate) + QString::fromUtf8("Kbps"); obs_data_release(settings); obs_data_set_string(current_config, settingName.toUtf8().constData(), mainEncoderDescription.toUtf8().constData()); } else if (found && obs_data_has_user_value(current_config, settingName.toUtf8().constData())) { obs_data_unset_user_value(current_config, settingName.toUtf8().constData()); } } obs_output_release(output); }