vulcast-vertical/config-dialog.cpp
Vulcast a9d0ebe0d3
Some checks are pending
Build Windows DLL / Build for Windows (push) Waiting to run
Add Vulcast Account tab - shows linked platforms from API
- New 'Vulcast' tab in settings dialog
- API key input with show/hide toggle
- Connect button fetches platform list from /api/plugin/status
- Shows connected platforms with horizontal/vertical destinations
- Shows stream status (live/idle)
- API key saved in OBS profile config
2026-06-30 20:39:11 +01:00

2069 lines
78 KiB
C++

#include "config-dialog.hpp"
#include <QCheckBox>
#include <QComboBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QScrollArea>
#include <QSpinBox>
#include <QStackedWidget>
#include <QTextEdit>
#include <QRadioButton>
#include <QPlainTextEdit>
#include <QCompleter>
#include <QDesktopServices>
#include <QUrl>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "hotkey-edit.hpp"
#include "obs-module.h"
#include "version.h"
#include "vertical-canvas.hpp"
#include <util/dstr.h>
#include <util/config-file.h>
OBSBasicSettings::OBSBasicSettings(CanvasDock *canvas_dock, QMainWindow *parent) : QDialog(parent), canvasDock(canvas_dock)
{
setMinimumWidth(983);
setMinimumHeight(480);
listWidget = new QListWidget(this);
listWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding);
listWidget->setMaximumWidth(180);
QListWidgetItem *listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/general.svg")));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("General")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/stream.svg")));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("Streaming")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/settings/images/settings/output.svg")));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("Recording")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(canvasDock->GetIconFromType(OBS_ICON_TYPE_UNKNOWN));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("Help")));
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/vulcast/media/logo.png")));
listwidgetitem->setText(QString::fromUtf8(obs_module_text("SupportButton")));
listWidget->setCurrentRow(0);
listWidget->setSpacing(1);
auto settingsPages = new QStackedWidget;
settingsPages->setContentsMargins(0, 0, 0, 0);
settingsPages->setFrameShape(QFrame::NoFrame);
settingsPages->setLineWidth(0);
QWidget *generalPage = new QWidget;
QScrollArea *scrollArea = new QScrollArea;
scrollArea->setWidget(generalPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
auto streamingPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(streamingPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
auto recordingPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(recordingPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
auto helpPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(helpPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
// Support page
QWidget *supportPage = new QWidget;
auto supportPageLayout = new QVBoxLayout;
supportPage->setLayout(supportPageLayout);
auto supportInfoBox = new QGroupBox(QString::fromUtf8(obs_module_text("SupportTitle")));
supportInfoBox->setStyleSheet("padding-top: 12px");
auto supportLayout = new QVBoxLayout;
supportInfoBox->setLayout(supportLayout);
auto supportLabel = new QLabel(QString::fromUtf8(obs_module_text("SupportText")));
supportLabel->setStyleSheet("font-size: 14px");
supportLabel->setWordWrap(true);
supportLabel->setTextFormat(Qt::RichText);
supportLabel->setOpenExternalLinks(true);
supportLayout->addWidget(supportLabel, 1);
supportPageLayout->addWidget(supportInfoBox, 1, Qt::AlignTop);
settingsPages->addWidget(supportPage);
// Vulcast Account page
listwidgetitem = new QListWidgetItem(listWidget);
listwidgetitem->setIcon(QIcon(QString::fromUtf8(":/vulcast/media/logo.png")));
listwidgetitem->setText(QString::fromUtf8("Vulcast"));
QWidget *accountPage = new QWidget;
scrollArea = new QScrollArea;
scrollArea->setWidget(accountPage);
scrollArea->setWidgetResizable(true);
scrollArea->setLineWidth(0);
scrollArea->setFrameShape(QFrame::NoFrame);
settingsPages->addWidget(scrollArea);
networkManager = new QNetworkAccessManager(this);
auto accountGroup = new QGroupBox;
accountGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
auto accountLayout = new QFormLayout;
accountLayout->setContentsMargins(9, 2, 9, 9);
accountLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
accountLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
accountGroup->setLayout(accountLayout);
auto account_title_layout = new QHBoxLayout;
auto account_title = new QLabel(QString::fromUtf8("Vulcast Account"));
account_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
account_title_layout->addWidget(account_title, 0, Qt::AlignLeft);
auto dashboard_link = new QLabel(QString::fromUtf8("<a href=\"https://vulcast.xyz/dashboard/settings\">") +
QString::fromUtf8("Get API Key") + QString::fromUtf8("</a>"));
dashboard_link->setOpenExternalLinks(true);
account_title_layout->addWidget(dashboard_link, 0, Qt::AlignRight);
accountLayout->addRow(account_title_layout);
auto apiInfoLabel = new QLabel(QString::fromUtf8("Enter your Vulcast API key to see linked platforms and stream settings. Generate one at vulcast.xyz/dashboard/settings."));
apiInfoLabel->setWordWrap(true);
apiInfoLabel->setStyleSheet("color: #999; font-size: 12px;");
accountLayout->addRow(apiInfoLabel);
apiKeyInput = new QLineEdit;
apiKeyInput->setEchoMode(QLineEdit::Password);
apiKeyInput->setPlaceholderText(QString::fromUtf8("vcast_..."));
QPushButton *showApiKey = new QPushButton(QString::fromUtf8("Show"));
showApiKey->setCheckable(true);
connect(showApiKey, &QAbstractButton::toggled, [showApiKey, this](bool checked) {
showApiKey->setText(checked ? QString::fromUtf8("Hide") : QString::fromUtf8("Show"));
apiKeyInput->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password);
});
auto apiKeyLayout = new QHBoxLayout;
apiKeyLayout->addWidget(apiKeyInput);
apiKeyLayout->addWidget(showApiKey);
accountLayout->addRow(QString::fromUtf8("API Key"), apiKeyLayout);
auto connectButton = new QPushButton(QString::fromUtf8("Connect"));
connect(connectButton, &QPushButton::clicked, [this] { FetchPlatformStatus(); });
accountLayout->addRow(connectButton);
apiStatus = new QLabel;
apiStatus->setStyleSheet("font-size: 12px;");
apiStatus->setVisible(false);
accountLayout->addRow(apiStatus);
auto platformGroup = new QGroupBox(QString::fromUtf8("Linked Platforms"));
platformGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
auto platformLayout = new QVBoxLayout;
platformLayout->setContentsMargins(9, 2, 9, 9);
platformGroup->setLayout(platformLayout);
platformList = new QListWidget;
platformList->setMinimumHeight(150);
platformList->setStyleSheet("QListWidget { border: 1px solid #333; }");
platformLayout->addWidget(platformList);
auto accountPageLayout = new QVBoxLayout;
accountPageLayout->addWidget(accountGroup);
accountPageLayout->addWidget(platformGroup);
accountPageLayout->addStretch();
accountPage->setLayout(accountPageLayout);
connect(listWidget, &QListWidget::currentRowChanged, settingsPages, &QStackedWidget::setCurrentIndex);
auto generalGroup = new QGroupBox;
generalGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
auto generalLayout = new QFormLayout;
generalLayout->setContentsMargins(9, 2, 9, 9);
generalLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
generalLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
generalGroup->setLayout(generalLayout);
auto general_title_layout = new QHBoxLayout;
auto general_title = new QLabel(QString::fromUtf8(obs_module_text("General")));
general_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
general_title_layout->addWidget(general_title, 0, Qt::AlignLeft);
auto guide_link = new QLabel(QString::fromUtf8("<a href=\"https://vulcast.tv/docs/vertical/general\">") +
QString::fromUtf8(obs_module_text("ViewGuide")) + QString::fromUtf8("</a>"));
guide_link->setOpenExternalLinks(true);
general_title_layout->addWidget(guide_link, 0, Qt::AlignRight);
generalLayout->addRow(general_title_layout);
resolution = new QComboBox;
resolution->setEditable(true);
resolution->addItem("720x1280");
resolution->addItem("1080x1920");
resolution->addItem("1080x1350");
resolution->addItem("1280x720");
resolution->addItem("1920x1080");
resolution->addItem("2560x1440");
resolution->addItem("3840x2160");
generalLayout->addRow(QString::fromUtf8(obs_module_text("Resolution")), resolution);
audioBitrate = new QComboBox;
audioBitrate->addItem("64", QVariant(64));
audioBitrate->addItem("96", QVariant(96));
audioBitrate->addItem("128", QVariant(128));
audioBitrate->addItem("160", QVariant(160));
audioBitrate->addItem("192", QVariant(192));
audioBitrate->addItem("224", QVariant(224));
audioBitrate->addItem("256", QVariant(256));
audioBitrate->addItem("288", QVariant(288));
audioBitrate->addItem("320", QVariant(320));
audioBitrate->setCurrentText("160");
generalLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.AudioBitrate")),
audioBitrate);
virtualCameraMode = new QComboBox;
virtualCameraMode->addItem(QString::fromUtf8(obs_module_text("VirtualCameraModeVertical")),
QVariant(VIRTUAL_CAMERA_VERTICAL));
virtualCameraMode->addItem(QString::fromUtf8(obs_module_text("VirtualCameraModeMain")), QVariant(VIRTUAL_CAMERA_MAIN));
virtualCameraMode->addItem(QString::fromUtf8(obs_module_text("VirtualCameraModeBoth")), QVariant(VIRTUAL_CAMERA_BOTH));
generalLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.VCam.VirtualCamera")), virtualCameraMode);
auto backtrackGroup = new QGroupBox;
backtrackGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
auto backtrackLayout = new QFormLayout;
backtrackLayout->setContentsMargins(9, 2, 9, 9);
backtrackLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
backtrackLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
backtrackGroup->setLayout(backtrackLayout);
auto backtrack_title_layout = new QHBoxLayout;
auto backtrack_title = new QLabel(QString::fromUtf8(obs_module_text("Backtrack")));
backtrack_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
backtrack_title_layout->addWidget(backtrack_title, 0, Qt::AlignLeft);
guide_link = new QLabel(QString::fromUtf8("<a href=\"https://vulcast.tv/docs/vertical/backtrack\">") +
QString::fromUtf8(obs_module_text("ViewGuide")) + QString::fromUtf8("</a>"));
guide_link->setOpenExternalLinks(true);
backtrack_title_layout->addWidget(guide_link, 0, Qt::AlignRight);
backtrackLayout->addRow(backtrack_title_layout);
backtrackLayout->addRow(new QLabel(QString::fromUtf8(obs_module_text("BacktrackEncoder"))));
backtrackClip = new QCheckBox(QString::fromUtf8(obs_module_text("BacktrackEnable")));
backtrackLayout->addWidget(backtrackClip);
backtrackDuration = new QSpinBox;
backtrackDuration->setSuffix(" s");
backtrackDuration->setMinimum(5);
backtrackDuration->setMaximum(21600);
backtrackLayout->addRow(QString::fromUtf8(obs_module_text("BacktrackDuration")), backtrackDuration);
QLayout *backtrackPathLayout = new QHBoxLayout;
backtrackPath = new QLineEdit();
backtrackPath->setReadOnly(true);
auto button = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Browse")));
button->setProperty("themeID", "settingsButtons");
button->setProperty("class", "icon-gear");
button->setAutoDefault(false);
connect(button, &QPushButton::clicked, [this] {
const QString dir = QFileDialog::getExistingDirectory(this, QString::fromUtf8(obs_module_text("BacktrackPath")),
backtrackPath->text(),
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (dir.isEmpty())
return;
backtrackPath->setText(dir);
});
backtrackPathLayout->addWidget(backtrackPath);
backtrackPathLayout->addWidget(button);
backtrackLayout->addRow(QString::fromUtf8(obs_module_text("BacktrackPath")), backtrackPathLayout);
auto replayHotkeys = GetHotKeysFromOutput(canvasDock->replayOutput);
for (auto &hotkey : replayHotkeys) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
if (strcmp(hn, "ReplayBuffer.Save") == 0) {
backtrackLayout->addRow(QString::fromUtf8(obs_module_text("SaveBacktrackHotkey")), hw);
} else {
backtrackLayout->addRow(QString::fromUtf8(obs_hotkey_get_description(hotkey)), hw);
}
hotkeys.push_back(hw);
}
OBSHotkeyWidget *otherHotkey = nullptr;
auto hotkey = GetHotkeyByName("VerticalCanvasDockStartBacktrack");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
otherHotkey = hw;
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StartBacktrackHotkey")));
hw->label = label;
backtrackLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
hotkey = GetHotkeyByName("VerticalCanvasDockStopBacktrack");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StopBacktrackHotkey")));
hw->label = label;
backtrackLayout->addRow(label, hw);
hotkeys.push_back(hw);
if (otherHotkey) {
hw->label->pairPartner = otherHotkey->label;
otherHotkey->label->pairPartner = hw->label;
}
}
auto maxWidth = 180;
for (int row = 0; row < generalLayout->rowCount(); row++) {
auto item = generalLayout->itemAt(row, QFormLayout::LabelRole);
if (!item)
continue;
auto label = dynamic_cast<QLabel *>(item->widget());
if (!label)
continue;
label->setFixedWidth(maxWidth);
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
}
for (int row = 0; row < backtrackLayout->rowCount(); row++) {
auto item = backtrackLayout->itemAt(row, QFormLayout::LabelRole);
if (!item)
continue;
auto label = dynamic_cast<QLabel *>(item->widget());
if (!label)
continue;
label->setFixedWidth(maxWidth);
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
}
auto vb = new QVBoxLayout;
vb->setContentsMargins(0, 0, 0, 0);
vb->addWidget(generalGroup);
vb->addWidget(backtrackGroup);
vb->addStretch();
generalPage->setLayout(vb);
auto streamingGroup = new QGroupBox;
streamingGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
streamingGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
streamingLayout = new QFormLayout;
streamingLayout->setContentsMargins(9, 2, 9, 9);
streamingLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
streamingLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
auto streaming_title_layout = new QHBoxLayout;
auto streaming_title = new QLabel(QString::fromUtf8(obs_module_text("Streaming")));
streaming_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
streaming_title_layout->addWidget(streaming_title, 0, Qt::AlignLeft);
guide_link = new QLabel(QString::fromUtf8("<a href=\"https://vulcast.tv/docs/vertical/streaming\">") +
QString::fromUtf8(obs_module_text("ViewGuide")) + QString::fromUtf8("</a>"));
guide_link->setOpenExternalLinks(true);
streaming_title_layout->addWidget(guide_link, 0, Qt::AlignRight);
streamingLayout->addRow(streaming_title_layout);
if (!canvasDock->disable_stream_settings) {
auto hl = new QHBoxLayout;
auto addButton = new QPushButton(QIcon(QString::fromUtf8(":/res/images/plus.svg")),
QString::fromUtf8(obs_frontend_get_locale_string("Add")));
addButton->setProperty("themeID", QVariant(QString::fromUtf8("addIconSmall")));
addButton->setProperty("class", "icon-plus");
connect(addButton, &QPushButton::clicked, [this] { AddServer(); });
hl->addWidget(addButton);
auto removeButton = new QPushButton(QIcon(":/res/images/minus.svg"),
QString::fromUtf8(obs_frontend_get_locale_string("Remove")));
removeButton->setProperty("themeID", QVariant(QString::fromUtf8("removeIconSmall")));
removeButton->setProperty("class", "icon-minus");
connect(removeButton, &QPushButton::clicked, [this] {
if (servers.size() <= 1)
return;
auto idx = (int)servers.size();
streamingLayout->removeRow(idx);
server_names.pop_back();
servers.pop_back();
keys.pop_back();
servers_enabled.pop_back();
});
hl->addWidget(removeButton);
streamingLayout->addRow(hl);
}
streamingVideoBitrate = new QSpinBox;
streamingVideoBitrate->setSuffix(" Kbps");
streamingVideoBitrate->setMinimum(2000);
streamingVideoBitrate->setMaximum(1000000);
streamingLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.VideoBitrate")),
streamingVideoBitrate);
streamingMatchMain = new QCheckBox(QString::fromUtf8(obs_module_text("StreamingMatchMain")));
streamingLayout->addWidget(streamingMatchMain);
otherHotkey = nullptr;
hotkey = GetHotkeyByName("VerticalCanvasDockStartStreaming");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
otherHotkey = hw;
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StartStreamingHotkey")));
hw->label = label;
streamingLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
hotkey = GetHotkeyByName("VerticalCanvasDockStopStreaming");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StopStreamingHotkey")));
hw->label = label;
streamingLayout->addRow(label, hw);
hotkeys.push_back(hw);
if (otherHotkey) {
hw->label->pairPartner = otherHotkey->label;
otherHotkey->label->pairPartner = hw->label;
}
}
streamingGroup->setLayout(streamingLayout);
auto streamingDelayGroup =
new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Advanced.StreamDelay")));
auto streamingDelayLayout = new QFormLayout;
streamingDelayLayout->setContentsMargins(9, 2, 9, 9);
streamingDelayLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
streamingDelayLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
streamDelayEnable = new QCheckBox(QString::fromUtf8(obs_frontend_get_locale_string("Enable")));
streamingDelayLayout->addWidget(streamDelayEnable);
streamDelayDuration = new QSpinBox();
streamDelayDuration->setSuffix(" s");
streamDelayDuration->setMinimumSize(QSize(80, 0));
streamDelayDuration->setMinimum(1);
streamDelayDuration->setMaximum(1800);
streamDelayDuration->setEnabled(false);
auto streamDelayDurationLabel =
new QLabel(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Advanced.StreamDelay.Duration")));
streamDelayDurationLabel->setEnabled(false);
streamingDelayLayout->addRow(streamDelayDurationLabel, streamDelayDuration);
streamDelayPreserve =
new QCheckBox(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Advanced.StreamDelay.Preserve")));
streamDelayPreserve->setEnabled(false);
streamingDelayLayout->addWidget(streamDelayPreserve);
QObject::connect(streamDelayEnable, &QCheckBox::toggled, streamDelayDurationLabel, &QLabel::setEnabled);
QObject::connect(streamDelayEnable, &QCheckBox::toggled, streamDelayDuration, &QSpinBox::setEnabled);
QObject::connect(streamDelayEnable, &QCheckBox::toggled, streamDelayPreserve, &QCheckBox::setEnabled);
streamingDelayGroup->setLayout(streamingDelayLayout);
auto streamingAdvancedGroup = new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Advanced")));
auto streamingAdvancedLayout = new QFormLayout;
streamingAdvancedLayout->setContentsMargins(9, 2, 9, 9);
streamingAdvancedLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
streamingAdvancedLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
streamingUseMain = new QCheckBox(QString::fromUtf8(obs_module_text("UseMain")));
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(streamingUseMain, &QCheckBox::checkStateChanged, [this, streamingAdvancedLayout, streamingDelayGroup] {
#else
connect(streamingUseMain, &QCheckBox::stateChanged, [this, streamingAdvancedLayout, streamingDelayGroup] {
#endif
bool checked = streamingUseMain->isChecked();
streamingVideoBitrate->setEnabled(checked);
streamingDelayGroup->setEnabled(!checked);
for (int i = 1; i < streamingAdvancedLayout->rowCount(); i++) {
auto field = streamingAdvancedLayout->itemAt(i, QFormLayout::FieldRole);
if (field) {
auto w = field->widget();
if (w)
w->setEnabled(!checked);
}
auto label = streamingAdvancedLayout->itemAt(i, QFormLayout::LabelRole);
if (label) {
auto w = label->widget();
if (w)
w->setEnabled(!checked);
}
}
});
streamingAdvancedLayout->addWidget(streamingUseMain);
auto audioTracks = new QFrame;
audioTracks->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
audioTracks->setContentsMargins(0, 0, 0, 0);
auto al = new QHBoxLayout;
al->setContentsMargins(0, 0, 0, 0);
audioTracks->setLayout(al);
for (int i = 1; i <= MAX_AUDIO_MIXES; i++) {
auto radio = new QRadioButton(QString::fromUtf8("%1").arg(i));
streamingAudioTracks.push_back(radio);
al->addWidget(radio);
}
streamingAdvancedLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Adv.AudioTrack")),
audioTracks);
streamingEncoder = new QComboBox;
const char *type;
size_t idx = 0;
while (obs_enum_encoder_types(idx++, &type)) {
if (obs_get_encoder_type(type) != OBS_ENCODER_VIDEO)
continue;
uint32_t caps = obs_get_encoder_caps(type);
if ((caps & (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL)) != 0)
continue;
const char *codec = obs_get_encoder_codec(type);
if (astrcmpi(codec, "h264") != 0 && astrcmpi(codec, "hevc") != 0 && astrcmpi(codec, "av1") != 0)
continue;
streamingEncoder->addItem(QString::fromUtf8(obs_encoder_get_display_name(type)), QVariant(QString::fromUtf8(type)));
}
connect(streamingEncoder, &QComboBox::currentIndexChanged, [streamingAdvancedLayout, this] {
int row = 0;
streamingAdvancedLayout->getWidgetPosition(streamingEncoder, &row, nullptr);
for (int i = streamingAdvancedLayout->rowCount() - 1; i > row; i--) {
streamingAdvancedLayout->removeRow(i);
}
if (streamingEncoder->currentIndex() < 0)
return;
auto encoder_string = streamingEncoder->currentData().toString().toUtf8();
auto encoder = encoder_string.constData();
obs_data_release(stream_encoder_settings);
stream_encoder_settings = obs_encoder_defaults(encoder);
obs_data_apply(stream_encoder_settings, canvasDock->stream_encoder_settings);
if (stream_encoder_properties)
obs_properties_destroy(stream_encoder_properties);
stream_encoder_property_widgets.clear();
stream_encoder_properties = obs_get_encoder_properties(encoder);
obs_property_t *property = obs_properties_first(stream_encoder_properties);
while (property) {
AddProperty(property, stream_encoder_settings, streamingAdvancedLayout, &stream_encoder_property_widgets);
obs_property_next(&property);
}
QMetaObject::invokeMethod(streamingUseMain, "stateChanged", Q_ARG(int, streamingUseMain->checkState()));
});
streamingAdvancedLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Encoder.Video")),
streamingEncoder);
QMetaObject::invokeMethod(streamingEncoder, "currentIndexChanged", Q_ARG(int, streamingEncoder->currentIndex()));
streamingAdvancedGroup->setLayout(streamingAdvancedLayout);
vb = new QVBoxLayout;
vb->setContentsMargins(0, 0, 0, 0);
// multistream is active, show warning
if (canvasDock->disable_stream_settings) {
auto multistream_warning_widget = new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Warning")));
auto multistream_warning_layout = new QVBoxLayout;
multistream_warning_widget->setStyleSheet(
"QGroupBox { background: #332701; border: 1px solid #997404; color: #ffda6a; padding-top: 16px; }");
auto multistream_warning_label = new QLabel(QString::fromUtf8(obs_module_text("OutputsMultistream")));
multistream_warning_label->setStyleSheet("color: #ffda6a; font-weight: bold;");
multistream_warning_layout->addWidget(multistream_warning_label);
multistream_warning_widget->setLayout(multistream_warning_layout);
vb->addWidget(multistream_warning_widget);
}
auto multitrackGroup = new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Multitrack")));
auto multitrackLayout = new QVBoxLayout;
multitrackGroup->setLayout(multitrackLayout);
multitrackLabel = new QLabel(QString::fromUtf8(obs_module_text("MultitrackDisabled")));
multitrackLayout->addWidget(multitrackLabel);
vb->addWidget(multitrackGroup);
vb->addWidget(streamingGroup);
vb->addWidget(streamingAdvancedGroup);
vb->addWidget(streamingDelayGroup);
vb->addStretch();
streamingPage->setLayout(vb);
auto recordGroup = new QGroupBox;
recordGroup->setStyleSheet(QString("QGroupBox{ padding-top: 4px;}"));
auto recordLayout = new QFormLayout;
recordLayout->setContentsMargins(9, 2, 9, 9);
recordLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
recordLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto record_title_layout = new QHBoxLayout;
auto record_title = new QLabel(QString::fromUtf8(obs_module_text("Recording")));
record_title->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
record_title_layout->addWidget(record_title, 0, Qt::AlignLeft);
guide_link = new QLabel(QString::fromUtf8("<a href=\"https://vulcast.tv/docs/vertical/recording\">") +
QString::fromUtf8(obs_module_text("ViewGuide")) + QString::fromUtf8("</a>"));
guide_link->setOpenExternalLinks(true);
record_title_layout->addWidget(guide_link, 0, Qt::AlignRight);
recordLayout->addRow(record_title_layout);
QLayout *recordPathLayout = new QHBoxLayout();
recordPath = new QLineEdit();
recordPath->setReadOnly(true);
button = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Browse")));
button->setProperty("themeID", "settingsButtons");
connect(button, &QPushButton::clicked, [this] {
const QString dir = QFileDialog::getExistingDirectory(
this, QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Simple.SavePath")),
recordPath->text(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (dir.isEmpty())
return;
recordPath->setText(dir);
});
recordPathLayout->addWidget(recordPath);
recordPathLayout->addWidget(button);
recordLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Simple.SavePath")),
recordPathLayout);
recordVideoBitrate = new QSpinBox;
recordVideoBitrate->setSuffix(" Kbps");
recordVideoBitrate->setMinimum(2000);
recordVideoBitrate->setMaximum(1000000);
recordLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.VideoBitrate")),
recordVideoBitrate);
recordingMatchMain = new QCheckBox(QString::fromUtf8(obs_module_text("RecordingMatchMain")));
recordLayout->addWidget(recordingMatchMain);
auto max_time_layout = new QHBoxLayout();
maxTimeEnable = new QCheckBox();
max_time_layout->addWidget(maxTimeEnable);
maxTime = new QSpinBox();
maxTime->setMinimum(0);
maxTime->setMaximum(31536000);
maxTime->setSuffix(" s");
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(maxTimeEnable, &QCheckBox::checkStateChanged, [this] { maxTime->setEnabled(maxTimeEnable->isChecked()); });
#else
connect(maxTimeEnable, &QCheckBox::stateChanged, [this] { maxTime->setEnabled(maxTimeEnable->isChecked()); });
#endif
max_time_layout->addWidget(maxTime, 1);
recordLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.SplitFile.Time")),
max_time_layout);
auto max_size_layout = new QHBoxLayout();
maxSizeEnable = new QCheckBox();
max_size_layout->addWidget(maxSizeEnable);
maxSize = new QSpinBox();
maxSize->setMinimum(0);
maxSize->setMaximum(1073741824);
maxSize->setSuffix(" MB");
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(maxSizeEnable, &QCheckBox::checkStateChanged, [this] { maxSize->setEnabled(maxSizeEnable->isChecked()); });
#else
connect(maxSizeEnable, &QCheckBox::stateChanged, [this] { maxSize->setEnabled(maxSizeEnable->isChecked()); });
#endif
max_size_layout->addWidget(maxSize, 1);
recordLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.SplitFile.Size")),
max_size_layout);
otherHotkey = nullptr;
hotkey = GetHotkeyByName("VerticalCanvasDockStartRecording");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
otherHotkey = hw;
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StartRecordingHotkey")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
hotkey = GetHotkeyByName("VerticalCanvasDockStopRecording");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_module_text("StopRecordingHotkey")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
if (otherHotkey) {
hw->label->pairPartner = otherHotkey->label;
otherHotkey->label->pairPartner = hw->label;
}
}
otherHotkey = nullptr;
hotkey = GetHotkeyByName("VerticalCanvasDockPause");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
otherHotkey = hw;
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.PauseRecording")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
hotkey = GetHotkeyByName("VerticalCanvasDockUnpause");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.UnpauseRecording")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
if (otherHotkey) {
hw->label->pairPartner = otherHotkey->label;
otherHotkey->label->pairPartner = hw->label;
}
}
hotkey = GetHotkeyByName("VerticalCanvasDockChapter");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.AddChapterMarker")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
hotkey = GetHotkeyByName("VerticalCanvasDockSplit");
if (hotkey) {
auto id = obs_hotkey_get_id(hotkey);
std::vector<obs_key_combination_t> combos = GetCombosForHotkey(id);
auto hn = obs_hotkey_get_name(hotkey);
auto hw = new OBSHotkeyWidget(this, id, hn, combos);
auto label = new OBSHotkeyLabel;
label->setText(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Main.SplitFile")));
hw->label = label;
recordLayout->addRow(label, hw);
hotkeys.push_back(hw);
}
recordGroup->setLayout(recordLayout);
auto recordingAdvancedGroup = new QGroupBox(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Advanced")));
auto recordingAdvancedLayout = new QFormLayout;
recordingAdvancedLayout->setContentsMargins(9, 2, 9, 9);
recordingAdvancedLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
recordingAdvancedLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
recordingUseMain = new QCheckBox(QString::fromUtf8(obs_module_text("UseMain")));
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(recordingUseMain, &QCheckBox::checkStateChanged, [this, recordingAdvancedLayout] {
#else
connect(recordingUseMain, &QCheckBox::stateChanged, [this, recordingAdvancedLayout] {
#endif
bool checked = recordingUseMain->isChecked();
recordVideoBitrate->setEnabled(checked);
for (int i = 1; i < recordingAdvancedLayout->rowCount(); i++) {
auto field = recordingAdvancedLayout->itemAt(i, QFormLayout::FieldRole);
if (field) {
auto w = field->widget();
if (w)
w->setEnabled(!checked);
}
auto label = recordingAdvancedLayout->itemAt(i, QFormLayout::LabelRole);
if (label) {
auto w = label->widget();
if (w)
w->setEnabled(!checked);
}
}
});
recordingAdvancedLayout->addWidget(recordingUseMain);
filenameFormat = new QLineEdit;
QStringList specList =
QString::fromUtf8(obs_frontend_get_locale_string("FilenameFormatting.completer")).split(QRegularExpression("\n"));
filenameFormat->setText(specList.first());
QCompleter *specCompleter = new QCompleter(specList);
specCompleter->setCaseSensitivity(Qt::CaseSensitive);
specCompleter->setFilterMode(Qt::MatchContains);
filenameFormat->setCompleter(specCompleter);
filenameFormat->setToolTip(QString::fromUtf8(obs_frontend_get_locale_string("FilenameFormatting.TT")));
connect(filenameFormat, &QLineEdit::textEdited, [this](const QString &text) {
QString safeStr = text;
#ifdef __APPLE__
safeStr.replace(QRegularExpression("[:]"), "");
#elif defined(_WIN32)
safeStr.replace(QRegularExpression("[<>:\"\\|\\?\\*]"), "");
#else
// TODO: Add filtering for other platforms
#endif
if (text != safeStr)
filenameFormat->setText(safeStr);
});
recordingAdvancedLayout->addRow(
QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Adv.Recording.Filename")), filenameFormat);
fileFormat = new QComboBox;
fileFormat->addItem(QString::fromUtf8("mp4"));
fileFormat->addItem(QString::fromUtf8("hybrid_mp4"));
fileFormat->addItem(QString::fromUtf8("fragmented_mp4"));
fileFormat->addItem(QString::fromUtf8("mov"));
fileFormat->addItem(QString::fromUtf8("fragmented_mov"));
fileFormat->addItem(QString::fromUtf8("mkv"));
fileFormat->addItem(QString::fromUtf8("ts"));
fileFormat->addItem(QString::fromUtf8("mp3u8"));
recordingAdvancedLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Format")),
fileFormat);
audioTracks = new QFrame;
audioTracks->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
audioTracks->setContentsMargins(0, 0, 0, 0);
al = new QHBoxLayout;
al->setContentsMargins(0, 0, 0, 0);
audioTracks->setLayout(al);
for (int i = 1; i <= MAX_AUDIO_MIXES; i++) {
auto check = new QCheckBox(QString::fromUtf8("%1").arg(i));
recordingAudioTracks.push_back(check);
al->addWidget(check);
}
recordingAdvancedLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Adv.AudioTrack")),
audioTracks);
recordingEncoder = new QComboBox;
recordingEncoder->addItem(
QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Adv.Recording.UseStreamEncoder")),
QVariant(QString::fromUtf8("")));
idx = 0;
while (obs_enum_encoder_types(idx++, &type)) {
if (obs_get_encoder_type(type) != OBS_ENCODER_VIDEO)
continue;
uint32_t caps = obs_get_encoder_caps(type);
if ((caps & (OBS_ENCODER_CAP_DEPRECATED | OBS_ENCODER_CAP_INTERNAL)) != 0)
continue;
recordingEncoder->addItem(QString::fromUtf8(obs_encoder_get_display_name(type)), QVariant(QString::fromUtf8(type)));
}
connect(recordingEncoder, &QComboBox::currentIndexChanged, [recordingAdvancedLayout, this] {
int row = 0;
recordingAdvancedLayout->getWidgetPosition(recordingEncoder, &row, nullptr);
for (int i = recordingAdvancedLayout->rowCount() - 1; i > row; i--) {
recordingAdvancedLayout->removeRow(i);
}
if (record_encoder_properties)
obs_properties_destroy(record_encoder_properties);
record_encoder_properties = nullptr;
record_encoder_property_widgets.clear();
if (recordingEncoder->currentIndex() < 1)
return;
auto encoder_string = recordingEncoder->currentData().toString().toUtf8();
auto encoder = encoder_string.constData();
obs_data_release(record_encoder_settings);
record_encoder_settings = obs_encoder_defaults(encoder);
obs_data_apply(record_encoder_settings, canvasDock->record_encoder_settings);
record_encoder_properties = obs_get_encoder_properties(encoder);
obs_property_t *property = obs_properties_first(record_encoder_properties);
while (property) {
AddProperty(property, record_encoder_settings, recordingAdvancedLayout, &record_encoder_property_widgets);
obs_property_next(&property);
}
QMetaObject::invokeMethod(recordingUseMain, "stateChanged", Q_ARG(int, recordingUseMain->checkState()));
});
recordingAdvancedLayout->addRow(QString::fromUtf8(obs_frontend_get_locale_string("Basic.Settings.Output.Encoder.Video")),
recordingEncoder);
recordingAdvancedGroup->setLayout(recordingAdvancedLayout);
vb = new QVBoxLayout;
vb->setContentsMargins(0, 0, 0, 0);
vb->addWidget(recordGroup);
vb->addWidget(recordingAdvancedGroup);
vb->addStretch();
recordingPage->setLayout(vb);
// HELP PAGE
auto helpButtonGroup = new QWidget();
auto helpButtonGroupLayout = new QVBoxLayout();
helpButtonGroupLayout->setContentsMargins(0, 0, 0, 0);
helpButtonGroupLayout->setSpacing(5);
helpButtonGroup->setLayout(helpButtonGroupLayout);
// intro text
auto helpTitle = new QLabel;
helpTitle->setText(QString::fromUtf8(obs_module_text("Help")));
helpTitle->setStyleSheet(QString::fromUtf8("font-weight: bold;"));
helpButtonGroupLayout->addWidget(helpTitle);
// intro desc
auto introDesc = new QLabel;
introDesc->setText(QString::fromUtf8(obs_module_text("HelpIntro")));
helpButtonGroupLayout->addWidget(introDesc);
// troubleshooter button
auto tsButton = new QPushButton;
tsButton->setObjectName(QStringLiteral("tsButton"));
tsButton->setCheckable(false);
tsButton->setText(QString::fromUtf8(obs_module_text("HelpTroubleshooterButton")));
connect(tsButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://vulcast.tv/docs/vertical")); });
helpButtonGroupLayout->addWidget(tsButton);
// guides button
auto guideButton = new QPushButton;
guideButton->setObjectName(QStringLiteral("guideButton"));
guideButton->setCheckable(false);
guideButton->setText(QString::fromUtf8(obs_module_text("HelpGuideButton")));
connect(guideButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://vulcast.tv/docs/vertical")); });
helpButtonGroupLayout->addWidget(guideButton);
// discord button
auto discordButton = new QPushButton;
discordButton->setObjectName(QStringLiteral("discordButton"));
discordButton->setCheckable(false);
discordButton->setText(QString::fromUtf8(obs_module_text("HelpDiscordButton")));
connect(discordButton, &QPushButton::clicked, [] { QDesktopServices::openUrl(QUrl("https://vulcast.tv/discord")); });
helpButtonGroupLayout->addWidget(discordButton);
vb = new QVBoxLayout;
vb->setContentsMargins(0, 0, 0, 0);
vb->addWidget(helpButtonGroup);
vb->addStretch();
helpPage->setLayout(vb);
QPushButton *okButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("OK")));
connect(okButton, &QPushButton::clicked, [this] {
SaveSettings();
close();
});
QPushButton *cancelButton = new QPushButton(QString::fromUtf8(obs_frontend_get_locale_string("Cancel")));
connect(cancelButton, &QPushButton::clicked, [this] { close(); });
QHBoxLayout *contentLayout = new QHBoxLayout;
contentLayout->addWidget(listWidget);
contentLayout->addWidget(settingsPages, 1);
QHBoxLayout *bottomLayout = new QHBoxLayout;
const auto version =
new QLabel(QString::fromUtf8(obs_module_text("Version")) + " " + QString::fromUtf8(PROJECT_VERSION) + " " +
QString::fromUtf8("powered by") + " <a href=\"https://vulcast.tv\">Vulcast</a>");
version->setOpenExternalLinks(true);
version->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
newVersion = new QLabel;
newVersion->setProperty("themeID", "warning");
button->setProperty("class", "text-warning");
newVersion->setVisible(false);
newVersion->setOpenExternalLinks(true);
newVersion->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
bottomLayout->addWidget(version, 1, Qt::AlignLeft);
bottomLayout->addWidget(newVersion, 1, Qt::AlignLeft);
bottomLayout->addWidget(okButton, 0, Qt::AlignRight);
bottomLayout->addWidget(cancelButton, 0, Qt::AlignRight);
QVBoxLayout *vlayout = new QVBoxLayout;
vlayout->setContentsMargins(11, 11, 11, 11);
vlayout->addLayout(contentLayout);
vlayout->addLayout(bottomLayout);
setLayout(vlayout);
setWindowTitle(obs_module_text("VerticalSettings"));
setSizeGripEnabled(true);
}
OBSBasicSettings::~OBSBasicSettings()
{
obs_properties_destroy(stream_encoder_properties);
obs_properties_destroy(record_encoder_properties);
obs_data_release(stream_encoder_settings);
obs_data_release(record_encoder_settings);
}
QIcon OBSBasicSettings::GetGeneralIcon() const
{
return listWidget->item(0)->icon();
}
QIcon OBSBasicSettings::GetAppearanceIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetStreamIcon() const
{
return listWidget->item(1)->icon();
}
QIcon OBSBasicSettings::GetOutputIcon() const
{
return listWidget->item(2)->icon();
}
QIcon OBSBasicSettings::GetAudioIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetVideoIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetHotkeysIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetAccessibilityIcon() const
{
return QIcon();
}
QIcon OBSBasicSettings::GetAdvancedIcon() const
{
return QIcon();
}
void OBSBasicSettings::SetGeneralIcon(const QIcon &icon)
{
listWidget->item(0)->setIcon(icon);
}
void OBSBasicSettings::SetAppearanceIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetStreamIcon(const QIcon &icon)
{
listWidget->item(1)->setIcon(icon);
}
void OBSBasicSettings::SetOutputIcon(const QIcon &icon)
{
listWidget->item(2)->setIcon(icon);
}
void OBSBasicSettings::SetAudioIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetVideoIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetHotkeysIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetAccessibilityIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::SetAdvancedIcon(const QIcon &icon)
{
UNUSED_PARAMETER(icon);
}
void OBSBasicSettings::AddServer()
{
int idx = (int)servers.size();
auto serverGroup = new QGroupBox;
serverGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
serverGroup->setStyleSheet(QString("QGroupBox{background-color: %1; padding-top: 4px;}")
.arg(palette().color(QPalette::ColorRole::Mid).name(QColor::HexRgb)));
auto serverLayout = new QFormLayout;
serverLayout->setContentsMargins(9, 2, 9, 9);
serverLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
serverLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignTrailing | Qt::AlignVCenter);
auto enabled = new QCheckBox(QString::fromUtf8(obs_module_text("Enabled")));
enabled->setChecked(true);
serverLayout->addRow(enabled);
servers_enabled.push_back(enabled);
auto server_name = new QLineEdit;
serverLayout->addRow(QString::fromUtf8(obs_module_text("Name")), server_name);
server_names.push_back(server_name);
auto server = new QComboBox;
server->setEditable(true);
server->addItem("rtmps://a.rtmps.youtube.com:443/live2");
server->addItem("rtmps://b.rtmps.youtube.com:443/live2?backup=1");
server->addItem("rtmp://a.rtmp.youtube.com/live2");
server->addItem("rtmp://b.rtmp.youtube.com/live2?backup=1");
server->setCurrentText("");
serverLayout->addRow(QString::fromUtf8(obs_module_text("Server")), server);
servers.push_back(server);
QLayout *subLayout = new QHBoxLayout();
auto key = new QLineEdit;
key->setEchoMode(QLineEdit::Password);
QPushButton *show = new QPushButton();
show->setText(QString::fromUtf8(obs_frontend_get_locale_string("Show")));
show->setCheckable(true);
connect(show, &QAbstractButton::toggled, [=](bool hide) {
show->setText(
QString::fromUtf8(hide ? obs_frontend_get_locale_string("Hide") : obs_frontend_get_locale_string("Show")));
key->setEchoMode(hide ? QLineEdit::Normal : QLineEdit::Password);
});
subLayout->addWidget(key);
subLayout->addWidget(show);
serverLayout->addRow(QString::fromUtf8(obs_module_text("Key")), subLayout);
keys.push_back(key);
serverGroup->setLayout(serverLayout);
streamingLayout->insertRow(idx + 1, serverGroup);
}
void OBSBasicSettings::LoadSettings()
{
if (!canvasDock->newer_version_available.isEmpty()) {
newVersion->setText(QString::fromUtf8(obs_module_text("NewVersion")).arg(canvasDock->newer_version_available));
newVersion->setVisible(true);
}
resolution->setCurrentText(QString::number(canvasDock->canvas_width) + "x" + QString::number(canvasDock->canvas_height));
bool enable = !obs_output_active(canvasDock->recordOutput) && !obs_output_active(canvasDock->virtualCamOutput);
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it) {
if (obs_output_active(it->output))
enable = false;
}
resolution->setEnabled(enable);
virtualCameraMode->setCurrentIndex(canvasDock->virtual_cam_mode);
recordVideoBitrate->setValue(canvasDock->recordVideoBitrate ? canvasDock->recordVideoBitrate : 6000);
maxTimeEnable->setChecked(canvasDock->max_time_sec > 0);
maxTime->setValue(canvasDock->max_time_sec);
maxSizeEnable->setChecked(canvasDock->max_size_mb > 0);
maxSize->setValue(canvasDock->max_size_mb);
recordingMatchMain->setChecked(canvasDock->recordingMatchMain);
streamingVideoBitrate->setValue(canvasDock->streamingVideoBitrate ? canvasDock->streamingVideoBitrate : 6000);
streamingMatchMain->setChecked(canvasDock->streamingMatchMain);
for (int i = 0; i < audioBitrate->count(); i++) {
if (audioBitrate->itemData(i).toUInt() == (canvasDock->audioBitrate ? canvasDock->audioBitrate : 160)) {
audioBitrate->setCurrentIndex(i);
}
}
backtrackClip->setChecked(canvasDock->startReplay);
backtrackDuration->setValue(canvasDock->replayDuration);
backtrackPath->setText(QString::fromUtf8(canvasDock->replayPath));
auto profile_config = obs_frontend_get_profile_config();
if (config_get_bool(profile_config, "Stream1", "EnableMultitrackVideo")) {
auto canvas_id = config_get_string(profile_config, "Stream1", "MultitrackExtraCanvas");
if (canvas_id && strcmp(canvas_id, obs_canvas_get_uuid(canvasDock->canvas)) == 0) {
multitrackLabel->setText(QString::fromUtf8(obs_module_text("MultitrackVerticalSelected")));
} else {
multitrackLabel->setText(QString::fromUtf8(obs_module_text("MultitrackVerticalNotSelected")));
}
} else {
multitrackLabel->setText(QString::fromUtf8(obs_module_text("MultitrackDisabled")));
}
if (!canvasDock->disable_stream_settings) {
for (size_t idx = 0; idx < canvasDock->streamOutputs.size(); idx++) {
if (idx >= servers.size()) {
AddServer();
}
auto name = canvasDock->streamOutputs[idx].name;
server_names[idx]->setText(name.empty() ? QString::fromUtf8(obs_module_text("Output")) + " " +
QString::number(idx + 1)
: QString::fromUtf8(name));
auto key = keys[idx];
key->setEchoMode(QLineEdit::Password);
key->setText(QString::fromUtf8(canvasDock->streamOutputs[idx].stream_key));
servers[idx]->setCurrentText(QString::fromUtf8(canvasDock->streamOutputs[idx].stream_server));
servers_enabled[idx]->setChecked(canvasDock->streamOutputs[idx].enabled);
}
if (servers.empty()) {
AddServer();
}
}
streamingUseMain->setChecked(!canvasDock->stream_advanced_settings);
if (canvasDock->stream_audio_track > 0)
streamingAudioTracks[canvasDock->stream_audio_track - 1]->setChecked(true);
streamDelayEnable->setChecked(canvasDock->stream_delay_enabled);
streamDelayDuration->setValue(canvasDock->stream_delay_duration);
streamDelayPreserve->setChecked(canvasDock->stream_delay_preserve);
auto idx = streamingEncoder->findData(QVariant(QString::fromUtf8(canvasDock->stream_encoder.c_str())));
if (idx != -1)
streamingEncoder->setCurrentIndex(idx);
for (const auto &kv : stream_encoder_property_widgets) {
LoadProperty(kv.first, canvasDock->stream_encoder_settings, kv.second);
}
recordPath->setText(QString::fromUtf8(canvasDock->recordPath));
recordingUseMain->setChecked(!canvasDock->record_advanced_settings);
if (!canvasDock->filename_formatting.empty())
filenameFormat->setText(canvasDock->filename_formatting.c_str());
idx = fileFormat->findData(QVariant(QString::fromUtf8(canvasDock->file_format.c_str())));
if (idx == -1) {
idx = fileFormat->findText(QString::fromUtf8(canvasDock->file_format.c_str()));
}
if (idx != -1 && idx != fileFormat->currentIndex())
fileFormat->setCurrentIndex(idx);
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
recordingAudioTracks[i]->setChecked((canvasDock->record_audio_tracks & (1ll << i)) != 0);
}
idx = recordingEncoder->findData(QVariant(QString::fromUtf8(canvasDock->record_encoder.c_str())));
if (idx != -1)
recordingEncoder->setCurrentIndex(idx);
for (const auto &kv : record_encoder_property_widgets) {
LoadProperty(kv.first, canvasDock->record_encoder_settings, kv.second);
}
// Load API key
auto profile_config = obs_frontend_get_profile_config();
auto saved_key = config_get_string(profile_config, "VulcastVertical", "ApiKey");
if (saved_key && strlen(saved_key) > 0) {
apiKeyInput->setText(QString::fromUtf8(saved_key));
}
}
void OBSBasicSettings::SaveSettings()
{
for (auto &hw : hotkeys) {
hw->Save();
}
const auto res = resolution->currentText();
uint32_t width, height;
if (sscanf(res.toUtf8().constData(), "%dx%d", &width, &height) == 2 && width > 0 && height > 0 &&
(width != canvasDock->canvas_width || height != canvasDock->canvas_height)) {
if (obs_output_active(canvasDock->replayOutput))
obs_output_stop(canvasDock->replayOutput);
blog(LOG_INFO, "[Vertical Canvas] resolution changed from %dx%d to %dx%d", canvasDock->canvas_width,
canvasDock->canvas_height, width, height);
canvasDock->canvas_width = width;
canvasDock->canvas_height = height;
obs_video_info ovi;
obs_get_video_info(&ovi);
ovi.base_width = width;
ovi.base_height = height;
ovi.output_width = width;
ovi.output_height = height;
obs_canvas_reset_video(canvasDock->canvas, &ovi);
auto t = obs_weak_source_get_source(canvasDock->source);
if (obs_source_get_type(t) == OBS_SOURCE_TYPE_TRANSITION) {
obs_transition_set_size(t, width, height);
}
obs_source_release(t);
canvasDock->LoadScenes();
canvasDock->ProfileChanged();
}
if (virtualCameraMode->currentIndex() >= 0)
canvasDock->virtual_cam_mode = virtualCameraMode->currentIndex();
uint32_t bitrate = (uint32_t)recordVideoBitrate->value();
if (bitrate != canvasDock->recordVideoBitrate) {
canvasDock->recordVideoBitrate = bitrate;
SetEncoderBitrate(obs_output_get_video_encoder(canvasDock->replayOutput), true);
SetEncoderBitrate(obs_output_get_video_encoder(canvasDock->recordOutput), true);
}
canvasDock->max_size_mb = maxSizeEnable->isChecked() ? maxSize->value() : 0;
canvasDock->max_time_sec = maxTimeEnable->isChecked() ? maxTime->value() : 0;
canvasDock->recordingMatchMain = recordingMatchMain->isChecked();
bitrate = (uint32_t)streamingVideoBitrate->value();
if (bitrate != canvasDock->streamingVideoBitrate) {
canvasDock->streamingVideoBitrate = bitrate;
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it)
SetEncoderBitrate(obs_output_get_video_encoder(it->output), false);
}
canvasDock->streamingMatchMain = streamingMatchMain->isChecked();
bitrate = (uint32_t)audioBitrate->currentData().toUInt();
if (bitrate != canvasDock->audioBitrate) {
canvasDock->audioBitrate = bitrate;
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
SetEncoderBitrate(obs_output_get_audio_encoder(canvasDock->replayOutput, i), true);
SetEncoderBitrate(obs_output_get_audio_encoder(canvasDock->recordOutput, i), true);
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it)
SetEncoderBitrate(obs_output_get_audio_encoder(it->output, i), false);
}
}
auto startReplay = backtrackClip->isChecked();
auto duration = (uint32_t)backtrackDuration->value();
std::string replayPath = backtrackPath->text().toUtf8().constData();
if (duration != canvasDock->replayDuration || replayPath != canvasDock->replayPath ||
canvasDock->startReplay != startReplay) {
canvasDock->replayDuration = duration;
canvasDock->replayPath = replayPath;
canvasDock->startReplay = startReplay;
if (canvasDock->replayAlwaysOn || startReplay) {
if (obs_output_active(canvasDock->replayOutput)) {
canvasDock->StopReplayBuffer();
QTimer::singleShot(500, this, [this] { canvasDock->CheckReplayBuffer(true); });
} else {
canvasDock->CheckReplayBuffer(true);
}
} else {
canvasDock->StopReplayBuffer();
}
}
if (!canvasDock->disable_stream_settings) {
for (size_t idx = 0; idx < servers.size(); idx++) {
std::string sk = keys[idx]->text().toUtf8().constData();
std::string ss = servers[idx]->currentText().toUtf8().constData();
if (idx >= canvasDock->streamOutputs.size()) {
StreamServer so;
so.stream_server = ss;
so.stream_key = sk;
std::string service_name = "vertical_canvas_stream_service_";
service_name += std::to_string(idx);
bool whip = strstr(so.stream_server.c_str(), "whip") != nullptr;
so.service = obs_service_create(whip ? "whip_custom" : "rtmp_custom", service_name.c_str(), nullptr,
nullptr);
canvasDock->streamOutputs.push_back(so);
}
canvasDock->streamOutputs[idx].name = server_names[idx]->text().toUtf8().constData();
if (sk != canvasDock->streamOutputs[idx].stream_key || ss != canvasDock->streamOutputs[idx].stream_server) {
canvasDock->streamOutputs[idx].stream_key = sk;
canvasDock->streamOutputs[idx].stream_server = ss;
if (obs_output_active(canvasDock->streamOutputs[idx].output)) {
//TODO restart
//StopStream();
//StartStream();
}
}
canvasDock->streamOutputs[idx].enabled = servers_enabled[idx]->isChecked();
}
if (canvasDock->streamOutputs.size() > servers.size()) {
for (auto idx = canvasDock->streamOutputs.size() - 1; idx >= servers.size(); idx--) {
if (obs_output_active(canvasDock->streamOutputs[idx].output))
obs_output_stop(canvasDock->streamOutputs[idx].output);
obs_output_release(canvasDock->streamOutputs[idx].output);
obs_service_release(canvasDock->streamOutputs[idx].service);
canvasDock->streamOutputs.pop_back();
}
}
}
canvasDock->UpdateMulti();
if (streamDelayEnable->isChecked() != canvasDock->stream_delay_enabled ||
(canvasDock->stream_delay_enabled && ((uint32_t)streamDelayDuration->value() != canvasDock->stream_delay_duration ||
streamDelayPreserve->isChecked() != canvasDock->stream_delay_preserve))) {
canvasDock->stream_delay_enabled = streamDelayEnable->isChecked();
canvasDock->stream_delay_duration = (uint32_t)streamDelayDuration->value();
canvasDock->stream_delay_preserve = streamDelayPreserve->isChecked();
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it) {
if (!it->output)
continue;
obs_output_set_delay(it->output, canvasDock->stream_delay_enabled ? canvasDock->stream_delay_duration : 0,
canvasDock->stream_delay_preserve ? OBS_OUTPUT_DELAY_PRESERVE : 0);
}
}
auto sa = !streamingUseMain->isChecked();
auto se = streamingEncoder->currentData().toString().toUtf8();
if (canvasDock->stream_advanced_settings != sa || canvasDock->stream_encoder != se.constData()) {
canvasDock->stream_advanced_settings = sa;
canvasDock->stream_encoder = se.constData();
obs_encoder_t *enc = nullptr;
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it) {
if (it->output && !obs_output_active(it->output)) {
if (!enc)
enc = obs_output_get_video_encoder(it->output);
obs_output_set_video_encoder(it->output, nullptr);
}
}
if (enc && strcmp(obs_encoder_get_name(enc), "vertical_canvas_video_encoder") == 0) {
obs_encoder_release(enc);
}
}
for (int i = 1; i <= (int)streamingAudioTracks.size(); i++) {
if (streamingAudioTracks[i - 1]->isChecked()) {
if (canvasDock->stream_audio_track != i) {
for (auto it = canvasDock->streamOutputs.begin(); it != canvasDock->streamOutputs.end(); ++it) {
if (it->output && !obs_output_active(it->output))
obs_output_set_audio_encoder(it->output, nullptr, 0);
}
canvasDock->stream_audio_track = i;
}
break;
}
}
obs_data_apply(canvasDock->stream_encoder_settings, stream_encoder_settings);
canvasDock->recordPath = recordPath->text().toUtf8().constData();
auto ra = !recordingUseMain->isChecked();
auto re = recordingEncoder->currentData().toString().toUtf8();
if (canvasDock->record_advanced_settings != ra || canvasDock->record_encoder != re.constData()) {
canvasDock->record_advanced_settings = ra;
canvasDock->record_encoder = re.constData();
if ((canvasDock->recordOutput && !obs_output_active(canvasDock->recordOutput)) ||
(canvasDock->replayOutput && !obs_output_active(canvasDock->replayOutput))) {
auto enc = canvasDock->recordOutput ? obs_output_get_video_encoder(canvasDock->recordOutput) : nullptr;
if (!enc && canvasDock->replayOutput) {
enc = obs_output_get_video_encoder(canvasDock->replayOutput);
}
if (canvasDock->recordOutput)
obs_output_set_video_encoder(canvasDock->recordOutput, nullptr);
if (canvasDock->replayOutput)
obs_output_set_video_encoder(canvasDock->replayOutput, nullptr);
if (enc && strcmp(obs_encoder_get_name(enc), "vertical_canvas_record_video_encoder") == 0) {
obs_encoder_release(enc);
}
}
}
canvasDock->filename_formatting = filenameFormat->text().toUtf8().constData();
canvasDock->file_format = fileFormat->currentText().toUtf8().constData();
long long tracks = 0;
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
if (recordingAudioTracks[i]->isChecked())
tracks += (1ll << i);
}
if (!tracks)
tracks = 1;
if (canvasDock->record_audio_tracks != tracks) {
canvasDock->record_audio_tracks = tracks;
if ((canvasDock->recordOutput && !obs_output_active(canvasDock->recordOutput)) ||
(canvasDock->replayOutput && !obs_output_active(canvasDock->replayOutput))) {
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
auto enc = canvasDock->recordOutput ? obs_output_get_audio_encoder(canvasDock->recordOutput, i)
: nullptr;
if (!enc && canvasDock->replayOutput) {
enc = obs_output_get_audio_encoder(canvasDock->replayOutput, i);
}
if (canvasDock->recordOutput)
obs_output_set_audio_encoder(canvasDock->recordOutput, nullptr, i);
if (canvasDock->replayOutput)
obs_output_set_audio_encoder(canvasDock->replayOutput, nullptr, i);
obs_encoder_release(enc);
}
}
}
obs_data_apply(canvasDock->record_encoder_settings, record_encoder_settings);
// Save API key
auto profile_config = obs_frontend_get_profile_config();
auto apiKey = apiKeyInput->text().toUtf8();
config_set_string(profile_config, "VulcastVertical", "ApiKey", apiKey.constData());
config_save(profile_config);
}
void OBSBasicSettings::FetchPlatformStatus()
{
auto apiKey = apiKeyInput->text().trimmed();
if (apiKey.isEmpty()) {
apiStatus->setText(QString::fromUtf8("Please enter an API key."));
apiStatus->setStyleSheet("color: #f66; font-size: 12px;");
apiStatus->setVisible(true);
return;
}
apiStatus->setText(QString::fromUtf8("Connecting..."));
apiStatus->setStyleSheet("color: #ff0; font-size: 12px;");
apiStatus->setVisible(true);
platformList->clear();
QNetworkRequest request(QUrl(QString::fromUtf8("https://vulcast.xyz/api/plugin/status")));
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
request.setRawHeader("Content-Type", "application/json");
QNetworkReply *reply = networkManager->get(request);
connect(reply, &QNetworkReply::finished, [this, reply] {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 401) {
apiStatus->setText(QString::fromUtf8("Invalid API key. Generate one at vulcast.xyz/dashboard/settings"));
} else {
apiStatus->setText(QString::fromUtf8("Connection failed: %1").arg(reply->errorString()));
}
apiStatus->setStyleSheet("color: #f66; font-size: 12px;");
return;
}
auto doc = QJsonDocument::fromJson(reply->readAll());
auto root = doc.object();
// Show user info
auto user = root["user"].toObject();
apiStatus->setText(QString::fromUtf8("Connected as %1").arg(user["email"].toString()));
apiStatus->setStyleSheet("color: #6f6; font-size: 12px;");
// Populate platform list
auto platforms = root["platforms"].toArray();
platformList->clear();
for (auto p : platforms) {
auto platform = p.toObject();
auto name = platform["platform"].toString();
auto label = platform["label"].toString();
auto connected = platform["connected"].toBool();
auto hasKey = platform["hasStreamKey"].toBool();
auto destinations = platform["destinations"].toArray();
QString icon = connected ? (hasKey ? QString::fromUtf8("") : QString::fromUtf8("⚠️")) : QString::fromUtf8("🔗");
QString statusText = connected ? (hasKey ? "Ready" : "No stream key") : "Manual";
auto item = new QListWidgetItem(QString("%1 %2 — %3 (%4)").arg(icon, name, label, statusText));
platformList->addItem(item);
// Show destinations (horizontal/vertical)
for (auto d : destinations) {
auto dest = d.toObject();
auto orientation = dest["orientation"].toString();
auto destLabel = dest["label"].toString();
auto enabled = dest["enabled"].toBool();
QString orientIcon = orientation == "vertical" ? QString::fromUtf8("📱") : QString::fromUtf8("🖥️");
QString enIcon = enabled ? QString::fromUtf8("") : QString::fromUtf8("");
auto subItem = new QListWidgetItem(QString(" %1 %2 %3").arg(orientIcon, destLabel, enIcon));
subItem->setForeground(QColor("#999"));
platformList->addItem(subItem);
}
}
if (platforms.isEmpty()) {
auto item = new QListWidgetItem(QString::fromUtf8("No platforms linked yet. Go to vulcast.xyz/dashboard/destinations"));
item->setForeground(QColor("#999"));
platformList->addItem(item);
}
// Show streams
auto streams = root["streams"].toArray();
if (!streams.isEmpty()) {
platformList->addItem(new QListWidgetItem(QString::fromUtf8("")));
platformList->addItem(new QListWidgetItem(QString::fromUtf8("📹 Streams:")));
for (auto s : streams) {
auto stream = s.toObject();
auto status = stream["status"].toString();
QString statusIcon = status == "live" ? QString::fromUtf8("🔴") : QString::fromUtf8("");
auto item = new QListWidgetItem(QString(" %1 %2 (%3)").arg(statusIcon, stream["name"].toString(), status));
platformList->addItem(item);
}
}
});
}
void OBSBasicSettings::SetEncoderBitrate(obs_encoder_t *encoder, bool record)
{
if (!encoder)
return;
auto settings = obs_encoder_get_settings(encoder);
if (!settings)
return;
auto bitrate = obs_encoder_get_type(encoder) == OBS_ENCODER_AUDIO
? canvasDock->audioBitrate
: (record ? canvasDock->recordVideoBitrate : canvasDock->streamingVideoBitrate);
if (obs_data_get_int(settings, "bitrate") == bitrate) {
obs_data_release(settings);
return;
}
obs_data_set_int(settings, "bitrate", bitrate);
obs_encoder_update(encoder, nullptr);
obs_data_release(settings);
}
std::vector<obs_hotkey_t *> OBSBasicSettings::GetHotKeysFromOutput(obs_output_t *output)
{
struct find_hotkey {
std::vector<obs_hotkey_t *> hotkeys;
obs_weak_output_t *output;
};
find_hotkey t = {};
t.output = obs_output_get_weak_output(output);
obs_enum_hotkeys(
[](void *param, obs_hotkey_id, obs_hotkey_t *key) {
if (obs_hotkey_get_registerer_type(key) != OBS_HOTKEY_REGISTERER_OUTPUT)
return true;
auto hp = (struct find_hotkey *)param;
auto o = obs_hotkey_get_registerer(key);
if (o == hp->output || obs_weak_output_references_output(hp->output, (obs_output_t *)o)) {
hp->hotkeys.push_back(key);
}
return true;
},
&t);
obs_weak_output_release(t.output);
return t.hotkeys;
}
std::vector<obs_key_combination_t> OBSBasicSettings::GetCombosForHotkey(obs_hotkey_id hotkey)
{
struct find_combos {
obs_hotkey_id hotkey;
std::vector<obs_key_combination_t> combos;
};
find_combos t = {hotkey, {}};
obs_enum_hotkey_bindings(
[](void *param, size_t idx, obs_hotkey_binding_t *binding) {
UNUSED_PARAMETER(idx);
auto fc = (struct find_combos *)param;
if (fc->hotkey == obs_hotkey_binding_get_hotkey_id(binding)) {
fc->combos.push_back(obs_hotkey_binding_get_key_combination(binding));
}
return true;
},
&t);
return t.combos;
}
std::vector<obs_hotkey_t *> OBSBasicSettings::GetHotkeyById(obs_hotkey_id hotkey)
{
struct find_hotkey {
std::vector<obs_hotkey_t *> hotkeys;
obs_hotkey_id hotkey_id;
};
find_hotkey t = {};
t.hotkey_id = hotkey;
obs_enum_hotkeys(
[](void *param, obs_hotkey_id id, obs_hotkey_t *key) {
auto hp = (struct find_hotkey *)param;
if (hp->hotkey_id == id) {
hp->hotkeys.push_back(key);
}
return true;
},
&t);
return t.hotkeys;
}
obs_hotkey_t *OBSBasicSettings::GetHotkeyByName(QString name)
{
struct find_hotkey {
obs_hotkey_t *hotkey;
const char *name;
};
find_hotkey t = {};
auto n = name.toUtf8();
t.name = n.constData();
obs_enum_hotkeys(
[](void *param, obs_hotkey_id id, obs_hotkey_t *key) {
UNUSED_PARAMETER(id);
const auto hp = (struct find_hotkey *)param;
const auto hn = obs_hotkey_get_name(key);
if (strcmp(hp->name, hn) == 0)
hp->hotkey = key;
return true;
},
&t);
return t.hotkey;
}
void OBSBasicSettings::AddProperty(obs_property_t *property, obs_data_t *settings, QFormLayout *layout,
std::map<obs_property_t *, QWidget *> *widgets)
{
obs_property_type type = obs_property_get_type(property);
if (type == OBS_PROPERTY_BOOL) {
auto widget = new QCheckBox(QString::fromUtf8(obs_property_description(property)));
widget->setChecked(obs_data_get_bool(settings, obs_property_name(property)));
layout->addWidget(widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
int row = 0;
layout->getWidgetPosition(widget, &row, nullptr);
auto item = layout->itemAt(row, QFormLayout::LabelRole);
if (item) {
auto w = item->widget();
if (w)
w->setVisible(false);
}
}
widgets->emplace(property, widget);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(widget, &QCheckBox::checkStateChanged, [this, property, settings, widget, widgets, layout] {
#else
connect(widget, &QCheckBox::stateChanged, [this, property, settings, widget, widgets, layout] {
#endif
obs_data_set_bool(settings, obs_property_name(property), widget->isChecked());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
} else if (type == OBS_PROPERTY_INT) {
auto widget = new QSpinBox();
widget->setEnabled(obs_property_enabled(property));
widget->setMinimum(obs_property_int_min(property));
widget->setMaximum(obs_property_int_max(property));
widget->setSingleStep(obs_property_int_step(property));
widget->setValue((int)obs_data_get_int(settings, obs_property_name(property)));
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
widget->setSuffix(QString::fromUtf8(obs_property_int_suffix(property)));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QSpinBox::valueChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_int(settings, obs_property_name(property), widget->value());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
} else if (type == OBS_PROPERTY_FLOAT) {
auto widget = new QDoubleSpinBox();
widget->setEnabled(obs_property_enabled(property));
widget->setMinimum(obs_property_float_min(property));
widget->setMaximum(obs_property_float_max(property));
widget->setSingleStep(obs_property_float_step(property));
widget->setValue(obs_data_get_double(settings, obs_property_name(property)));
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
widget->setSuffix(QString::fromUtf8(obs_property_float_suffix(property)));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QDoubleSpinBox::valueChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_double(settings, obs_property_name(property), widget->value());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
} else if (type == OBS_PROPERTY_TEXT) {
obs_text_type text_type = obs_property_text_type(property);
if (text_type == OBS_TEXT_MULTILINE) {
auto widget = new QPlainTextEdit;
widget->document()->setDefaultStyleSheet("font { white-space: pre; }");
widget->setTabStopDistance(40);
widget->setPlainText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(property))));
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
connect(widget, &QPlainTextEdit::textChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property), widget->toPlainText().toUtf8());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
} else {
auto widget = new QLineEdit();
widget->setText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(property))));
if (text_type == OBS_TEXT_PASSWORD)
widget->setEchoMode(QLineEdit::Password);
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
if (text_type != OBS_TEXT_INFO) {
connect(widget, &QLineEdit::textChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property), widget->text().toUtf8());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
}
}
} else if (type == OBS_PROPERTY_LIST) {
auto widget = new QComboBox();
widget->setMaxVisibleItems(40);
widget->setToolTip(QString::fromUtf8(obs_property_long_description(property)));
auto list_type = obs_property_list_type(property);
obs_combo_format format = obs_property_list_format(property);
size_t count = obs_property_list_item_count(property);
for (size_t i = 0; i < count; i++) {
QVariant var;
if (format == OBS_COMBO_FORMAT_INT) {
long long val = obs_property_list_item_int(property, i);
var = QVariant::fromValue<long long>(val);
} else if (format == OBS_COMBO_FORMAT_FLOAT) {
double val = obs_property_list_item_float(property, i);
var = QVariant::fromValue<double>(val);
} else if (format == OBS_COMBO_FORMAT_STRING) {
var = QByteArray(obs_property_list_item_string(property, i));
}
widget->addItem(QString::fromUtf8(obs_property_list_item_name(property, i)), var);
}
if (list_type == OBS_COMBO_TYPE_EDITABLE)
widget->setEditable(true);
auto name = obs_property_name(property);
QVariant value;
switch (format) {
case OBS_COMBO_FORMAT_INT:
value = QVariant::fromValue(obs_data_get_int(settings, name));
break;
case OBS_COMBO_FORMAT_FLOAT:
value = QVariant::fromValue(obs_data_get_double(settings, name));
break;
case OBS_COMBO_FORMAT_STRING:
value = QByteArray(obs_data_get_string(settings, name));
break;
default:;
}
if (format == OBS_COMBO_FORMAT_STRING && list_type == OBS_COMBO_TYPE_EDITABLE) {
widget->lineEdit()->setText(value.toString());
} else {
auto idx = widget->findData(value);
if (idx != -1)
widget->setCurrentIndex(idx);
}
auto label = new QLabel(QString::fromUtf8(obs_property_description(property)));
layout->addRow(label, widget);
if (!obs_property_visible(property)) {
widget->setVisible(false);
label->setVisible(false);
}
widgets->emplace(property, widget);
switch (format) {
case OBS_COMBO_FORMAT_INT:
connect(widget, &QComboBox::currentIndexChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_int(settings, obs_property_name(property), widget->currentData().toInt());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
break;
case OBS_COMBO_FORMAT_FLOAT:
connect(widget, &QComboBox::currentIndexChanged, [this, property, settings, widget, widgets, layout] {
obs_data_set_double(settings, obs_property_name(property), widget->currentData().toDouble());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
break;
case OBS_COMBO_FORMAT_STRING:
if (list_type == OBS_COMBO_TYPE_EDITABLE) {
connect(widget, &QComboBox::currentTextChanged,
[this, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property),
widget->currentText().toUtf8().constData());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
} else {
connect(widget, &QComboBox::currentIndexChanged,
[this, property, settings, widget, widgets, layout] {
obs_data_set_string(settings, obs_property_name(property),
widget->currentData().toString().toUtf8().constData());
if (obs_property_modified(property, settings)) {
RefreshProperties(widgets, layout);
}
});
}
break;
default:;
}
} else {
// OBS_PROPERTY_PATH
// OBS_PROPERTY_COLOR
// OBS_PROPERTY_BUTTON
// OBS_PROPERTY_FONT
// OBS_PROPERTY_EDITABLE_LIST
// OBS_PROPERTY_FRAME_RATE
// OBS_PROPERTY_GROUP
// OBS_PROPERTY_COLOR_ALPHA
}
obs_property_modified(property, settings);
}
void OBSBasicSettings::LoadProperty(obs_property_t *prop, obs_data_t *settings, QWidget *widget)
{
auto type = obs_property_get_type(prop);
if (type == OBS_PROPERTY_BOOL) {
((QCheckBox *)widget)->setChecked(obs_data_get_bool(settings, obs_property_name(prop)));
} else if (type == OBS_PROPERTY_INT) {
((QSpinBox *)widget)->setValue((int)obs_data_get_int(settings, obs_property_name(prop)));
} else if (type == OBS_PROPERTY_FLOAT) {
((QDoubleSpinBox *)widget)->setValue(obs_data_get_double(settings, obs_property_name(prop)));
} else if (type == OBS_PROPERTY_TEXT) {
obs_text_type text_type = obs_property_text_type(prop);
if (text_type == OBS_TEXT_MULTILINE) {
((QPlainTextEdit *)widget)
->setPlainText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(prop))));
} else if (text_type != OBS_TEXT_INFO) {
((QLineEdit *)widget)->setText(QString::fromUtf8(obs_data_get_string(settings, obs_property_name(prop))));
}
} else if (type == OBS_PROPERTY_LIST) {
obs_combo_format format = obs_property_list_format(prop);
auto name = obs_property_name(prop);
QVariant value;
switch (format) {
case OBS_COMBO_FORMAT_INT:
value = QVariant::fromValue(obs_data_get_int(settings, name));
break;
case OBS_COMBO_FORMAT_FLOAT:
value = QVariant::fromValue(obs_data_get_double(settings, name));
break;
case OBS_COMBO_FORMAT_STRING:
value = QByteArray(obs_data_get_string(settings, name));
break;
default:;
}
if (format == OBS_COMBO_FORMAT_STRING && obs_property_list_type(prop) == OBS_COMBO_TYPE_EDITABLE) {
((QComboBox *)widget)->lineEdit()->setText(value.toString());
} else {
auto idx = ((QComboBox *)widget)->findData(value);
if (idx != -1)
((QComboBox *)widget)->setCurrentIndex(idx);
}
} else {
// OBS_PROPERTY_PATH
// OBS_PROPERTY_COLOR
// OBS_PROPERTY_BUTTON
// OBS_PROPERTY_FONT
// OBS_PROPERTY_EDITABLE_LIST
// OBS_PROPERTY_FRAME_RATE
// OBS_PROPERTY_GROUP
// OBS_PROPERTY_COLOR_ALPHA
}
}
void OBSBasicSettings::RefreshProperties(std::map<obs_property_t *, QWidget *> *widgets, QFormLayout *layout)
{
if (widgets == &stream_encoder_property_widgets) {
obs_property_t *property = obs_properties_first(stream_encoder_properties);
while (property) {
auto widget = widgets->at(property);
auto visible = obs_property_visible(property);
if (widget->isVisible() != visible) {
widget->setVisible(visible);
int row = 0;
layout->getWidgetPosition(widget, &row, nullptr);
auto item = layout->itemAt(row, QFormLayout::LabelRole);
if (item) {
widget = item->widget();
if (widget)
widget->setVisible(visible);
}
}
obs_property_next(&property);
}
} else if (widgets == &record_encoder_property_widgets) {
obs_property_t *property = obs_properties_first(record_encoder_properties);
while (property) {
auto widget = widgets->at(property);
auto visible = obs_property_visible(property);
if (widget->isVisible() != visible) {
widget->setVisible(visible);
int row = 0;
layout->getWidgetPosition(widget, &row, nullptr);
auto item = layout->itemAt(row, QFormLayout::LabelRole);
if (item) {
widget = item->widget();
if (widget)
widget->setVisible(visible);
}
}
obs_property_next(&property);
}
}
}