mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-03-21 17:34:57 -05:00
590 lines
14 KiB
C++
590 lines
14 KiB
C++
#include "advanced-scene-switcher.hpp"
|
|
#include "layout-helpers.hpp"
|
|
#include "source-helpers.hpp"
|
|
#include "selection-helpers.hpp"
|
|
#include "switcher-data.hpp"
|
|
#include "ui-helpers.hpp"
|
|
#include "utility.hpp"
|
|
|
|
#include <QFileDialog>
|
|
#include <QBuffer>
|
|
#include <QToolTip>
|
|
#include <thread>
|
|
|
|
namespace advss {
|
|
|
|
bool VideoSwitch::pause = false;
|
|
static QObject *addPulse = nullptr;
|
|
|
|
void AdvSceneSwitcher::on_videoAdd_clicked()
|
|
{
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switcher->videoSwitches.emplace_back();
|
|
|
|
VideoSwitchWidget *sw =
|
|
new VideoSwitchWidget(this, &switcher->videoSwitches.back());
|
|
|
|
listAddClicked(ui->videoSwitches, sw, ui->videoAdd, addPulse);
|
|
|
|
ui->videoHelp->setVisible(false);
|
|
}
|
|
|
|
void AdvSceneSwitcher::on_videoRemove_clicked()
|
|
{
|
|
QListWidgetItem *item = ui->videoSwitches->currentItem();
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
int idx = ui->videoSwitches->currentRow();
|
|
auto &switches = switcher->videoSwitches;
|
|
switches.erase(switches.begin() + idx);
|
|
}
|
|
|
|
delete item;
|
|
}
|
|
|
|
void AdvSceneSwitcher::on_videoUp_clicked()
|
|
{
|
|
int index = ui->videoSwitches->currentRow();
|
|
if (!listMoveUp(ui->videoSwitches)) {
|
|
return;
|
|
}
|
|
|
|
VideoSwitchWidget *s1 =
|
|
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
|
|
ui->videoSwitches->item(index));
|
|
VideoSwitchWidget *s2 =
|
|
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
|
|
ui->videoSwitches->item(index - 1));
|
|
VideoSwitchWidget::swapSwitchData(s1, s2);
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
|
|
std::swap(switcher->videoSwitches[index],
|
|
switcher->videoSwitches[index - 1]);
|
|
}
|
|
|
|
void AdvSceneSwitcher::on_videoDown_clicked()
|
|
{
|
|
int index = ui->videoSwitches->currentRow();
|
|
|
|
if (!listMoveDown(ui->videoSwitches)) {
|
|
return;
|
|
}
|
|
|
|
VideoSwitchWidget *s1 =
|
|
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
|
|
ui->videoSwitches->item(index));
|
|
VideoSwitchWidget *s2 =
|
|
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(
|
|
ui->videoSwitches->item(index + 1));
|
|
VideoSwitchWidget::swapSwitchData(s1, s2);
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
|
|
std::swap(switcher->videoSwitches[index],
|
|
switcher->videoSwitches[index + 1]);
|
|
}
|
|
|
|
void AdvSceneSwitcher::on_getScreenshot_clicked()
|
|
{
|
|
QListWidgetItem *item = ui->videoSwitches->currentItem();
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
VideoSwitchWidget *sw =
|
|
(VideoSwitchWidget *)ui->videoSwitches->itemWidget(item);
|
|
auto s = sw->getSwitchData();
|
|
if (!s || !s->videoSource) {
|
|
return;
|
|
}
|
|
|
|
auto source = obs_weak_source_get_source(s->videoSource);
|
|
auto screenshotData = std::make_unique<ScreenshotHelper>(source);
|
|
obs_source_release(source);
|
|
|
|
QString filePath = QFileDialog::getSaveFileName(this);
|
|
if (filePath.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QFile file(filePath);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
|
return;
|
|
}
|
|
|
|
// During selection of the save path enough time should usually have
|
|
// passed already
|
|
// Add this just in case ...
|
|
if (!screenshotData->done) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
|
|
if (!screenshotData->done) {
|
|
DisplayMessage("Failed to get screenshot of source!");
|
|
return;
|
|
}
|
|
|
|
screenshotData->image.save(file.fileName());
|
|
sw->SetFilePath(file.fileName());
|
|
}
|
|
|
|
bool SwitcherData::checkVideoSwitch(OBSWeakSource &scene,
|
|
OBSWeakSource &transition)
|
|
{
|
|
if (VideoSwitch::pause) {
|
|
return false;
|
|
}
|
|
|
|
bool match = false;
|
|
for (auto &s : videoSwitches) {
|
|
bool matched = s.checkMatch();
|
|
if (!match && matched) {
|
|
match = true;
|
|
scene = s.getScene();
|
|
transition = s.transition;
|
|
if (VerboseLoggingEnabled()) {
|
|
s.logMatch();
|
|
}
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
void SwitcherData::saveVideoSwitches(obs_data_t *obj)
|
|
{
|
|
obs_data_array_t *videoArray = obs_data_array_create();
|
|
for (VideoSwitch &s : videoSwitches) {
|
|
obs_data_t *array_obj = obs_data_create();
|
|
|
|
s.save(array_obj);
|
|
obs_data_array_push_back(videoArray, array_obj);
|
|
|
|
obs_data_release(array_obj);
|
|
}
|
|
obs_data_set_array(obj, "videoSwitches", videoArray);
|
|
obs_data_array_release(videoArray);
|
|
}
|
|
|
|
void SwitcherData::loadVideoSwitches(obs_data_t *obj)
|
|
{
|
|
videoSwitches.clear();
|
|
|
|
obs_data_array_t *videoArray = obs_data_get_array(obj, "videoSwitches");
|
|
size_t count = obs_data_array_count(videoArray);
|
|
|
|
for (size_t i = 0; i < count; i++) {
|
|
obs_data_t *array_obj = obs_data_array_item(videoArray, i);
|
|
|
|
videoSwitches.emplace_back();
|
|
videoSwitches.back().load(array_obj);
|
|
|
|
obs_data_release(array_obj);
|
|
}
|
|
obs_data_array_release(videoArray);
|
|
}
|
|
|
|
void AdvSceneSwitcher::SetupVideoTab()
|
|
{
|
|
for (auto &s : switcher->videoSwitches) {
|
|
QListWidgetItem *item;
|
|
item = new QListWidgetItem(ui->videoSwitches);
|
|
ui->videoSwitches->addItem(item);
|
|
VideoSwitchWidget *sw = new VideoSwitchWidget(this, &s);
|
|
item->setSizeHint(sw->minimumSizeHint());
|
|
ui->videoSwitches->setItemWidget(item, sw);
|
|
}
|
|
|
|
if (switcher->videoSwitches.size() == 0) {
|
|
if (!switcher->disableHints) {
|
|
addPulse = HighlightWidget(ui->videoAdd,
|
|
QColor(Qt::green));
|
|
}
|
|
ui->videoHelp->setVisible(true);
|
|
} else {
|
|
ui->videoHelp->setVisible(false);
|
|
}
|
|
|
|
ui->getScreenshot->setToolTip(
|
|
obs_module_text("AdvSceneSwitcher.videoTab.getScreenshotHelp"));
|
|
}
|
|
|
|
bool VideoSwitch::initialized()
|
|
{
|
|
return SceneSwitcherEntry::initialized() && videoSource;
|
|
}
|
|
|
|
bool VideoSwitch::valid()
|
|
{
|
|
return !initialized() ||
|
|
(SceneSwitcherEntry::valid() && WeakSourceValid(videoSource));
|
|
}
|
|
|
|
void VideoSwitch::save(obs_data_t *obj)
|
|
{
|
|
SceneSwitcherEntry::save(obj);
|
|
|
|
obs_data_set_string(obj, "videoSource",
|
|
GetWeakSourceName(videoSource).c_str());
|
|
obs_data_set_int(obj, "condition", static_cast<int>(condition));
|
|
obs_data_set_double(obj, "duration", duration);
|
|
obs_data_set_string(obj, "filePath", file.c_str());
|
|
obs_data_set_bool(obj, "ignoreInactiveSource", ignoreInactiveSource);
|
|
}
|
|
|
|
bool requiresFileInput(videoSwitchType t)
|
|
{
|
|
return t == videoSwitchType::MATCH || t == videoSwitchType::DIFFER;
|
|
}
|
|
|
|
void VideoSwitch::load(obs_data_t *obj)
|
|
{
|
|
SceneSwitcherEntry::load(obj);
|
|
|
|
const char *videoSourceName = obs_data_get_string(obj, "videoSource");
|
|
videoSource = GetWeakSourceByName(videoSourceName);
|
|
condition = static_cast<videoSwitchType>(
|
|
obs_data_get_int(obj, "condition"));
|
|
duration = obs_data_get_double(obj, "duration");
|
|
file = obs_data_get_string(obj, "filePath");
|
|
ignoreInactiveSource = obs_data_get_bool(obj, "ignoreInactiveSource");
|
|
|
|
if (requiresFileInput(condition)) {
|
|
(void)loadImageFromFile();
|
|
}
|
|
}
|
|
|
|
void VideoSwitch::getScreenshot()
|
|
{
|
|
auto source = obs_weak_source_get_source(videoSource);
|
|
screenshotData = std::make_unique<ScreenshotHelper>(source);
|
|
obs_source_release(source);
|
|
}
|
|
|
|
bool VideoSwitch::loadImageFromFile()
|
|
{
|
|
if (!matchImage.load(QString::fromStdString(file))) {
|
|
blog(LOG_WARNING, "Cannot load image data from file '%s'",
|
|
file.c_str());
|
|
return false;
|
|
}
|
|
matchImage =
|
|
matchImage.convertToFormat(QImage::Format::Format_RGBA8888);
|
|
return true;
|
|
}
|
|
|
|
bool VideoSwitch::checkMatch()
|
|
{
|
|
if (ignoreInactiveSource) {
|
|
obs_source_t *vs = obs_weak_source_get_source(videoSource);
|
|
bool videoActive = obs_source_active(vs);
|
|
obs_source_release(vs);
|
|
|
|
if (!videoActive) {
|
|
screenshotData.reset(nullptr);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool match = false;
|
|
|
|
if (screenshotData) {
|
|
if (screenshotData->done) {
|
|
bool conditionMatch = false;
|
|
|
|
switch (condition) {
|
|
case videoSwitchType::MATCH:
|
|
conditionMatch = screenshotData->image ==
|
|
matchImage;
|
|
break;
|
|
case videoSwitchType::DIFFER:
|
|
conditionMatch = screenshotData->image !=
|
|
matchImage;
|
|
break;
|
|
case videoSwitchType::HAS_NOT_CHANGED:
|
|
conditionMatch = screenshotData->image ==
|
|
matchImage;
|
|
break;
|
|
case videoSwitchType::HAS_CHANGED:
|
|
conditionMatch = screenshotData->image !=
|
|
matchImage;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (conditionMatch) {
|
|
currentMatchDuration +=
|
|
std::chrono::duration_cast<
|
|
std::chrono::milliseconds>(
|
|
screenshotData->time -
|
|
previousTime);
|
|
} else {
|
|
currentMatchDuration = {};
|
|
}
|
|
|
|
bool durationMatch = currentMatchDuration.count() >=
|
|
duration * 1000;
|
|
|
|
if (conditionMatch && durationMatch) {
|
|
match = true;
|
|
}
|
|
|
|
if (!requiresFileInput(condition)) {
|
|
matchImage = std::move(screenshotData->image);
|
|
}
|
|
previousTime = std::move(screenshotData->time);
|
|
|
|
screenshotData.reset(nullptr);
|
|
}
|
|
}
|
|
|
|
getScreenshot();
|
|
return match;
|
|
}
|
|
|
|
void swap(VideoSwitch &first, VideoSwitch &second)
|
|
{
|
|
std::swap(first.targetType, second.targetType);
|
|
std::swap(first.group, second.group);
|
|
std::swap(first.scene, second.scene);
|
|
std::swap(first.transition, second.transition);
|
|
std::swap(first.usePreviousScene, second.usePreviousScene);
|
|
std::swap(first.videoSource, second.videoSource);
|
|
}
|
|
|
|
static inline void populateConditionSelection(QComboBox *list)
|
|
{
|
|
list->addItem(
|
|
obs_module_text("AdvSceneSwitcher.videoTab.condition.match"));
|
|
list->setItemData(
|
|
0,
|
|
obs_module_text(
|
|
"AdvSceneSwitcher.videoTab.condition.match.tooltip"),
|
|
Qt::ToolTipRole);
|
|
list->addItem(
|
|
obs_module_text("AdvSceneSwitcher.videoTab.condition.differ"));
|
|
list->addItem(obs_module_text(
|
|
"AdvSceneSwitcher.videoTab.condition.hasNotChanged"));
|
|
list->addItem(obs_module_text(
|
|
"AdvSceneSwitcher.videoTab.condition.hasChanged"));
|
|
}
|
|
|
|
VideoSwitchWidget::VideoSwitchWidget(QWidget *parent, VideoSwitch *s)
|
|
: SwitchWidget(parent, s, true, true)
|
|
{
|
|
videoSources = new QComboBox();
|
|
condition = new QComboBox();
|
|
duration = new QDoubleSpinBox();
|
|
filePath = new QLineEdit();
|
|
browseButton =
|
|
new QPushButton(obs_module_text("AdvSceneSwitcher.browse"));
|
|
ignoreInactiveSource = new QCheckBox(obs_module_text(
|
|
"AdvSceneSwitcher.videoTab.ignoreInactiveSource"));
|
|
|
|
filePath->setFixedWidth(100);
|
|
|
|
browseButton->setStyleSheet("border:1px solid gray;");
|
|
|
|
duration->setMinimum(0.0);
|
|
duration->setMaximum(99.000000);
|
|
duration->setSuffix("s");
|
|
|
|
QWidget::connect(videoSources,
|
|
SIGNAL(currentTextChanged(const QString &)), this,
|
|
SLOT(SourceChanged(const QString &)));
|
|
QWidget::connect(condition, SIGNAL(currentIndexChanged(int)), this,
|
|
SLOT(ConditionChanged(int)));
|
|
QWidget::connect(duration, SIGNAL(valueChanged(double)), this,
|
|
SLOT(DurationChanged(double)));
|
|
QWidget::connect(filePath, SIGNAL(editingFinished()), this,
|
|
SLOT(FilePathChanged()));
|
|
QWidget::connect(browseButton, SIGNAL(clicked()), this,
|
|
SLOT(BrowseButtonClicked()));
|
|
QWidget::connect(ignoreInactiveSource, SIGNAL(stateChanged(int)), this,
|
|
SLOT(IgnoreInactiveChanged(int)));
|
|
|
|
PopulateVideoSelection(videoSources);
|
|
populateConditionSelection(condition);
|
|
|
|
if (s) {
|
|
videoSources->setCurrentText(
|
|
GetWeakSourceName(s->videoSource).c_str());
|
|
condition->setCurrentIndex(static_cast<int>(s->condition));
|
|
duration->setValue(s->duration);
|
|
filePath->setText(QString::fromStdString(s->file));
|
|
ignoreInactiveSource->setChecked(s->ignoreInactiveSource);
|
|
|
|
if (!requiresFileInput(s->condition)) {
|
|
filePath->hide();
|
|
browseButton->hide();
|
|
}
|
|
}
|
|
|
|
QHBoxLayout *switchLayout = new QHBoxLayout;
|
|
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
|
{"{{videoSources}}", videoSources},
|
|
{"{{condition}}", condition},
|
|
{"{{duration}}", duration},
|
|
{"{{filePath}}", filePath},
|
|
{"{{browseButton}}", browseButton},
|
|
{"{{ignoreInactiveSource}}", ignoreInactiveSource},
|
|
{"{{scenes}}", scenes},
|
|
{"{{transitions}}", transitions}};
|
|
PlaceWidgets(obs_module_text("AdvSceneSwitcher.videoTab.entry"),
|
|
switchLayout, widgetPlaceholders);
|
|
|
|
QVBoxLayout *mainLayout = new QVBoxLayout;
|
|
|
|
mainLayout->addLayout(switchLayout);
|
|
setLayout(mainLayout);
|
|
|
|
switchData = s;
|
|
UpdatePreviewTooltip();
|
|
|
|
loading = false;
|
|
}
|
|
|
|
VideoSwitch *VideoSwitchWidget::getSwitchData()
|
|
{
|
|
return switchData;
|
|
}
|
|
|
|
void VideoSwitchWidget::setSwitchData(VideoSwitch *s)
|
|
{
|
|
switchData = s;
|
|
}
|
|
|
|
void VideoSwitchWidget::swapSwitchData(VideoSwitchWidget *s1,
|
|
VideoSwitchWidget *s2)
|
|
{
|
|
SwitchWidget::swapSwitchData(s1, s2);
|
|
|
|
VideoSwitch *t = s1->getSwitchData();
|
|
s1->setSwitchData(s2->getSwitchData());
|
|
s2->setSwitchData(t);
|
|
}
|
|
|
|
void VideoSwitchWidget::UpdatePreviewTooltip()
|
|
{
|
|
if (!switchData || !requiresFileInput(switchData->condition)) {
|
|
return;
|
|
}
|
|
|
|
QImage preview =
|
|
switchData->matchImage.scaled({300, 300}, Qt::KeepAspectRatio);
|
|
|
|
QByteArray data;
|
|
QBuffer buffer(&data);
|
|
if (!preview.save(&buffer, "PNG")) {
|
|
return;
|
|
}
|
|
|
|
QString html =
|
|
QString("<html><img src='data:image/png;base64, %0'/></html>")
|
|
.arg(QString(data.toBase64()));
|
|
this->setToolTip(html);
|
|
}
|
|
|
|
void VideoSwitchWidget::SetFilePath(const QString &text)
|
|
{
|
|
filePath->setText(text);
|
|
FilePathChanged();
|
|
}
|
|
|
|
void VideoSwitchWidget::SourceChanged(const QString &text)
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switchData->videoSource = GetWeakSourceByQString(text);
|
|
}
|
|
|
|
void VideoSwitchWidget::ConditionChanged(int cond)
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switchData->condition = static_cast<videoSwitchType>(cond);
|
|
|
|
if (requiresFileInput(switchData->condition)) {
|
|
filePath->show();
|
|
browseButton->show();
|
|
} else {
|
|
filePath->hide();
|
|
browseButton->hide();
|
|
}
|
|
|
|
// Reload image data to avoid incorrect matches.
|
|
//
|
|
// Condition type HAS_NOT_CHANGED will use matchImage to store previous
|
|
// frame of video source, which will differ from the image stored at
|
|
// specified file location.
|
|
if (switchData->loadImageFromFile()) {
|
|
UpdatePreviewTooltip();
|
|
}
|
|
}
|
|
|
|
void VideoSwitchWidget::DurationChanged(double dur)
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switchData->duration = dur;
|
|
}
|
|
|
|
void VideoSwitchWidget::FilePathChanged()
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switchData->file = filePath->text().toUtf8().constData();
|
|
if (switchData->loadImageFromFile()) {
|
|
UpdatePreviewTooltip();
|
|
}
|
|
}
|
|
|
|
void VideoSwitchWidget::BrowseButtonClicked()
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
QString path = QFileDialog::getOpenFileName(
|
|
this,
|
|
tr(obs_module_text("AdvSceneSwitcher.fileTab.selectRead")),
|
|
QDir::currentPath(),
|
|
tr(obs_module_text("AdvSceneSwitcher.fileTab.anyFileType")));
|
|
if (path.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
filePath->setText(path);
|
|
FilePathChanged();
|
|
}
|
|
|
|
void VideoSwitchWidget::IgnoreInactiveChanged(int state)
|
|
{
|
|
if (loading || !switchData) {
|
|
return;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(switcher->m);
|
|
switchData->ignoreInactiveSource = state;
|
|
}
|
|
|
|
} // namespace advss
|