mirror of
https://github.com/Aitum/obs-aitum-multistream.git
synced 2024-11-25 03:33:28 +01:00
1020 lines
32 KiB
C++
1020 lines
32 KiB
C++
#include "multistream.hpp"
|
|
#include "obs-module.h"
|
|
#include "version.h"
|
|
#include <obs-frontend-api.h>
|
|
#include <QDesktopServices>
|
|
#include <QGroupBox>
|
|
#include <QLabel>
|
|
#include <QMainWindow>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QScrollArea>
|
|
#include <QVBoxLayout>
|
|
#include <util/config-file.h>
|
|
#include <util/platform.h>
|
|
#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<QMainWindow *>(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()) {
|
|
while (QLayoutItem *item = widget->layout()->takeAt(0)) {
|
|
RemoveLayoutItem(item);
|
|
}
|
|
delete widget->layout();
|
|
}
|
|
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
|
|
|
|
// 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);
|
|
}
|
|
|
|
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(30, 30));
|
|
|
|
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] {
|
|
if (obs_frontend_streaming_active()) {
|
|
obs_frontend_streaming_stop();
|
|
mainStreamButton->setChecked(false);
|
|
} 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->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<obs_output_t *>(*it));
|
|
if (venc && !obs_encoder_active(venc))
|
|
obs_encoder_set_video(venc, mainVideo);
|
|
}
|
|
}
|
|
|
|
auto service = obs_frontend_get_streaming_service();
|
|
auto url = QString::fromUtf8(obs_service_get_connect_info(service, OBS_SERVICE_CONNECT_INFO_SERVER_URL));
|
|
if (url != mainPlatformUrl) {
|
|
mainPlatformUrl = url;
|
|
mainPlatformIconLabel->setPixmap(ConfigUtils::getPlatformIconFromEndpoint(url).pixmap(30, 30));
|
|
}
|
|
|
|
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<std::string>(*it) != name)
|
|
continue;
|
|
|
|
auto active = obs_output_active(std::get<obs_output_t *>(*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<obs_output_t *>(*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<std::string>(*it) != nameChars)
|
|
continue;
|
|
if (obs_data_get_bool(output_data, "advanced")) {
|
|
auto output = std::get<obs_output_t *>(*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<QPushButton *>(*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(30, 30));
|
|
|
|
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());
|
|
if (streamButton->isChecked()) {
|
|
if (!proc_handler_call(ph, "aitum_vertical_start_stream_output", &cd))
|
|
streamButton->setChecked(false);
|
|
} else {
|
|
proc_handler_call(ph, "aitum_vertical_stop_stream_output", &cd);
|
|
}
|
|
calldata_free(&cd);
|
|
|
|
outputButtonStyle(streamButton);
|
|
});
|
|
} else {
|
|
connect(streamButton, &QPushButton::clicked, [this, streamButton, output_data] {
|
|
if (streamButton->isChecked()) {
|
|
if (!StartOutput(output_data, streamButton))
|
|
streamButton->setChecked(false);
|
|
} else {
|
|
const char *name2 = obs_data_get_string(output_data, "name");
|
|
for (auto it = outputs.begin(); it != outputs.end(); it++) {
|
|
if (std::get<std::string>(*it) != name2)
|
|
continue;
|
|
obs_queue_task(
|
|
OBS_TASK_GRAPHICS, [](void *param) { obs_output_stop((obs_output_t *)param); },
|
|
std::get<obs_output *>(*it), false);
|
|
}
|
|
}
|
|
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;
|
|
const char *name = obs_data_get_string(settings, "name");
|
|
for (auto it = outputs.begin(); it != outputs.end(); it++) {
|
|
if (std::get<std::string>(*it) != name)
|
|
continue;
|
|
auto old = std::get<obs_output_t *>(*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();
|
|
venc = obs_output_get_video_encoder2(main_output, obs_data_get_int(settings, "video_encoder_index"));
|
|
if (!venc || !obs_output_active(main_output)) {
|
|
obs_output_release(main_output);
|
|
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")),
|
|
QString::fromUtf8(obs_module_text("MainOutputNotActive")));
|
|
return false;
|
|
}
|
|
obs_output_release(main_output);
|
|
} 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();
|
|
aenc = obs_output_get_audio_encoder(main_output, obs_data_get_int(settings, "audio_encoder_index"));
|
|
if (!aenc || !obs_output_active(main_output)) {
|
|
obs_output_release(main_output);
|
|
QMessageBox::warning(this, QString::fromUtf8(obs_module_text("MainOutputNotActive")),
|
|
QString::fromUtf8(obs_module_text("MainOutputNotActive")));
|
|
return false;
|
|
}
|
|
obs_output_release(main_output);
|
|
} 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);
|
|
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<obs_output_t *>(*it) != output)
|
|
continue;
|
|
auto button = std::get<QPushButton *>(*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<obs_output_t *>(*it) != output)
|
|
continue;
|
|
auto button = std::get<QPushButton *>(*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);
|
|
}
|