#include "macro-condition-video.hpp" #include "cascade-classifier-detector.hpp" #include "screenshot-dialog.hpp" #include "layout-helpers.hpp" #include "macro-condition-edit.hpp" #include "plugin-state-helpers.hpp" #include "selection-helpers.hpp" #include "ui-helpers.hpp" #include #include #include namespace advss { const std::string MacroConditionVideo::id = "video"; bool MacroConditionVideo::_registered = MacroConditionFactory::Register( MacroConditionVideo::id, {MacroConditionVideo::Create, MacroConditionVideoEdit::Create, "AdvSceneSwitcher.condition.video"}); const static std::map videoInputTypes = { {VideoInput::Type::OBS_MAIN_OUTPUT, "AdvSceneSwitcher.condition.video.type.main"}, {VideoInput::Type::SOURCE, "AdvSceneSwitcher.condition.video.type.source"}, {VideoInput::Type::SCENE, "AdvSceneSwitcher.condition.video.type.scene"}, }; const static std::map patternMatchModes = { {cv::TemplateMatchModes::TM_CCOEFF_NORMED, "AdvSceneSwitcher.condition.video.patternMatchMode.correlationCoefficient"}, {cv::TemplateMatchModes::TM_CCORR_NORMED, "AdvSceneSwitcher.condition.video.patternMatchMode.crossCorrelation"}, {cv::TemplateMatchModes::TM_SQDIFF_NORMED, "AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference"}, }; static bool requiresFileInput(VideoCondition t) { return t == VideoCondition::MATCH || t == VideoCondition::DIFFER || t == VideoCondition::PATTERN; } bool MacroConditionVideo::CheckShouldBeSkipped() { if (_condition != VideoCondition::PATTERN && _condition != VideoCondition::OBJECT_CASCADE && _condition != VideoCondition::HAS_CHANGED && _condition != VideoCondition::HAS_NOT_CHANGED) { return false; } if (_throttleEnabled) { if (_runCount <= _throttleCount) { _runCount++; return true; } else { _runCount = 0; } } return false; } MacroConditionVideo::MacroConditionVideo(Macro *m) : QObject(), MacroCondition(m, true) { } void MacroConditionVideo::UpdateActiveKeeper() { _activeKeeper.SetActive(_keepActive); if (_video.type == VideoInput::Type::OBS_MAIN_OUTPUT) { return; } if (!_keepActive) { _lastActiveKeeperSource = nullptr; return; } auto videoSource = _video.GetVideo(); if (videoSource == _lastActiveKeeperSource) { return; } _lastActiveKeeperSource = videoSource; _activeKeeper.SetSource(OBSGetStrongRef(videoSource)); } bool MacroConditionVideo::CheckCondition() { if (!_video.ValidSelection()) { return false; } UpdateActiveKeeper(); bool match = false; if (CheckShouldBeSkipped()) { return _lastMatchResult; } if (!FileInputIsUpToDate()) { LoadImageFromFile(); } if (_blockUntilScreenshotDone) { GetScreenshot(true); } if (_screenshotData.IsDone()) { match = Compare(); _lastMatchResult = match; if (!requiresFileInput(_condition)) { _matchImage = std::move(_screenshotData.GetImage()); } _getNextScreenshot = true; } else { match = _lastMatchResult; } if (!_blockUntilScreenshotDone && _getNextScreenshot) { GetScreenshot(); } return match; } bool MacroConditionVideo::Save(obs_data_t *obj) const { MacroCondition::Save(obj); _video.Save(obj); obs_data_set_bool(obj, "keepActive", _keepActive); obs_data_set_int(obj, "condition", static_cast(_condition)); obs_data_set_string(obj, "filePath", _file.c_str()); obs_data_set_bool(obj, "blockUntilScreenshotDone", _blockUntilScreenshotDone); _brightnessThreshold.Save(obj, "brightnessThreshold"); _patternMatchParameters.Save(obj); _cascadeMatchParameters.Save(obj); _ocrParameters.Save(obj); _colorParameters.Save(obj); obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled); obs_data_set_int(obj, "throttleCount", _throttleCount); _areaParameters.Save(obj); return true; } bool MacroConditionVideo::Load(obs_data_t *obj) { MacroCondition::Load(obj); _video.Load(obj); _keepActive = obs_data_get_bool(obj, "keepActive"); SetCondition(static_cast( obs_data_get_int(obj, "condition"))); _file = obs_data_get_string(obj, "filePath"); _blockUntilScreenshotDone = obs_data_get_bool(obj, "blockUntilScreenshotDone"); // TODO: Remove this fallback in a future version if (obs_data_has_user_value(obj, "brightness")) { _brightnessThreshold = obs_data_get_double(obj, "brightness"); } else { _brightnessThreshold.Load(obj, "brightnessThreshold"); } _patternMatchParameters.Load(obj); _cascadeMatchParameters.Load(obj); _ocrParameters.Load(obj); _colorParameters.Load(obj); _throttleEnabled = obs_data_get_bool(obj, "throttleEnabled"); _throttleCount = obs_data_get_int(obj, "throttleCount"); _areaParameters.Load(obj); if (requiresFileInput(_condition)) { (void)LoadImageFromFile(); } return true; } std::string MacroConditionVideo::GetShortDesc() const { return _video.ToString(); } void MacroConditionVideo::GetScreenshot(bool blocking) { auto source = obs_weak_source_get_source(_video.GetVideo()); _screenshotData.~Screenshot(); QRect screenshotArea; if (_areaParameters.enable && _condition != VideoCondition::NO_IMAGE) { screenshotArea.setRect(_areaParameters.area.x, _areaParameters.area.y, _areaParameters.area.width, _areaParameters.area.height); } const int timeout = GetIntervalValue() < 300 ? 300 : GetIntervalValue(); new (&_screenshotData) Screenshot(source, screenshotArea, blocking, timeout); obs_source_release(source); _getNextScreenshot = false; } bool MacroConditionVideo::LoadImageFromFile() { const QFileInfo info(QString::fromStdString(_file)); _loadedFileLastModified = info.lastModified(); _loadedFile = _file; if (!_matchImage.load(info.absoluteFilePath())) { blog(LOG_WARNING, "Cannot load image data from file '%s'", _file.c_str()); (&_matchImage)->~QImage(); new (&_matchImage) QImage(); _patternImageData = {}; return false; } _matchImage = _matchImage.convertToFormat(QImage::Format::Format_RGBA8888); _patternMatchParameters.image = _matchImage; _patternImageData = CreatePatternData(_matchImage); emit InputFileChanged(); return true; } void MacroConditionVideo::SetPageSegMode(tesseract::PageSegMode mode) { _ocrParameters.SetPageMode(mode); } bool MacroConditionVideo::SetLanguageCode(const std::string &language) { return _ocrParameters.SetLanguageCode(language); } bool MacroConditionVideo::SetTesseractBaseDir(const std::string &dir) { return _ocrParameters.SetTesseractBasePath(dir); } void MacroConditionVideo::SetCondition(VideoCondition condition) { _condition = condition; SetupTempVars(); } bool MacroConditionVideo::ScreenshotContainsPattern() { cv::Mat result; double bestMatchValue = MatchPattern(_screenshotData.GetImage(), _patternImageData, _patternMatchParameters.threshold, result, _patternMatchParameters.useAlphaAsMask, _patternMatchParameters.matchMode); if (result.total() == 0) { SetTempVarValue("similarity", std::to_string(bestMatchValue)); SetTempVarValue("patternCount", "0"); SetTempVarValue("matchX", "-1"); SetTempVarValue("matchY", "-1"); SetTempVarValue("matchWidth", "0"); SetTempVarValue("matchHeight", "0"); return false; } SetTempVarValue("similarity", std::to_string(bestMatchValue)); SetTempVarValue("matchWidth", std::to_string(_patternImageData.rgbaPattern.cols)); SetTempVarValue("matchHeight", std::to_string(_patternImageData.rgbaPattern.rows)); if (IsTempVarInUse("matchX") || IsTempVarInUse("matchY")) { double maxVal; cv::Point maxLoc; cv::minMaxLoc(result, nullptr, &maxVal, nullptr, &maxLoc); if (maxVal > 0.0) { int matchX = maxLoc.x; int matchY = maxLoc.y; if (_areaParameters.enable) { matchX += _areaParameters.area.x; matchY += _areaParameters.area.y; } SetTempVarValue("matchX", std::to_string(matchX)); SetTempVarValue("matchY", std::to_string(matchY)); } else { SetTempVarValue("matchX", "-1"); SetTempVarValue("matchY", "-1"); } } if (IsTempVarInUse("patternCount")) { const auto count = CountPatternMatches( result, {_patternImageData.rgbaPattern.cols, _patternImageData.rgbaPattern.rows}); SetTempVarValue("patternCount", std::to_string(count)); return count > 0; } return countNonZero(result) > 0; } bool MacroConditionVideo::FileInputIsUpToDate() const { if (!requiresFileInput(_condition)) { return true; } const QFileInfo info(QString::fromStdString(_file)); return (_loadedFileLastModified == info.lastModified()) && (_file == _loadedFile); } bool MacroConditionVideo::OutputChanged() { if (!_patternMatchParameters.useForChangedCheck) { return _screenshotData.GetImage() != _matchImage; } cv::Mat result; _patternImageData = CreatePatternData(_matchImage); double bestMatchValue = MatchPattern(_screenshotData.GetImage(), _patternImageData, _patternMatchParameters.threshold, result, _patternMatchParameters.useAlphaAsMask, _patternMatchParameters.matchMode); SetTempVarValue("similarity", std::to_string(bestMatchValue)); if (result.total() == 0) { return false; } return countNonZero(result) == 0; } bool MacroConditionVideo::ScreenshotContainsObject() { auto *detector = _cascadeMatchParameters.GetDetector(); if (!detector) { return false; } auto objects = detector->Detect(_screenshotData.GetImage()); const auto count = objects.size(); SetTempVarValue("objectCount", std::to_string(count)); return count > 0; } bool MacroConditionVideo::CheckBrightnessThreshold() { _currentBrightness = GetAvgBrightness(_screenshotData.GetImage()) / 255.; SetTempVarValue("brightness", std::to_string(_currentBrightness)); return _currentBrightness > _brightnessThreshold; } bool MacroConditionVideo::CheckOCR() { auto *ocr = _ocrParameters.GetOCR(); if (!ocr) { return false; } auto text = RunOCR(ocr, _screenshotData.GetImage(), _ocrParameters.color.GetValue(), _ocrParameters.colorThreshold); if (!text) { return false; } SetVariableValue(*text); SetTempVarValue("text", *text); if (!_ocrParameters.regex.Enabled()) { return text == std::string(_ocrParameters.text); } return _ocrParameters.regex.Matches(*text, _ocrParameters.text); } bool MacroConditionVideo::CheckColor() { const bool ret = ContainsPixelsInColorRange( _screenshotData.GetImage(), _colorParameters.color.GetValue(), _colorParameters.colorThreshold, _colorParameters.matchThreshold); SetTempVarValue("color", [&]() { return GetAverageColor(_screenshotData.GetImage()) .name(QColor::HexArgb) .toStdString(); }); SetTempVarValue("dominantColor", [&]() { return GetDominantColor(_screenshotData.GetImage(), 3) .name(QColor::HexArgb) .toStdString(); }); return ret; } bool MacroConditionVideo::Compare() { if (_condition != VideoCondition::OCR) { SetVariableValue(""); } switch (_condition) { case VideoCondition::MATCH: return _screenshotData.GetImage() == _matchImage; case VideoCondition::DIFFER: return _screenshotData.GetImage() != _matchImage; case VideoCondition::HAS_CHANGED: return OutputChanged(); case VideoCondition::HAS_NOT_CHANGED: return !OutputChanged(); case VideoCondition::NO_IMAGE: return _screenshotData.GetImage().isNull(); case VideoCondition::PATTERN: return ScreenshotContainsPattern(); case VideoCondition::OBJECT_CASCADE: return ScreenshotContainsObject(); case VideoCondition::BRIGHTNESS: return CheckBrightnessThreshold(); case VideoCondition::OCR: return CheckOCR(); case VideoCondition::COLOR: return CheckColor(); default: break; } return false; } void MacroConditionVideo::SetupTempVars() { MacroCondition::SetupTempVars(); switch (_condition) { case VideoCondition::HAS_CHANGED: case VideoCondition::HAS_NOT_CHANGED: if (!_patternMatchParameters.useForChangedCheck) { break; } AddTempvar( "similarity", obs_module_text( "AdvSceneSwitcher.tempVar.video.similarity"), obs_module_text( "AdvSceneSwitcher.tempVar.video.similarity.description")); break; case VideoCondition::PATTERN: AddTempvar( "similarity", obs_module_text( "AdvSceneSwitcher.tempVar.video.similarity"), obs_module_text( "AdvSceneSwitcher.tempVar.video.similarity.description")); AddTempvar( "patternCount", obs_module_text( "AdvSceneSwitcher.tempVar.video.patternCount"), obs_module_text( "AdvSceneSwitcher.tempVar.video.patternCount.description")); AddTempvar( "matchX", obs_module_text( "AdvSceneSwitcher.tempVar.video.matchX"), obs_module_text( "AdvSceneSwitcher.tempVar.video.matchX.description")); AddTempvar( "matchY", obs_module_text( "AdvSceneSwitcher.tempVar.video.matchY"), obs_module_text( "AdvSceneSwitcher.tempVar.video.matchY.description")); AddTempvar( "matchWidth", obs_module_text( "AdvSceneSwitcher.tempVar.video.matchWidth"), obs_module_text( "AdvSceneSwitcher.tempVar.video.matchWidth.description")); AddTempvar( "matchHeight", obs_module_text( "AdvSceneSwitcher.tempVar.video.matchHeight"), obs_module_text( "AdvSceneSwitcher.tempVar.video.matchHeight.description")); break; case VideoCondition::OBJECT_CASCADE: AddTempvar( "objectCount", obs_module_text( "AdvSceneSwitcher.tempVar.video.objectCount"), obs_module_text( "AdvSceneSwitcher.tempVar.video.objectCount.description")); break; case VideoCondition::BRIGHTNESS: AddTempvar( "brightness", obs_module_text( "AdvSceneSwitcher.tempVar.video.brightness"), obs_module_text( "AdvSceneSwitcher.tempVar.video.brightness.description")); break; case VideoCondition::OCR: AddTempvar( "text", obs_module_text("AdvSceneSwitcher.tempVar.video.text"), obs_module_text( "AdvSceneSwitcher.tempVar.video.text.description")); break; case VideoCondition::COLOR: AddTempvar( "color", obs_module_text("AdvSceneSwitcher.tempVar.video.color"), obs_module_text( "AdvSceneSwitcher.tempVar.video.color.description")); AddTempvar( "dominantColor", obs_module_text( "AdvSceneSwitcher.tempVar.video.dominantColor"), obs_module_text( "AdvSceneSwitcher.tempVar.video.dominantColor.description")); break; case VideoCondition::MATCH: case VideoCondition::DIFFER: case VideoCondition::NO_IMAGE: default: break; } } static inline void populateVideoInputSelection(QComboBox *list) { for (const auto &[_, name] : videoInputTypes) { list->addItem(obs_module_text(name.c_str())); } } static inline void populateConditionSelection(QComboBox *list) { const static std::vector> conditionTypes = { {VideoCondition::MATCH, "AdvSceneSwitcher.condition.video.condition.match"}, {VideoCondition::DIFFER, "AdvSceneSwitcher.condition.video.condition.differ"}, {VideoCondition::HAS_NOT_CHANGED, "AdvSceneSwitcher.condition.video.condition.hasNotChanged"}, {VideoCondition::HAS_CHANGED, "AdvSceneSwitcher.condition.video.condition.hasChanged"}, {VideoCondition::NO_IMAGE, "AdvSceneSwitcher.condition.video.condition.noImage"}, {VideoCondition::PATTERN, "AdvSceneSwitcher.condition.video.condition.pattern"}, {VideoCondition::OBJECT_CASCADE, "AdvSceneSwitcher.condition.video.condition.object"}, {VideoCondition::BRIGHTNESS, "AdvSceneSwitcher.condition.video.condition.brightness"}, #ifdef OCR_SUPPORT {VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"}, #endif {VideoCondition::COLOR, "AdvSceneSwitcher.condition.video.condition.color"}, }; for (auto &[value, name] : conditionTypes) { list->addItem(obs_module_text(name.c_str()), static_cast(value)); } SetRowVisibleByValue( list, obs_module_text( "AdvSceneSwitcher.condition.video.condition.object"), CascadeClassifierDetector::IsSupported()); } static inline void populatePatternMatchModeSelection(QComboBox *list) { for (const auto &[mode, name] : patternMatchModes) { list->addItem(obs_module_text(name.c_str()), static_cast(mode)); } } static QStringList getVideoSourcesList() { auto sources = GetVideoSourceNames(); sources.sort(); return sources; } MacroConditionVideoEdit::MacroConditionVideoEdit( QWidget *parent, std::shared_ptr entryData) : QWidget(parent), _videoInputTypes(new QComboBox()), _scenes(new SceneSelectionWidget(this, true, false, true, true, true)), _sources(new SourceSelectionWidget(this, getVideoSourcesList, true)), _condition(new QComboBox()), _reduceLatency(new QCheckBox(obs_module_text( "AdvSceneSwitcher.condition.video.reduceLatency"))), _usePatternForChangedCheck(new QCheckBox(obs_module_text( "AdvSceneSwitcher.condition.video.usePatternForChangedCheck"))), _imagePath(new FileSelection()), _patternThreshold(new SliderSpinBox( 0., 1., obs_module_text( "AdvSceneSwitcher.condition.video.patternThreshold"), obs_module_text( "AdvSceneSwitcher.condition.video.patternThresholdDescription"))), _useAlphaAsMask(new QCheckBox(obs_module_text( "AdvSceneSwitcher.condition.video.patternThresholdUseAlphaAsMask"))), _patternMatchModeLayout(new QHBoxLayout()), _patternMatchMode(new QComboBox()), _showMatch(new QPushButton(obs_module_text( "AdvSceneSwitcher.condition.video.showMatch"))), _previewDialog(this), _brightness(new BrightnessEdit(this, entryData)), _ocr(new OCREdit(this, &_previewDialog, entryData)), _cascadeClassifierEdit( new CascadeClassifierEdit(this, &_previewDialog, entryData)), _color(new ColorEdit(this, entryData)), _area(new AreaEdit(this, &_previewDialog, entryData)), _throttleControlLayout(new QHBoxLayout), _throttleEnable(new QCheckBox()), _throttleCount(new QSpinBox()), _keepActive(new QCheckBox( obs_module_text("AdvSceneSwitcher.keepSourceActive"))), _keepActiveHelp(new HelpIcon( obs_module_text("AdvSceneSwitcher.keepSourceActive.help"), this)) { _reduceLatency->setToolTip(obs_module_text( "AdvSceneSwitcher.condition.video.reduceLatency.tooltip")); _imagePath->Button()->disconnect(); _usePatternForChangedCheck->setToolTip(obs_module_text( "AdvSceneSwitcher.condition.video.usePatternForChangedCheck.tooltip")); _patternMatchMode->setToolTip(obs_module_text( "AdvSceneSwitcher.condition.video.patternMatchMode.tip")); populatePatternMatchModeSelection(_patternMatchMode); _throttleCount->setMinimum(1 * GetIntervalValue()); _throttleCount->setMaximum(10 * GetIntervalValue()); _throttleCount->setSingleStep(GetIntervalValue()); _brightness->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _ocr->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _cascadeClassifierEdit->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _color->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); _area->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); QWidget::connect(_videoInputTypes, SIGNAL(currentIndexChanged(int)), this, SLOT(VideoInputTypeChanged(int))); QWidget::connect(_sources, SIGNAL(SourceChanged(const SourceSelection &)), this, SLOT(SourceChanged(const SourceSelection &))); QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)), this, SLOT(SceneChanged(const SceneSelection &))); QWidget::connect(_condition, SIGNAL(currentIndexChanged(int)), this, SLOT(ConditionChanged(int))); QWidget::connect(_reduceLatency, SIGNAL(stateChanged(int)), this, SLOT(ReduceLatencyChanged(int))); QWidget::connect(_imagePath, SIGNAL(PathChanged(const QString &)), this, SLOT(ImagePathChanged(const QString &))); QWidget::connect(_imagePath->Button(), SIGNAL(clicked()), this, SLOT(ImageBrowseButtonClicked())); QWidget::connect(_usePatternForChangedCheck, SIGNAL(stateChanged(int)), this, SLOT(UsePatternForChangedCheckChanged(int))); QWidget::connect( _patternThreshold, SIGNAL(DoubleValueChanged(const NumberVariable &)), this, SLOT(PatternThresholdChanged(const NumberVariable &))); QWidget::connect(_useAlphaAsMask, SIGNAL(stateChanged(int)), this, SLOT(UseAlphaAsMaskChanged(int))); QWidget::connect(_patternMatchMode, SIGNAL(currentIndexChanged(int)), this, SLOT(PatternMatchModeChanged(int))); QWidget::connect(_throttleEnable, SIGNAL(stateChanged(int)), this, SLOT(ThrottleEnableChanged(int))); QWidget::connect(_throttleCount, SIGNAL(valueChanged(int)), this, SLOT(ThrottleCountChanged(int))); QWidget::connect(_keepActive, SIGNAL(stateChanged(int)), this, SLOT(KeepActiveChanged(int))); QWidget::connect(_showMatch, SIGNAL(clicked()), this, SLOT(ShowMatchClicked())); QWidget::connect(this, SIGNAL(VideoSelectionChanged(const VideoInput &)), &_previewDialog, SLOT(VideoSelectionChanged(const VideoInput &))); QWidget::connect(_area, SIGNAL(Resized()), this, SLOT(Resize())); QWidget::connect(entryData.get(), &MacroConditionVideo::InputFileChanged, this, [this]() { UpdatePreviewTooltip(); _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); }); populateVideoInputSelection(_videoInputTypes); populateConditionSelection(_condition); _patternMatchModeLayout->setContentsMargins(0, 0, 0, 0); _throttleControlLayout->setContentsMargins(0, 0, 0, 0); QHBoxLayout *entryLine1Layout = new QHBoxLayout; std::unordered_map widgetPlaceholders = { {"{{videoInputTypes}}", _videoInputTypes}, {"{{sources}}", _sources}, {"{{scenes}}", _scenes}, {"{{condition}}", _condition}, {"{{reduceLatency}}", _reduceLatency}, {"{{imagePath}}", _imagePath}, {"{{throttleEnable}}", _throttleEnable}, {"{{throttleCount}}", _throttleCount}, {"{{patternMatchingModes}}", _patternMatchMode}, }; PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.video.layout"), entryLine1Layout, widgetPlaceholders); PlaceWidgets( obs_module_text( "AdvSceneSwitcher.condition.video.patternMatchMode"), _patternMatchModeLayout, widgetPlaceholders); PlaceWidgets( obs_module_text( "AdvSceneSwitcher.condition.video.layout.throttle"), _throttleControlLayout, widgetPlaceholders); QHBoxLayout *keepActiveLayout = new QHBoxLayout; keepActiveLayout->addWidget(_keepActive); keepActiveLayout->addWidget(_keepActiveHelp); keepActiveLayout->addStretch(); QHBoxLayout *showMatchLayout = new QHBoxLayout; showMatchLayout->addWidget(_showMatch); showMatchLayout->addStretch(); auto mainLayout = new QVBoxLayout; mainLayout->addLayout(entryLine1Layout); mainLayout->addWidget(_usePatternForChangedCheck); mainLayout->addWidget(_patternThreshold); mainLayout->addWidget(_useAlphaAsMask); mainLayout->addLayout(_patternMatchModeLayout); mainLayout->addWidget(_brightness); mainLayout->addWidget(_ocr); mainLayout->addWidget(_cascadeClassifierEdit); mainLayout->addWidget(_color); mainLayout->addLayout(_throttleControlLayout); mainLayout->addWidget(_area); mainLayout->addLayout(keepActiveLayout); mainLayout->addWidget(_reduceLatency); mainLayout->addLayout(showMatchLayout); setLayout(mainLayout); _entryData = entryData; UpdateEntryData(); _loading = false; } void MacroConditionVideoEdit::UpdatePreviewTooltip() { if (!_entryData) { return; } if (!requiresFileInput(_entryData->GetCondition())) { this->setToolTip(""); return; } QImage preview = _entryData->GetMatchImage().scaled( {300, 300}, Qt::KeepAspectRatio); QByteArray data; QBuffer buffer(&data); if (!preview.save(&buffer, "PNG")) { return; } QString html = QString("") .arg(QString(data.toBase64())); this->setToolTip(html); } void MacroConditionVideoEdit::SourceChanged(const SourceSelection &source) { GUARD_LOADING_AND_LOCK(); _entryData->_video.source = source; HandleVideoInputUpdate(); } void MacroConditionVideoEdit::SceneChanged(const SceneSelection &scene) { GUARD_LOADING_AND_LOCK(); _entryData->_video.scene = scene; HandleVideoInputUpdate(); } void MacroConditionVideoEdit::VideoInputTypeChanged(int type) { GUARD_LOADING_AND_LOCK(); _entryData->_video.type = static_cast(type); HandleVideoInputUpdate(); SetWidgetVisibility(); } void MacroConditionVideoEdit::HandleVideoInputUpdate() { _entryData->ResetLastMatch(); emit HeaderInfoChanged( QString::fromStdString(_entryData->GetShortDesc())); emit VideoSelectionChanged(_entryData->_video); } void MacroConditionVideoEdit::ConditionChanged(int idx) { GUARD_LOADING_AND_LOCK(); _entryData->SetCondition( static_cast(_condition->itemData(idx).toInt())); _entryData->ResetLastMatch(); SetWidgetVisibility(); // 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 (_entryData->LoadImageFromFile()) { UpdatePreviewTooltip(); } _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); SetupPreviewDialogParams(); } void MacroConditionVideoEdit::ImagePathChanged(const QString &text) { GUARD_LOADING_AND_LOCK(); _entryData->_file = text.toUtf8().constData(); _entryData->ResetLastMatch(); if (_entryData->LoadImageFromFile()) { UpdatePreviewTooltip(); } _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); } void MacroConditionVideoEdit::ImageBrowseButtonClicked() { if (_loading || !_entryData) { return; } QString path; bool useExistingFile = false; // Ask whether to create screenshot or to select existing file if (_entryData->_video.ValidSelection()) { QMessageBox msgBox( QMessageBox::Question, obs_module_text("AdvSceneSwitcher.windowTitle"), obs_module_text( "AdvSceneSwitcher.condition.video.askFileAction"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); auto yes = msgBox.button(QMessageBox::StandardButton::Yes); yes->setText(obs_module_text( "AdvSceneSwitcher.condition.video.askFileAction.file")); auto no = msgBox.button(QMessageBox::StandardButton::No); no->setText(obs_module_text( "AdvSceneSwitcher.condition.video.askFileAction.screenshot")); msgBox.setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::CustomizeWindowHint); const auto result = msgBox.exec(); if (result == QMessageBox::Cancel) { return; } useExistingFile = result == QMessageBox::Yes; } if (useExistingFile) { path = QFileDialog::getOpenFileName( this, "", FileSelection::ValidPathOrDesktop( QString::fromStdString(_entryData->_file))); if (path.isEmpty()) { return; } } else { path = QFileDialog::getSaveFileName( this, "", FileSelection::ValidPathOrDesktop( QString::fromStdString(_entryData->_file)), "*.png"); if (path.isEmpty()) { return; } QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } auto screenshot = ScreenshotDialog::AskForScreenshot( _entryData->_video, _entryData->_areaParameters); if (!screenshot) { return; } screenshot->save(path); } _imagePath->SetPath(path); ImagePathChanged(path); } void MacroConditionVideoEdit::UsePatternForChangedCheckChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_patternMatchParameters.useForChangedCheck = value; _entryData->SetupTempVars(); SetWidgetVisibility(); } void MacroConditionVideoEdit::PatternThresholdChanged( const DoubleVariable &value) { GUARD_LOADING_AND_LOCK(); _entryData->_patternMatchParameters.threshold = value; _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); } void MacroConditionVideoEdit::ReduceLatencyChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_blockUntilScreenshotDone = value; } void MacroConditionVideoEdit::UseAlphaAsMaskChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_patternMatchParameters.useAlphaAsMask = value; _entryData->LoadImageFromFile(); _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); } void MacroConditionVideoEdit::PatternMatchModeChanged(int idx) { GUARD_LOADING_AND_LOCK(); _entryData->_patternMatchParameters.matchMode = static_cast( _patternMatchMode->itemData(idx).toInt()); _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); } void MacroConditionVideoEdit::ThrottleEnableChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_throttleEnabled = value; _throttleCount->setEnabled(value); } void MacroConditionVideoEdit::ThrottleCountChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_throttleCount = value / GetIntervalValue(); } void MacroConditionVideoEdit::KeepActiveChanged(int value) { GUARD_LOADING_AND_LOCK(); _entryData->_keepActive = value; } void MacroConditionVideoEdit::ShowMatchClicked() { _previewDialog.show(); _previewDialog.raise(); _previewDialog.activateWindow(); _previewDialog.ShowMatch(); } static bool needsShowMatch(VideoCondition cond) { return cond == VideoCondition::PATTERN || cond == VideoCondition::OBJECT_CASCADE || cond == VideoCondition::OCR; } static bool needsThrottleControls(VideoCondition cond) { return cond == VideoCondition::PATTERN || cond == VideoCondition::OBJECT_CASCADE || cond == VideoCondition::HAS_CHANGED || cond == VideoCondition::HAS_NOT_CHANGED; } static bool needsThreshold(VideoCondition cond) { return cond == VideoCondition::PATTERN || cond == VideoCondition::HAS_CHANGED || cond == VideoCondition::HAS_NOT_CHANGED; } static bool patternControlIsOptional(VideoCondition cond) { return cond == VideoCondition::HAS_CHANGED || cond == VideoCondition::HAS_NOT_CHANGED; } static bool needsAreaControls(VideoCondition cond) { return cond != VideoCondition::NO_IMAGE; } void MacroConditionVideoEdit::SetWidgetVisibility() { _sources->setVisible(_entryData->_video.type == VideoInput::Type::SOURCE); _scenes->setVisible(_entryData->_video.type == VideoInput::Type::SCENE); _imagePath->setVisible(requiresFileInput(_entryData->GetCondition())); _usePatternForChangedCheck->setVisible( patternControlIsOptional(_entryData->GetCondition())); _patternThreshold->setVisible( needsThreshold(_entryData->GetCondition())); _useAlphaAsMask->setVisible(_entryData->GetCondition() == VideoCondition::PATTERN); SetLayoutVisible(_patternMatchModeLayout, _entryData->GetCondition() == VideoCondition::PATTERN); _brightness->setVisible(_entryData->GetCondition() == VideoCondition::BRIGHTNESS); _showMatch->setVisible(needsShowMatch(_entryData->GetCondition())); _ocr->setVisible(_entryData->GetCondition() == VideoCondition::OCR); _cascadeClassifierEdit->setVisible(_entryData->GetCondition() == VideoCondition::OBJECT_CASCADE); _color->setVisible(_entryData->GetCondition() == VideoCondition::COLOR); SetLayoutVisible(_throttleControlLayout, needsThrottleControls(_entryData->GetCondition())); _area->setVisible(needsAreaControls(_entryData->GetCondition())); const bool sourceOrScene = _entryData->_video.type != VideoInput::Type::OBS_MAIN_OUTPUT; _keepActive->setVisible(sourceOrScene); _keepActiveHelp->setVisible(sourceOrScene); if (_entryData->GetCondition() == VideoCondition::HAS_CHANGED || _entryData->GetCondition() == VideoCondition::HAS_NOT_CHANGED) { _patternThreshold->setVisible( _entryData->_patternMatchParameters.useForChangedCheck); SetLayoutVisible( _patternMatchModeLayout, _entryData->_patternMatchParameters.useForChangedCheck); } // TODO: // Remove "reduce matching latency" and "reduce cpu load" options as they // have become superfluous with short circuit evaluation if (!_entryData->_throttleEnabled) { SetLayoutVisible(_throttleControlLayout, false); } if (_entryData->_blockUntilScreenshotDone) { _reduceLatency->hide(); } Resize(); } void MacroConditionVideoEdit::Resize() { adjustSize(); updateGeometry(); } void MacroConditionVideoEdit::SetupPreviewDialogParams() { _previewDialog.PatternMatchParametersChanged( _entryData->_patternMatchParameters); _previewDialog.CascadeClassifierParametersChanged( _entryData->_cascadeMatchParameters); _previewDialog.OCRParametersChanged(_entryData->_ocrParameters); _previewDialog.VideoSelectionChanged(_entryData->_video); _previewDialog.AreaParametersChanged(_entryData->_areaParameters); _previewDialog.ConditionChanged( static_cast(_entryData->GetCondition())); } void MacroConditionVideoEdit::UpdateEntryData() { if (!_entryData) { return; } _videoInputTypes->setCurrentIndex( static_cast(_entryData->_video.type)); _scenes->SetScene(_entryData->_video.scene); _sources->SetSource(_entryData->_video.source); _condition->setCurrentIndex(_condition->findData( static_cast(_entryData->GetCondition()))); _reduceLatency->setChecked(_entryData->_blockUntilScreenshotDone); _imagePath->SetPath(QString::fromStdString(_entryData->_file)); _usePatternForChangedCheck->setChecked( _entryData->_patternMatchParameters.useForChangedCheck); _patternThreshold->SetDoubleValue( _entryData->_patternMatchParameters.threshold); _useAlphaAsMask->setChecked( _entryData->_patternMatchParameters.useAlphaAsMask); _patternMatchMode->setCurrentIndex(_patternMatchMode->findData( _entryData->_patternMatchParameters.matchMode)); _throttleEnable->setChecked(_entryData->_throttleEnabled); _throttleCount->setValue(_entryData->_throttleCount * GetIntervalValue()); _keepActive->setChecked(_entryData->_keepActive); UpdatePreviewTooltip(); SetupPreviewDialogParams(); SetWidgetVisibility(); } } // namespace advss