obs-aitum-multistream/multistream.cpp

934 lines
29 KiB
C++
Raw Normal View History

2024-06-13 11:58:07 +02:00
#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>
2024-06-18 08:34:29 +02:00
extern "C" {
#include "file-updater.h"
}
2024-06-13 11:58:07 +02:00
OBS_DECLARE_MODULE()
OBS_MODULE_AUTHOR("Aitum");
OBS_MODULE_USE_DEFAULT_LOCALE("aitum-multistream", "en-US")
2024-06-18 08:34:29 +02:00
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;
}
2024-06-13 11:58:07 +02:00
bool obs_module_load(void)
{
//return true;
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);
2024-06-18 08:34:29 +02:00
version_update_info = update_info_create_single("[Aitum Multistream]", "OBS", "https://api.aitum.tv/multi",
version_info_downloaded, nullptr);
2024-06-13 11:58:07 +02:00
return true;
}
2024-07-11 13:54:58 +02:00
void obs_module_post_load()
{
2024-07-17 08:50:12 +02:00
if (multistream_dock)
multistream_dock->LoadVerticalOutputs(true);
2024-07-11 13:54:58 +02:00
}
2024-06-13 11:58:07 +02:00
void obs_module_unload()
{
2024-06-18 08:34:29 +02:00
update_info_destroy(version_update_info);
2024-06-13 11:58:07 +02:00
if (multistream_dock) {
delete multistream_dock;
}
}
2024-06-14 10:36:42 +02:00
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;
}
// Platform icons deciphered from endpoints
2024-07-17 08:50:12 +02:00
QIcon MultistreamDock::getPlatformFromEndpoint(QString endpoint)
{
if (endpoint.contains(QString::fromUtf8(".contribute.live-video.net")) ||
endpoint.contains(QString::fromUtf8(".twitch.tv"))) { // twitch
return platformIconTwitch;
} else if (endpoint.contains(QString::fromUtf8(".youtube.com"))) { // youtube
return platformIconYouTube;
} else if (endpoint.contains(QString::fromUtf8("fa723fc1b171.global-contribute.live-video.net"))) { // kick
2024-07-16 16:59:53 +02:00
return platformIconKick;
} else if (endpoint.contains(QString::fromUtf8(".tiktokcdn-"))) { // tiktok
return platformIconTikTok;
} else if (endpoint.contains(QString::fromUtf8(".pscp.tv"))) { // twitter
return platformIconTwitter;
2024-07-17 08:50:12 +02:00
} else if (endpoint.contains(QString::fromUtf8("livepush.trovo.live"))) { // trovo
return platformIconTrovo;
} else if (endpoint.contains(QString::fromUtf8(".facebook.com"))) { // facebook
return platformIconFacebook;
} else { // unknown
return platformIconUnknown;
}
}
// Output button styling
2024-07-15 14:18:54 +02:00
void MultistreamDock::outputButtonStyle(QPushButton *button)
2024-07-15 14:10:49 +02:00
{
button->setMinimumHeight(24);
std::string baseStyles = "min-width: 30px; padding: 2px 10px; ";
button->setStyleSheet(QString::fromUtf8(baseStyles + (button->isChecked() ? "background: rgb(0,210,153);" : "")));
2024-07-15 14:18:54 +02:00
button->setIcon(button->isChecked() ? streamActiveIcon : streamInactiveIcon);
}
// Common styling things here
2024-07-17 08:50:12 +02:00
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
2024-07-17 08:50:12 +02:00
auto outputTitleStyle = QString("QLabel{}"); // "Built -in stream"
2024-07-15 14:10:49 +02:00
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
2024-07-15 14:10:49 +02:00
void showVerticalWarning(QVBoxLayout *verticalLayout)
{
auto verticalWarning = new QWidget;
2024-07-16 13:24:32 +02:00
verticalWarning->setContentsMargins(0, 0, 0, 0);
2024-07-15 14:10:49 +02:00
auto verticalWarningLayout = new QVBoxLayout;
2024-07-16 13:24:32 +02:00
verticalWarningLayout->setContentsMargins(0, 0, 0, 0);
2024-07-15 14:10:49 +02:00
auto label = new QLabel(QString::fromUtf8(obs_module_text("NoVerticalWarning")));
2024-07-16 13:24:32 +02:00
label->setStyleSheet(QString("padding: 0px;"));
2024-07-15 14:10:49 +02:00
label->setWordWrap(true);
verticalWarningLayout->addWidget(label);
verticalWarning->setLayout(verticalWarningLayout);
2024-07-15 14:10:49 +02:00
verticalLayout->addWidget(verticalWarning);
}
2024-06-13 11:58:07 +02:00
MultistreamDock::MultistreamDock(QWidget *parent) : QFrame(parent)
{
2024-07-15 14:10:49 +02:00
// Main layout
auto mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
2024-07-16 13:24:32 +02:00
mainLayout->setSpacing(0);
setLayout(mainLayout);
2024-07-15 14:10:49 +02:00
2024-06-13 11:58:07 +02:00
auto t = new QWidget;
auto tl = new QVBoxLayout;
2024-07-15 14:10:49 +02:00
tl->setSpacing(8); // between canvas groups
2024-07-16 13:24:32 +02:00
tl->setContentsMargins(0, 0, 0, 0);
2024-07-15 14:10:49 +02:00
t->setStyleSheet(QString("padding: 0px; margin:0px;"));
2024-06-13 11:58:07 +02:00
t->setLayout(tl);
2024-07-15 14:10:49 +02:00
// Group for built in canvas
auto mainCanvasGroup = new QGroupBox;
2024-07-15 14:10:49 +02:00
mainCanvasGroup->setStyleSheet(canvasGroupStyle);
2024-07-17 08:50:12 +02:00
2024-06-13 11:58:07 +02:00
mainCanvasLayout = new QVBoxLayout;
2024-07-15 14:10:49 +02:00
mainCanvasLayout->setSpacing(4); // between outputs on main canvas
// Layout for header row
auto mainCanvasTitleRowLayout = new QHBoxLayout;
2024-07-17 08:50:12 +02:00
auto mainCanvasLabel = new QLabel(QString::fromUtf8(obs_module_text("MainCanvas")));
mainCanvasLabel->setStyleSheet(canvasGroupHeaderStyle);
mainCanvasTitleRowLayout->addWidget(mainCanvasLabel);
2024-07-17 08:50:12 +02:00
mainCanvasLayout->addLayout(mainCanvasTitleRowLayout);
// We store the actual outputs here
mainCanvasOutputLayout = new QVBoxLayout;
mainCanvasOutputLayout->setSpacing(4); // between outputs on main canvas
2024-07-17 08:50:12 +02:00
2024-06-13 11:58:07 +02:00
auto mainStreamGroup = new QGroupBox;
2024-07-15 14:10:49 +02:00
mainStreamGroup->setStyleSheet(outputGroupStyle);
2024-06-13 11:58:07 +02:00
auto mainStreamLayout = new QVBoxLayout;
auto l2 = new QHBoxLayout;
2024-07-15 14:10:49 +02:00
// Label for built in stream
auto bisHeaderLabel = new QLabel(QString::fromUtf8(obs_module_text("BuiltinStream")));
bisHeaderLabel->setStyleSheet(outputTitleStyle);
// blank because we're not pulling settings through from bis, fix this
2024-07-17 08:09:30 +02:00
mainPlatformIconLabel = new QLabel;
auto platformIcon = getPlatformFromEndpoint(QString::fromUtf8(""));
2024-07-17 08:50:12 +02:00
2024-07-17 08:09:30 +02:00
mainPlatformIconLabel->setPixmap(platformIcon.pixmap(30, 30));
2024-07-17 08:50:12 +02:00
2024-07-17 08:09:30 +02:00
l2->addWidget(mainPlatformIconLabel);
l2->addWidget(bisHeaderLabel, 1);
2024-07-15 14:10:49 +02:00
2024-06-13 11:58:07 +02:00
mainStreamButton = new QPushButton;
mainStreamButton->setObjectName(QStringLiteral("canvasStream"));
mainStreamButton->setIcon(streamInactiveIcon);
mainStreamButton->setCheckable(true);
mainStreamButton->setChecked(false);
2024-07-15 14:10:49 +02:00
outputButtonStyle(mainStreamButton);
2024-06-13 11:58:07 +02:00
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);
}
2024-07-15 14:10:49 +02:00
outputButtonStyle(mainStreamButton);
2024-06-13 11:58:07 +02:00
});
//streamButton->setSizePolicy(sp2);
mainStreamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream")));
l2->addWidget(mainStreamButton);
mainStreamLayout->addLayout(l2);
mainStreamGroup->setLayout(mainStreamLayout);
mainCanvasOutputLayout->addWidget(mainStreamGroup);
2024-07-17 08:50:12 +02:00
mainCanvasLayout->addLayout(mainCanvasOutputLayout);
2024-06-13 11:58:07 +02:00
mainCanvasGroup->setLayout(mainCanvasLayout);
tl->addWidget(mainCanvasGroup);
2024-07-15 14:10:49 +02:00
// VERTICAL
auto verticalCanvasGroup = new QGroupBox;
2024-07-15 14:10:49 +02:00
verticalCanvasGroup->setStyleSheet(canvasGroupStyle);
2024-07-11 13:54:58 +02:00
verticalCanvasLayout = new QVBoxLayout;
verticalCanvasGroup->setLayout(verticalCanvasLayout);
tl->addWidget(verticalCanvasGroup);
tl->addStretch(1);
2024-06-13 11:58:07 +02:00
// Layout for header row
auto verticalCanvasTitleRowLayout = new QHBoxLayout;
2024-07-17 08:50:12 +02:00
auto verticalCanvasLabel = new QLabel(QString::fromUtf8(obs_module_text("VerticalCanvas")));
verticalCanvasLabel->setStyleSheet(canvasGroupHeaderStyle);
verticalCanvasTitleRowLayout->addWidget(verticalCanvasLabel);
2024-07-17 08:50:12 +02:00
verticalCanvasLayout->addLayout(verticalCanvasTitleRowLayout);
2024-07-17 08:50:12 +02:00
// We store the actual outputs here
verticalCanvasOutputLayout = new QVBoxLayout;
verticalCanvasOutputLayout->setSpacing(4); // between outputs on vertical canvas
2024-07-17 08:50:12 +02:00
verticalCanvasLayout->addLayout(verticalCanvasOutputLayout); // Add output layout to parent
2024-07-17 08:50:12 +02:00
2024-06-13 11:58:07 +02:00
//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);
2024-06-13 11:58:07 +02:00
2024-07-16 13:24:32 +02:00
// Bottom Button Row
2024-06-13 11:58:07 +02:00
auto buttonRow = new QHBoxLayout;
2024-07-15 14:38:40 +02:00
buttonRow->setContentsMargins(8, 6, 8, 4);
2024-07-16 13:24:32 +02:00
buttonRow->setSpacing(8);
// Config Button
2024-06-18 08:34:29 +02:00
configButton = new QPushButton;
2024-06-13 11:58:07 +02:00
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);
2024-07-11 13:54:58 +02:00
configDialog->LoadVerticalSettings();
2024-07-16 09:52:05 +02:00
configDialog->LoadOutputStats(&oldVideo);
2024-06-18 08:34:29 +02:00
configDialog->SetNewerVersion(newer_version_available);
2024-06-13 11:58:07 +02:00
configDialog->setResult(QDialog::Rejected);
if (configDialog->exec() == QDialog::Accepted) {
if (current_config) {
obs_data_apply(current_config, settings);
obs_data_release(settings);
SaveSettings();
LoadSettings();
2024-07-11 13:54:58 +02:00
configDialog->SaveVerticalSettings();
LoadVerticalOutputs(false);
2024-06-13 11:58:07 +02:00
} else {
current_config = settings;
}
} else {
obs_data_release(settings);
}
});
buttonRow->addWidget(configButton);
2024-07-16 13:24:32 +02:00
// Aitum Button
2024-06-13 11:58:07 +02:00
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);
2024-06-13 11:58:07 +02:00
obs_frontend_add_event_callback(frontend_event, this);
2024-07-16 09:52:05 +02:00
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(it->second);
if (venc && !obs_encoder_active(venc))
obs_encoder_set_video(venc, mainVideo);
}
}
2024-07-16 13:24:32 +02:00
2024-07-17 08:09:30 +02:00
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;
auto platformIcon = getPlatformFromEndpoint(url);
mainPlatformIconLabel->setPixmap(platformIcon.pixmap(30, 30));
}
2024-07-17 08:50:12 +02:00
2024-07-16 13:24:32 +02:00
int idx = 1;
while (auto item = mainCanvasOutputLayout->itemAt(idx++)) {
2024-07-16 13:24:32 +02:00
auto streamGroup = item->widget();
std::string name = streamGroup->objectName().toUtf8().constData();
if (name.empty())
continue;
auto it = outputs.find(name);
if (it != outputs.end() && it->second) {
auto active = obs_output_active(it->second);
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++)) {
2024-07-16 13:24:32 +02:00
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);
2024-07-16 09:52:05 +02:00
});
2024-07-16 13:24:32 +02:00
videoCheckTimer.start(500);
2024-06-13 11:58:07 +02:00
}
MultistreamDock::~MultistreamDock()
{
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 ||
event == OBS_FRONTEND_EVENT_EXIT) {
md->SaveSettings();
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING || event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
md->mainStreamButton->setChecked(true);
2024-07-15 14:18:54 +02:00
md->outputButtonStyle(md->mainStreamButton);
2024-07-15 14:10:49 +02:00
2024-06-13 11:58:07 +02:00
md->mainStreamButton->setIcon(md->streamActiveIcon);
} else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING || event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
md->mainStreamButton->setChecked(false);
2024-07-15 14:18:54 +02:00
md->outputButtonStyle(md->mainStreamButton);
2024-06-13 11:58:07 +02:00
}
}
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 outputs = obs_data_get_array(current_config, "outputs");
auto count = obs_data_array_count(outputs);
int idx = 1;
while (auto item = mainCanvasOutputLayout->itemAt(idx)) {
2024-06-13 11:58:07 +02:00
auto streamGroup = item->widget();
auto name = streamGroup->objectName();
bool found = false;
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(outputs, i);
if (QString::fromUtf8(obs_data_get_string(item, "name")) == name) {
found = true;
}
obs_data_release(item);
}
if (!found) {
mainCanvasOutputLayout->removeWidget(streamGroup);
2024-06-14 10:36:42 +02:00
RemoveWidget(streamGroup);
2024-06-13 11:58:07 +02:00
} else {
idx++;
}
}
obs_data_array_enum(
outputs,
[](obs_data_t *data, void *param) {
auto d = (MultistreamDock *)param;
2024-07-11 13:54:58 +02:00
d->LoadOutput(data, false);
2024-06-13 11:58:07 +02:00
},
this);
obs_data_array_release(outputs);
}
2024-07-11 13:54:58 +02:00
void MultistreamDock::LoadOutput(obs_data_t *data, bool vertical)
2024-06-13 11:58:07 +02:00
{
auto name = QString::fromUtf8(obs_data_get_string(data, "name"));
auto endpoint = QString::fromUtf8(obs_data_get_string(data, "stream_server"));
2024-07-15 13:58:18 +02:00
if (vertical) {
for (int i = 1; i < verticalCanvasOutputLayout->count(); i++) {
auto item = verticalCanvasOutputLayout->itemAt(i);
2024-07-15 13:58:18 +02:00
auto oName = item->widget()->objectName();
if (oName == name) {
return;
}
}
} else {
for (int i = 1; i < mainCanvasOutputLayout->count(); i++) {
auto item = mainCanvasOutputLayout->itemAt(i);
2024-07-15 13:58:18 +02:00
auto oName = item->widget()->objectName();
if (oName == name) {
return;
}
2024-06-13 11:58:07 +02:00
}
}
auto streamGroup = new QGroupBox;
2024-07-15 14:10:49 +02:00
streamGroup->setStyleSheet(outputGroupStyle);
2024-06-13 11:58:07 +02:00
streamGroup->setObjectName(name);
auto streamLayout = new QVBoxLayout;
auto l2 = new QHBoxLayout;
2024-07-17 08:50:12 +02:00
auto platformIconLabel = new QLabel;
auto platformIcon = getPlatformFromEndpoint(endpoint);
2024-07-17 08:50:12 +02:00
platformIconLabel->setPixmap(platformIcon.pixmap(30, 30));
2024-07-17 08:50:12 +02:00
l2->addWidget(platformIconLabel);
2024-07-17 08:50:12 +02:00
2024-06-13 11:58:07 +02:00
l2->addWidget(new QLabel(name), 1);
auto streamButton = new QPushButton;
streamButton->setMinimumHeight(30);
streamButton->setObjectName(QStringLiteral("canvasStream"));
streamButton->setIcon(streamInactiveIcon);
streamButton->setCheckable(true);
streamButton->setChecked(false);
2024-07-15 14:10:49 +02:00
outputButtonStyle(streamButton);
2024-07-11 13:54:58 +02:00
if (vertical) {
connect(streamButton, &QPushButton::clicked, [this, streamButton, data] {
auto ph = obs_get_proc_handler();
struct calldata cd;
calldata_init(&cd);
calldata_set_string(&cd, "name", obs_data_get_string(data, "name"));
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);
2024-07-15 14:10:49 +02:00
outputButtonStyle(streamButton);
2024-07-11 13:54:58 +02:00
});
} else {
connect(streamButton, &QPushButton::clicked, [this, streamButton, data] {
if (streamButton->isChecked()) {
if (!StartOutput(data, streamButton))
streamButton->setChecked(false);
} else {
const char *name = obs_data_get_string(data, "name");
auto it = outputs.find(name);
if (it != outputs.end()) {
obs_output_stop(it->second);
obs_output_release(it->second);
outputs.erase(it);
}
}
2024-07-15 14:10:49 +02:00
outputButtonStyle(streamButton);
2024-07-11 13:54:58 +02:00
});
}
2024-06-13 11:58:07 +02:00
//streamButton->setSizePolicy(sp2);
streamButton->setToolTip(QString::fromUtf8(obs_module_text("Stream")));
l2->addWidget(streamButton);
streamLayout->addLayout(l2);
streamGroup->setLayout(streamLayout);
2024-07-11 13:54:58 +02:00
if (vertical)
verticalCanvasOutputLayout->addWidget(streamGroup);
2024-07-11 13:54:58 +02:00
else
mainCanvasOutputLayout->addWidget(streamGroup);
2024-06-13 11:58:07 +02:00
}
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)
{
2024-07-11 13:54:58 +02:00
if (!settings)
return false;
2024-06-13 11:58:07 +02:00
const char *name = obs_data_get_string(settings, "name");
auto old = outputs.find(name);
if (old != outputs.end()) {
auto service = obs_output_get_service(old->second);
if (obs_output_active(old->second)) {
obs_output_stop(old->second);
}
obs_output_release(old->second);
obs_service_release(service);
outputs.erase(old);
}
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);
2024-06-13 11:58:07 +02:00
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, divisor);
bool scale = obs_data_get_bool(settings, "scale");
if (scale) {
2024-06-13 17:17:25 +02:00
obs_encoder_set_scaled_size(venc, obs_data_get_int(settings, "width"),
obs_data_get_int(settings, "height"));
2024-06-13 11:58:07 +02:00
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(venc_name, audio_encoder_name.c_str(), s,
2024-06-18 08:34:29 +02:00
obs_data_get_int(settings, "audio_track"), nullptr);
2024-06-13 11:58:07 +02:00
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;
}
2024-07-11 13:54:58 +02:00
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);
}
2024-06-18 08:34:29 +02:00
bool whip = strstr(server, "whip") != nullptr;
2024-06-13 11:58:07 +02:00
auto s = obs_data_create();
2024-06-18 08:34:29 +02:00
obs_data_set_string(s, "server", server);
2024-07-11 13:54:58 +02:00
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);
}
2024-06-18 08:34:29 +02:00
if (whip) {
2024-07-11 13:54:58 +02:00
obs_data_set_string(s, "bearer_token", key);
2024-06-18 08:34:29 +02:00
} else {
2024-07-11 13:54:58 +02:00
obs_data_set_string(s, "key", key);
2024-06-18 08:34:29 +02:00
}
2024-06-13 11:58:07 +02:00
//use_auth
//username
//password
std::string service_name = "aitum_multi_service_";
service_name += name;
2024-06-18 08:34:29 +02:00
auto service = obs_service_create(whip ? "whip_custom" : "rtmp_custom", service_name.c_str(), s, nullptr);
2024-06-13 11:58:07 +02:00
obs_data_release(s);
const char *type = obs_service_get_preferred_output_type(service);
if (!type) {
type = "rtmp_output";
2024-06-18 08:34:29 +02:00
if (strncmp(server, "ftl", 3) == 0) {
2024-06-13 11:58:07 +02:00
type = "ftl_output";
2024-06-18 08:34:29 +02:00
} else if (strncmp(server, "rtmp", 4) != 0) {
2024-06-13 11:58:07 +02:00
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);
2024-06-13 11:58:07 +02:00
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");
int delaySec = 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);
2024-07-15 13:58:18 +02:00
signal_handler_disconnect(signal, "start", stream_output_start, streamButton);
signal_handler_disconnect(signal, "stop", stream_output_stop, streamButton);
2024-06-13 11:58:07 +02:00
signal_handler_connect(signal, "start", stream_output_start, streamButton);
signal_handler_connect(signal, "stop", stream_output_stop, streamButton);
//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[obs_data_get_string(settings, "name")] = output;
return true;
}
void MultistreamDock::stream_output_start(void *data, calldata_t *calldata)
{
UNUSED_PARAMETER(calldata);
auto streamButton = (QPushButton *)data;
streamButton->setChecked(true);
}
void MultistreamDock::stream_output_stop(void *data, calldata_t *calldata)
{
2024-06-13 17:17:25 +02:00
UNUSED_PARAMETER(calldata);
2024-06-13 11:58:07 +02:00
auto streamButton = (QPushButton *)data;
2024-06-13 17:17:25 +02:00
//const char *last_error = (const char *)calldata_ptr(calldata, "last_error");
2024-06-13 11:58:07 +02:00
streamButton->setChecked(false);
}
2024-06-18 08:34:29 +02:00
void MultistreamDock::NewerVersionAvailable(QString version)
{
newer_version_available = version;
configButton->setStyleSheet(QString::fromUtf8("background: rgb(192,128,0);"));
}
2024-07-11 13:54:58 +02:00
void MultistreamDock::LoadVerticalOutputs(bool firstLoad)
2024-07-11 13:54:58 +02:00
{
auto ph = obs_get_proc_handler();
struct calldata cd;
calldata_init(&cd);
if (!proc_handler_call(ph, "aitum_vertical_get_stream_settings", &cd)) {
2024-07-17 08:50:12 +02:00
if (firstLoad) { // only display warning on first load
showVerticalWarning(verticalCanvasOutputLayout); // show warning
}
2024-07-11 13:54:58 +02:00
calldata_free(&cd);
return;
}
2024-07-15 14:10:49 +02:00
2024-07-11 13:54:58 +02:00
auto outputs = (obs_data_array_t *)calldata_ptr(&cd, "outputs");
calldata_free(&cd);
auto count = obs_data_array_count(outputs);
int idx = 1;
while (auto item = verticalCanvasOutputLayout->itemAt(idx)) {
2024-07-11 13:54:58 +02:00
auto streamGroup = item->widget();
auto name = streamGroup->objectName();
bool found = false;
for (size_t i = 0; i < count; i++) {
auto item = obs_data_array_item(outputs, i);
if (QString::fromUtf8(obs_data_get_string(item, "name")) == name) {
found = true;
}
obs_data_release(item);
}
if (!found) {
verticalCanvasOutputLayout->removeWidget(streamGroup);
2024-07-11 13:54:58 +02:00
RemoveWidget(streamGroup);
} else {
idx++;
}
}
obs_data_array_enum(
outputs,
[](obs_data_t *data, void *param) {
auto d = (MultistreamDock *)param;
d->LoadOutput(data, true);
},
this);
obs_data_array_release(outputs);
}