Compare commits

...

11 Commits

Author SHA1 Message Date
WarmUpTill
3220809c04 Fix macro list displaying wrong contents after deleting group
This was only a visual issue - the correct macros were deleted in the
backend
2023-02-11 01:45:28 -08:00
WarmUpTill
72d5255c9e Fix typo 2023-02-11 01:45:28 -08:00
WarmUpTill
5995563a80 Add action to open projector 2023-02-04 08:34:20 -08:00
WarmUpTill
347743d93a Also trigger HighlightOnChange() if highlighting actions 2023-02-04 08:29:33 -08:00
WarmUpTill
3c067ce723 Rework preview dialog to use signals / slots to update image data
Previous implementation caused crashes on some setups
2023-02-03 13:36:51 -08:00
WarmUpTill
8dd2170dfa Update locale 2023-01-31 20:23:14 +01:00
WarmUpTill
5f3a3dd5cf Dynamically increase delay between screenshots up to one second
Otherwise users with a very low switcher interval might not be able to
use the preview dialog at all
2023-01-31 11:20:18 -08:00
WarmUpTill
8ad3563963 Always call obs_remove_tick_callback() in destructor
Seems like in some scenarios a screenshot thread would still be active
while the ScreenshotHelper object was deleted
2023-01-31 11:20:18 -08:00
WarmUpTill
492128ef86 Avoid deadlocks while opening settings window and calling frontend API 2023-01-31 11:20:18 -08:00
WarmUpTill
b4936274f2 Synchronize re-ordering and grouping of macros 2023-01-31 11:20:18 -08:00
WarmUpTill
64ccd5ba53 Detect invalid group setups and attempt to clean them up 2023-01-31 11:20:18 -08:00
20 changed files with 625 additions and 176 deletions

View File

@@ -113,6 +113,8 @@ target_sources(
src/macro-core/macro-action-plugin-state.hpp
src/macro-core/macro-action-profile.cpp
src/macro-core/macro-action-profile.hpp
src/macro-core/macro-action-projector.cpp
src/macro-core/macro-action-projector.hpp
src/macro-core/macro-action-random.cpp
src/macro-core/macro-action-random.hpp
src/macro-core/macro-action-recording.cpp

View File

@@ -17,7 +17,7 @@ AdvSceneSwitcher.generalTab.status.autoStart.never="Never"
AdvSceneSwitcher.generalTab.status.autoStart.recording="Recording"
AdvSceneSwitcher.generalTab.status.autoStart.streaming="Streaming"
AdvSceneSwitcher.generalTab.status.autoStart.recordingAndStreaming="Recording or Streaming"
AdvSceneSwitcher.generalTab.status.checkInterval="Check switch conditions every"
AdvSceneSwitcher.generalTab.status.checkInterval="Check conditions every"
AdvSceneSwitcher.generalTab.generalBehavior="General behavior"
AdvSceneSwitcher.generalTab.generalBehavior.onNoMet="If no actions are performed for"
AdvSceneSwitcher.generalTab.generalBehavior.onNoMetDelayTooltip="Will only ever be as accurate as the configured check interval."
@@ -594,6 +594,17 @@ AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
AdvSceneSwitcher.action.variable.entry="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}"
AdvSceneSwitcher.action.variable.entry.substringIndex="Substring start:{{subStringStart}} Substring size:{{subStringSize}}"
AdvSceneSwitcher.action.variable.entry.substringRegex="Assign value of{{regexMatchIdx}}match using regular expression:"
AdvSceneSwitcher.action.projector="Projector"
AdvSceneSwitcher.action.projector.type.source="Source"
AdvSceneSwitcher.action.projector.type.scene="Scene"
AdvSceneSwitcher.action.projector.type.preview="Preview"
AdvSceneSwitcher.action.projector.type.program="Program"
AdvSceneSwitcher.action.projector.type.multiview="Multiview"
AdvSceneSwitcher.action.projector.display="Display"
AdvSceneSwitcher.action.projector.windowed="Windowed"
AdvSceneSwitcher.action.projector.fullscreen="Fullscreen"
AdvSceneSwitcher.action.projector.entry="Open{{windowTypes}}projector of{{types}}{{scenes}}{{sources}}"
AdvSceneSwitcher.action.projector.entry.monitor="on{{monitors}}"
; Transition Tab

View File

@@ -244,6 +244,7 @@ void SwitcherData::Thread()
switcher->firstIntervalAfterStop = false;
}
mainLoopLock = nullptr;
blog(LOG_INFO, "stopped");
}

View File

@@ -653,7 +653,7 @@ void SwitcherData::saveGeneralSettings(obs_data_t *obj)
obs_data_set_int(obj, "threadPriority", threadPriority);
obs_data_set_bool(obj, "tansitionOverrideOverride",
obs_data_set_bool(obj, "transitionOverrideOverride",
transitionOverrideOverride);
obs_data_set_bool(obj, "adjustActiveTransitionType",
adjustActiveTransitionType);
@@ -735,8 +735,14 @@ void SwitcherData::loadGeneralSettings(obs_data_t *obj)
QThread::NormalPriority);
threadPriority = obs_data_get_int(obj, "threadPriority");
transitionOverrideOverride =
obs_data_get_bool(obj, "tansitionOverrideOverride");
// TODO: Remove this fallback in future version
if (obs_data_has_user_value(obj, "tansitionOverrideOverride")) {
transitionOverrideOverride =
obs_data_get_bool(obj, "tansitionOverrideOverride");
} else {
transitionOverrideOverride =
obs_data_get_bool(obj, "transitionOverrideOverride");
}
adjustActiveTransitionType =
obs_data_get_bool(obj, "adjustActiveTransitionType");

View File

@@ -0,0 +1,290 @@
#include "macro-action-projector.hpp"
#include "advanced-scene-switcher.hpp"
#include "utility.hpp"
#include <QScreen>
const std::string MacroActionProjector::id = "projector";
bool MacroActionProjector::_registered = MacroActionFactory::Register(
MacroActionProjector::id,
{MacroActionProjector::Create, MacroActionProjectorEdit::Create,
"AdvSceneSwitcher.action.projector"});
const static std::map<MacroActionProjector::Type, std::string> selectionTypes = {
{MacroActionProjector::Type::SOURCE,
"AdvSceneSwitcher.action.projector.type.source"},
{MacroActionProjector::Type::SCENE,
"AdvSceneSwitcher.action.projector.type.scene"},
{MacroActionProjector::Type::PREVIEW,
"AdvSceneSwitcher.action.projector.type.preview"},
{MacroActionProjector::Type::PROGRAM,
"AdvSceneSwitcher.action.projector.type.program"},
{MacroActionProjector::Type::MULTIVIEW,
"AdvSceneSwitcher.action.projector.type.multiview"},
};
bool MacroActionProjector::PerformAction()
{
std::string name = "";
const char *type = "";
switch (_type) {
case MacroActionProjector::Type::SOURCE:
type = "Source";
name = GetWeakSourceName(_source.GetSource());
if (name.empty()) {
return true;
}
break;
case MacroActionProjector::Type::SCENE:
type = "Scene";
name = GetWeakSourceName(_scene.GetScene());
if (name.empty()) {
return true;
}
break;
case MacroActionProjector::Type::PREVIEW:
type = "Preview";
break;
case MacroActionProjector::Type::PROGRAM:
type = "StudioProgram";
break;
case MacroActionProjector::Type::MULTIVIEW:
type = "Multiview";
break;
}
obs_frontend_open_projector(type, _fullscreen ? _monitor : -1, "",
name.c_str());
return true;
}
void MacroActionProjector::LogAction() const
{
auto it = selectionTypes.find(_type);
if (it != selectionTypes.end()) {
vblog(LOG_INFO,
"performed projector action \"%s\" with"
"source \"%s\","
"scene \"%s\","
"monitor %d",
it->second.c_str(), _source.ToString(true).c_str(),
_scene.ToString(true).c_str(), _monitor);
} else {
blog(LOG_WARNING, "ignored unknown projector action %d",
static_cast<int>(_type));
}
}
bool MacroActionProjector::Save(obs_data_t *obj) const
{
MacroAction::Save(obj);
obs_data_set_int(obj, "type", static_cast<int>(_type));
obs_data_set_int(obj, "monitor", _monitor);
obs_data_set_bool(obj, "fullscreen", _fullscreen);
_scene.Save(obj);
_source.Save(obj);
return true;
}
bool MacroActionProjector::Load(obs_data_t *obj)
{
MacroAction::Load(obj);
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
_monitor = obs_data_get_int(obj, "monitor");
_fullscreen = obs_data_get_bool(obj, "fullscreen");
_scene.Load(obj);
_source.Load(obj);
return true;
}
static inline void populateSelectionTypes(QComboBox *list)
{
for (auto entry : selectionTypes) {
list->addItem(obs_module_text(entry.second.c_str()));
}
}
static inline void populateWindowTypes(QComboBox *list)
{
list->addItem(
obs_module_text("AdvSceneSwitcher.action.projector.windowed"));
list->addItem(obs_module_text(
"AdvSceneSwitcher.action.projector.fullscreen"));
}
static QStringList getMonitorNames()
{
QStringList monitorNames;
QList<QScreen *> screens = QGuiApplication::screens();
for (int i = 0; i < screens.size(); i++) {
QScreen *screen = screens[i];
QRect screenGeometry = screen->geometry();
qreal ratio = screen->devicePixelRatio();
QString name = "";
#if defined(__APPLE__) || defined(_WIN32)
name = screen->name();
#else
name = screen->model().simplified();
if (name.length() > 1 && name.endsWith("-")) {
name.chop(1);
}
#endif
name = name.simplified();
if (name.length() == 0) {
name = QString("%1 %2")
.arg(obs_module_text(
"AdvSceneSwitcher.action.projector.display"))
.arg(QString::number(i + 1));
}
QString str =
QString("%1: %2x%3 @ %4,%5")
.arg(name,
QString::number(screenGeometry.width() *
ratio),
QString::number(screenGeometry.height() *
ratio),
QString::number(screenGeometry.x()),
QString::number(screenGeometry.y()));
monitorNames << str;
}
return monitorNames;
}
MacroActionProjectorEdit::MacroActionProjectorEdit(
QWidget *parent, std::shared_ptr<MacroActionProjector> entryData)
: QWidget(parent),
_windowTypes(new QComboBox()),
_types(new QComboBox()),
_scenes(new SceneSelectionWidget(window(), true, false, true, true,
true)),
_sources(new SourceSelectionWidget(window(), QStringList(), true)),
_monitorSelection(new QHBoxLayout()),
_monitors(new QComboBox())
{
populateWindowTypes(_windowTypes);
populateSelectionTypes(_types);
auto sources = GetSourceNames();
sources.sort();
_sources->SetSourceNameList(sources);
_monitors->addItems(getMonitorNames());
QWidget::connect(_windowTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(WindowTypeChanged(int)));
QWidget::connect(_types, SIGNAL(currentIndexChanged(int)), this,
SLOT(TypeChanged(int)));
QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
this, SLOT(SceneChanged(const SceneSelection &)));
QWidget::connect(_sources,
SIGNAL(SourceChanged(const SourceSelection &)), this,
SLOT(SourceChanged(const SourceSelection &)));
QWidget::connect(_monitors, SIGNAL(currentIndexChanged(int)), this,
SLOT(MonitorChanged(int)));
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{windowTypes}}", _windowTypes}, {"{{types}}", _types},
{"{{scenes}}", _scenes}, {"{{sources}}", _sources},
{"{{monitors}}", _monitors},
};
placeWidgets(obs_module_text(
"AdvSceneSwitcher.action.projector.entry.monitor"),
_monitorSelection, widgetPlaceholders);
QHBoxLayout *mainLayout = new QHBoxLayout;
placeWidgets(obs_module_text("AdvSceneSwitcher.action.projector.entry"),
mainLayout, widgetPlaceholders);
mainLayout->insertLayout(mainLayout->count() - 1, _monitorSelection);
setLayout(mainLayout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroActionProjectorEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_windowTypes->setCurrentIndex(_entryData->_fullscreen ? 1 : 0);
_types->setCurrentIndex(static_cast<int>(_entryData->_type));
_scenes->SetScene(_entryData->_scene);
_sources->SetSource(_entryData->_source);
_monitors->setCurrentIndex(_entryData->_monitor);
SetWidgetVisibility();
}
void MacroActionProjectorEdit::SceneChanged(const SceneSelection &s)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_scene = s;
}
void MacroActionProjectorEdit::SourceChanged(const SourceSelection &source)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_source = source;
}
void MacroActionProjectorEdit::MonitorChanged(int value)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_monitor = value;
}
void MacroActionProjectorEdit::WindowTypeChanged(int value)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_fullscreen =
_windowTypes->currentText() ==
obs_module_text("AdvSceneSwitcher.action.projector.fullscreen");
SetWidgetVisibility();
}
void MacroActionProjectorEdit::TypeChanged(int value)
{
if (_loading || !_entryData) {
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
_entryData->_type = static_cast<MacroActionProjector::Type>(value);
SetWidgetVisibility();
}
void MacroActionProjectorEdit::SetWidgetVisibility()
{
if (!_entryData) {
return;
}
_scenes->setVisible(_entryData->_type ==
MacroActionProjector::Type::SCENE);
_sources->setVisible(_entryData->_type ==
MacroActionProjector::Type::SOURCE);
setLayoutVisible(_monitorSelection, _entryData->_fullscreen);
adjustSize();
updateGeometry();
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include "macro-action-edit.hpp"
#include "scene-selection.hpp"
#include "source-selection.hpp"
class MacroActionProjector : public MacroAction {
public:
MacroActionProjector(Macro *m) : MacroAction(m) {}
bool PerformAction();
void LogAction() const;
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; };
static std::shared_ptr<MacroAction> Create(Macro *m)
{
return std::make_shared<MacroActionProjector>(m);
}
enum class Type {
SOURCE,
SCENE,
PREVIEW,
PROGRAM,
MULTIVIEW,
};
Type _type = Type::SCENE;
SourceSelection _source;
SceneSelection _scene;
int _monitor = 0;
bool _fullscreen = true;
private:
static bool _registered;
static const std::string id;
};
class MacroActionProjectorEdit : public QWidget {
Q_OBJECT
public:
MacroActionProjectorEdit(
QWidget *parent,
std::shared_ptr<MacroActionProjector> entryData = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroAction> action)
{
return new MacroActionProjectorEdit(
parent, std::dynamic_pointer_cast<MacroActionProjector>(
action));
}
private slots:
void WindowTypeChanged(int value);
void TypeChanged(int value);
void SceneChanged(const SceneSelection &);
void SourceChanged(const SourceSelection &);
void MonitorChanged(int value);
protected:
QComboBox *_windowTypes;
QComboBox *_types;
SceneSelectionWidget *_scenes;
SourceSelectionWidget *_sources;
QHBoxLayout *_monitorSelection;
QComboBox *_monitors;
std::shared_ptr<MacroActionProjector> _entryData;
private:
void SetWidgetVisibility();
bool _loading = true;
};

View File

@@ -106,25 +106,12 @@ bool MacroActionSwitchScene::WaitForTransition(OBSWeakSource &scene,
scene, transition, _duration.seconds);
switcher->abortMacroWait = false;
bool isInMainLoop = QThread::currentThread() == switcher->th;
if (isInMainLoop) {
if (expectedTransitionDuration < 0) {
waitForTransitionChange(transition, switcher->GetLock(),
GetMacro());
} else {
waitForTransitionChangeFixedDuration(
expectedTransitionDuration, switcher->GetLock(),
GetMacro());
}
std::unique_lock<std::mutex> lock(switcher->m);
if (expectedTransitionDuration < 0) {
waitForTransitionChange(transition, &lock, GetMacro());
} else {
std::mutex temp;
std::unique_lock<std::mutex> lock(temp);
if (expectedTransitionDuration < 0) {
waitForTransitionChange(transition, &lock, GetMacro());
} else {
waitForTransitionChangeFixedDuration(
expectedTransitionDuration, &lock, GetMacro());
}
waitForTransitionChangeFixedDuration(expectedTransitionDuration,
&lock, GetMacro());
}
return !switcher->abortMacroWait;

View File

@@ -52,14 +52,8 @@ bool MacroActionWait::PerformAction()
std::chrono::milliseconds((int)(sleepDuration * 1000));
switcher->abortMacroWait = false;
bool isInMainLoop = QThread::currentThread() == switcher->th;
if (isInMainLoop) {
waitHelper(switcher->GetLock(), GetMacro(), time);
} else {
std::mutex temp;
std::unique_lock<std::mutex> lock(temp);
waitHelper(&lock, GetMacro(), time);
}
std::unique_lock<std::mutex> lock(switcher->m);
waitHelper(&lock, GetMacro(), time);
return !switcher->abortMacroWait;
}

View File

@@ -335,7 +335,8 @@ void AdvSceneSwitcher::MacroSelectionChanged(const QItemSelection &,
void AdvSceneSwitcher::HighlightOnChange()
{
if (!switcher->macroProperties._highlightExecuted) {
if (!switcher->macroProperties._highlightActions &&
!switcher->macroProperties._highlightExecuted) {
return;
}

View File

@@ -1,6 +1,7 @@
#include "macro-tree.hpp"
#include "macro.hpp"
#include "utility.hpp"
#include "switcher-data-structs.hpp"
#include <obs.h>
#include <string>
@@ -374,24 +375,24 @@ void MacroTreeModel::Add(std::shared_ptr<Macro> item)
void MacroTreeModel::Remove(std::shared_ptr<Macro> item)
{
auto startIdx = GetItemModelIndex(item);
if (startIdx == -1) {
auto uiStartIdx = GetItemModelIndex(item);
if (uiStartIdx == -1) {
return;
}
auto macroStartIdx = ModelIndexToMacroIndex(startIdx, _macros);
auto macroStartIdx = ModelIndexToMacroIndex(uiStartIdx, _macros);
auto endIdx = startIdx;
auto uiEndIdx = uiStartIdx;
auto macroEndIdx = macroStartIdx;
bool isGroup = item->IsGroup();
if (isGroup) {
macroEndIdx += item->GroupSize();
if (!item->IsCollapsed()) {
endIdx = item->GroupSize();
uiEndIdx += item->GroupSize();
}
}
beginRemoveRows(QModelIndex(), startIdx, endIdx);
beginRemoveRows(QModelIndex(), uiStartIdx, uiEndIdx);
_macros.erase(std::next(_macros.begin(), macroStartIdx),
std::next(_macros.begin(), macroEndIdx + 1));
endRemoveRows();
@@ -563,6 +564,7 @@ void MacroTreeModel::GroupSelectedItems(QModelIndexList &indices)
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
QString name = GetNewGroupName();
std::vector<std::shared_ptr<Macro>> items;
items.reserve(indices.size());
@@ -606,6 +608,7 @@ void MacroTreeModel::UngroupSelectedGroups(QModelIndexList &indices)
return;
}
std::lock_guard<std::mutex> lock(switcher->m);
for (int i = indices.count() - 1; i >= 0; i--) {
std::shared_ptr<Macro> item = _macros[ModelIndexToMacroIndex(
indices[i].row(), _macros)];
@@ -966,6 +969,7 @@ void MacroTree::dropEvent(QDropEvent *event)
}
// Move items in backend
std::lock_guard<std::mutex> lock(switcher->m);
int to = row;
try {
to = ModelIndexToMacroIndex(row, items);

View File

@@ -34,7 +34,7 @@ Macro::CreateGroup(const std::string &name,
{
auto group = std::make_shared<Macro>(name, false);
for (auto &c : children) {
c->SetParent(group.get());
c->SetParent(group);
}
group->_isGroup = true;
group->_groupSize = children.size();
@@ -82,7 +82,7 @@ void Macro::PrepareMoveToGroup(std::shared_ptr<Macro> group,
oldGroup->_groupSize--;
}
item->SetParent(group.get());
item->SetParent(group);
if (group) {
group->_groupSize++;
}
@@ -204,8 +204,9 @@ bool Macro::PerformActions(bool forceParallel, bool ignorePause)
RunActions(ret, ignorePause);
}
_wasExecutedRecently = true;
if (_parent) {
_parent->_wasExecutedRecently = true;
auto group = _parent.lock();
if (group) {
group->_wasExecutedRecently = true;
}
return ret;
}
@@ -313,6 +314,12 @@ void Macro::UpdateConditionIndices()
}
}
Macro *Macro::Parent()
{
auto p = _parent.lock();
return p.get();
}
bool Macro::Save(obs_data_t *obj) const
{
obs_data_set_string(obj, "name", _name.c_str());
@@ -709,18 +716,41 @@ void SwitcherData::loadMacros(obs_data_t *obj)
obs_data_array_release(macroArray);
int groupCount = 0;
Macro *group = nullptr;
std::shared_ptr<Macro> group;
std::vector<std::shared_ptr<Macro>> invalidGroups;
for (auto &m : macros) {
if (groupCount && m->IsGroup()) {
blog(LOG_ERROR,
"nested group detected - will delete \"%s\"",
m->Name().c_str());
invalidGroups.emplace_back(m);
continue;
}
if (groupCount) {
m->SetParent(group);
groupCount--;
}
if (m->IsGroup()) {
groupCount = m->GroupSize();
group = m.get();
group = m;
}
m->PostLoad();
}
if (groupCount) {
blog(LOG_ERROR,
"invalid group size detected - will delete \"%s\"",
group->Name().c_str());
invalidGroups.emplace_back(group);
}
for (auto &m : invalidGroups) {
auto it = std::find(macros.begin(), macros.end(), m);
if (it == macros.end()) {
continue;
}
macros.erase(it);
}
}
bool SwitcherData::checkMacros()
@@ -741,8 +771,28 @@ bool SwitcherData::checkMacros()
bool SwitcherData::runMacros()
{
for (auto m : macros) {
if (m->Matched()) {
// Create copy of macor list as elements might be removed, inserted, or
// reordered while macros are currently being executed.
// For example, this can happen if a macro is performing a wait action,
// as the main lock will be unlocked during this time.
auto runPhaseMacros = macros;
// Avoid deadlocks when opening settings window and calling frontend
// API functions at the same time.
//
// If the timing is just right, the frontend API call will call
// QMetaObject::invokeMethod(...) with Qt::BlockingQueuedConnection
// while holding the main switcher mutex.
// But this invokeMethod call itself will be blocked as it is waiting
// the constructor of AdvSceneSwitcher() to complete.
// The constructor of AdvSceneSwitcher() cannot continue however as it
// cannot lock the main switcher mutex.
if (GetLock()) {
GetLock()->unlock();
}
for (auto &m : runPhaseMacros) {
if (m && m->Matched()) {
vblog(LOG_INFO, "running macro: %s", m->Name().c_str());
if (!m->PerformActions()) {
blog(LOG_WARNING, "abort macro: %s",
@@ -750,6 +800,9 @@ bool SwitcherData::runMacros()
}
}
}
if (GetLock()) {
GetLock()->lock();
}
return true;
}

View File

@@ -55,11 +55,11 @@ public:
std::shared_ptr<Macro> item);
bool IsGroup() { return _isGroup; }
uint32_t GroupSize() { return _groupSize; }
bool IsSubitem() { return !!_parent; }
bool IsSubitem() { return !_parent.expired(); }
void SetCollapsed(bool val) { _isCollapsed = val; }
bool IsCollapsed() { return _isCollapsed; }
void SetParent(Macro *m) { _parent = m; }
Macro *Parent() { return _parent; }
void SetParent(std::shared_ptr<Macro> m) { _parent = m; }
Macro *Parent();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
@@ -93,7 +93,7 @@ private:
std::deque<std::shared_ptr<MacroCondition>> _conditions;
std::deque<std::shared_ptr<MacroAction>> _actions;
Macro *_parent = nullptr;
std::weak_ptr<Macro> _parent;
uint32_t _groupSize = 0;
bool _runInParallel = false;

View File

@@ -342,7 +342,7 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
_throttleCount(new QSpinBox()),
_showMatch(new QPushButton(obs_module_text(
"AdvSceneSwitcher.condition.video.showMatch"))),
_previewDialog(this, GetSwitcher()->interval)
_previewDialog(this)
{
_reduceLatency->setToolTip(obs_module_text(
"AdvSceneSwitcher.condition.video.reduceLatency.tooltip"));

View File

@@ -18,8 +18,8 @@ PatternImageData createPatternData(QImage &pattern)
return data;
}
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
cv::Mat &result, bool useAlphaAsMask)
void matchPattern(QImage &img, const PatternImageData &patternData,
double threshold, cv::Mat &result, bool useAlphaAsMask)
{
if (img.isNull() || patternData.rgbaPattern.empty()) {
return;

View File

@@ -14,8 +14,9 @@ struct PatternImageData {
};
PatternImageData createPatternData(QImage &pattern);
void matchPattern(QImage &img, PatternImageData &patternData, double threshold,
cv::Mat &result, bool useAlphaAsMask = true);
void matchPattern(QImage &img, const PatternImageData &patternData,
double threshold, cv::Mat &result,
bool useAlphaAsMask = true);
void matchPattern(QImage &img, QImage &pattern, double threshold,
cv::Mat &result, bool useAlphaAsMask);
std::vector<cv::Rect> matchObject(QImage &img, cv::CascadeClassifier &cascade,

View File

@@ -5,12 +5,11 @@
#include <screenshot-helper.hpp>
PreviewDialog::PreviewDialog(QWidget *parent, int delay)
PreviewDialog::PreviewDialog(QWidget *parent)
: QDialog(parent),
_scrollArea(new QScrollArea),
_imageLabel(new QLabel(this)),
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this)),
_delay(delay)
_rubberBand(new QRubberBand(QRubberBand::Rectangle, this))
{
setWindowTitle("Advanced Scene Switcher");
setWindowFlags(windowFlags() | Qt::WindowMaximizeButtonHint |
@@ -39,22 +38,12 @@ PreviewDialog::PreviewDialog(QWidget *parent, int delay)
layout->addWidget(_statusLabel);
layout->addWidget(_scrollArea);
setLayout(layout);
// This is a workaround to handle random segfaults triggered when using:
// QMetaObject::invokeMethod(this, "RedrawImage", Qt::QueuedConnection,
// Q_ARG(QImage, image));
// from within CheckForMatchLoop().
// Even using BlockingQueuedConnection causes deadlocks
_timer.setInterval(300);
QWidget::connect(&_timer, &QTimer::timeout, this,
&PreviewDialog::ResizeImageLabel);
_timer.start();
}
void PreviewDialog::mousePressEvent(QMouseEvent *event)
{
_selectingArea = true;
if (_type == Type::SHOW_MATCH) {
if (_type == PreviewType::SHOW_MATCH) {
return;
}
_origin = event->pos();
@@ -64,7 +53,7 @@ void PreviewDialog::mousePressEvent(QMouseEvent *event)
void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
{
if (_type == Type::SHOW_MATCH) {
if (_type == PreviewType::SHOW_MATCH) {
return;
}
_rubberBand->setGeometry(QRect(_origin, event->pos()).normalized());
@@ -72,7 +61,7 @@ void PreviewDialog::mouseMoveEvent(QMouseEvent *event)
void PreviewDialog::mouseReleaseEvent(QMouseEvent *)
{
if (_type == Type::SHOW_MATCH) {
if (_type == PreviewType::SHOW_MATCH) {
return;
}
auto selectionStart =
@@ -102,14 +91,14 @@ void PreviewDialog::ShowMatch()
{
Start();
_rubberBand->hide();
_type = Type::SHOW_MATCH;
_type = PreviewType::SHOW_MATCH;
}
void PreviewDialog::SelectArea()
{
_selectingArea = false;
Start();
_type = Type::SELECT_AREA;
_type = PreviewType::SELECT_AREA;
DrawFrame();
_statusLabel->setText(obs_module_text(
"AdvSceneSwitcher.condition.video.selectArea.status"));
@@ -117,11 +106,8 @@ void PreviewDialog::SelectArea()
void PreviewDialog::Stop()
{
_stop = true;
_cv.notify_all();
if (_thread.joinable()) {
_thread.join();
}
_thread.quit();
_thread.wait();
}
void PreviewDialog::closeEvent(QCloseEvent *event)
@@ -164,68 +150,66 @@ void PreviewDialog::ConditionChanged(int cond)
_condition = static_cast<VideoCondition>(cond);
}
void PreviewDialog::ResizeImageLabel()
void PreviewDialog::UpdateStatus(const QString &status)
{
_statusLabel->setText(status);
}
void PreviewDialog::UpdateImage(const QPixmap &image)
{
_imageLabel->setPixmap(image);
_imageLabel->adjustSize();
if (_type == Type::SELECT_AREA && !_selectingArea) {
if (_type == PreviewType::SELECT_AREA && !_selectingArea) {
DrawFrame();
}
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
_objDetectParams, _areaParams, _condition);
}
void PreviewDialog::Start()
{
if (_thread.joinable()) {
return;
}
if (!_video.ValidSelection()) {
DisplayMessage(obs_module_text(
"AdvSceneSwitcher.condition.video.screenshotFail"));
close();
return;
}
_stop = false;
_thread = std::thread(&PreviewDialog::CheckForMatchLoop, this);
}
void PreviewDialog::CheckForMatchLoop()
{
while (!_stop) {
std::unique_lock<std::mutex> lock(_mtx);
auto source = obs_weak_source_get_source(_video.GetVideo());
ScreenshotHelper screenshot(source);
obs_source_release(source);
_cv.wait_for(lock, std::chrono::milliseconds(_delay));
if (_stop || isHidden()) {
return;
}
if (!screenshot.done || !_video.ValidSelection()) {
_statusLabel->setText(obs_module_text(
"AdvSceneSwitcher.condition.video.screenshotFail"));
_imageLabel->setPixmap(QPixmap());
continue;
}
if (screenshot.image.width() == 0 ||
screenshot.image.height() == 0) {
_statusLabel->setText(obs_module_text(
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
_imageLabel->setPixmap(QPixmap());
continue;
}
if (_type == Type::SHOW_MATCH) {
if (_areaParams.enable) {
screenshot.image = screenshot.image.copy(
_areaParams.area.x, _areaParams.area.y,
_areaParams.area.width,
_areaParams.area.height);
}
MarkMatch(screenshot.image);
}
_imageLabel->setPixmap(QPixmap::fromImage(screenshot.image));
if (_thread.isRunning()) {
return;
}
PreviewImage *worker = new PreviewImage();
worker->moveToThread(&_thread);
connect(&_thread, &QThread::finished, worker, &QObject::deleteLater);
connect(worker, &PreviewImage::ImageReady, this,
&PreviewDialog::UpdateImage);
connect(worker, &PreviewImage::StatusUpdate, this,
&PreviewDialog::UpdateStatus);
connect(this, &PreviewDialog::NeedImage, worker,
&PreviewImage::CreateImage);
_thread.start();
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
_objDetectParams, _areaParams, _condition);
}
void markPatterns(cv::Mat &matchResult, QImage &image, cv::Mat &pattern)
void PreviewDialog::DrawFrame()
{
if (!_video.ValidSelection()) {
return;
}
auto imageStart =
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
auto windowStart = mapToGlobal(rect().topLeft());
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
_rubberBand->move(
_areaParams.area.x + (imageStart.x() - windowStart.x()),
_areaParams.area.y + (imageStart.y() - windowStart.y()));
_rubberBand->show();
}
void markPatterns(cv::Mat &matchResult, QImage &image, const cv::Mat &pattern)
{
auto matchImg = QImageToMat(image);
for (int row = 0; row < matchResult.rows - 1; row++) {
@@ -251,50 +235,79 @@ void markObjects(QImage &image, std::vector<cv::Rect> &objects)
}
}
void PreviewDialog::MarkMatch(QImage &screenshot)
void PreviewImage::CreateImage(const VideoInput &video, PreviewType type,
const PatternMatchParameters &patternMatchParams,
const PatternImageData &patternImageData,
ObjDetectParamerts objDetectParams,
const AreaParamters &areaParams,
VideoCondition condition)
{
if (_condition == VideoCondition::PATTERN) {
auto source = obs_weak_source_get_source(video.GetVideo());
ScreenshotHelper screenshot(source, true);
obs_source_release(source);
if (!video.ValidSelection() || !screenshot.done) {
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.screenshotFail"));
emit ImageReady(QPixmap());
return;
}
if (screenshot.image.width() == 0 || screenshot.image.height() == 0) {
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.screenshotEmpty"));
emit ImageReady(QPixmap());
return;
}
if (type == PreviewType::SHOW_MATCH) {
if (areaParams.enable) {
screenshot.image = screenshot.image.copy(
areaParams.area.x, areaParams.area.y,
areaParams.area.width, areaParams.area.height);
}
// Will emit status label update
MarkMatch(screenshot.image, patternMatchParams,
patternImageData, objDetectParams, condition);
} else {
emit StatusUpdate("");
}
emit ImageReady(QPixmap::fromImage(screenshot.image));
}
void PreviewImage::MarkMatch(QImage &screenshot,
const PatternMatchParameters &patternMatchParams,
const PatternImageData &patternImageData,
ObjDetectParamerts &objDetectParams,
VideoCondition condition)
{
if (condition == VideoCondition::PATTERN) {
cv::Mat result;
matchPattern(screenshot, _patternImageData,
_patternMatchParams.threshold, result,
_patternMatchParams.useAlphaAsMask);
matchPattern(screenshot, patternImageData,
patternMatchParams.threshold, result,
patternMatchParams.useAlphaAsMask);
if (countNonZero(result) == 0) {
_statusLabel->setText(obs_module_text(
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.patternMatchFail"));
} else {
_statusLabel->setText(obs_module_text(
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.patternMatchSuccess"));
markPatterns(result, screenshot,
_patternImageData.rgbaPattern);
patternImageData.rgbaPattern);
}
} else if (_condition == VideoCondition::OBJECT) {
auto objects = matchObject(screenshot, _objDetectParams.cascade,
_objDetectParams.scaleFactor,
_objDetectParams.minNeighbors,
_objDetectParams.minSize.CV(),
_objDetectParams.maxSize.CV());
} else if (condition == VideoCondition::OBJECT) {
auto objects = matchObject(screenshot, objDetectParams.cascade,
objDetectParams.scaleFactor,
objDetectParams.minNeighbors,
objDetectParams.minSize.CV(),
objDetectParams.maxSize.CV());
if (objects.empty()) {
_statusLabel->setText(obs_module_text(
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.objectMatchFail"));
} else {
_statusLabel->setText(obs_module_text(
emit StatusUpdate(obs_module_text(
"AdvSceneSwitcher.condition.video.objectMatchSuccess"));
markObjects(screenshot, objects);
}
}
}
void PreviewDialog::DrawFrame()
{
if (!_video.ValidSelection()) {
return;
}
auto imageStart =
_imageLabel->mapToGlobal(_imageLabel->rect().topLeft());
auto windowStart = mapToGlobal(rect().topLeft());
_rubberBand->resize(_areaParams.area.width, _areaParams.area.height);
_rubberBand->move(
_areaParams.area.x + (imageStart.x() - windowStart.x()),
_areaParams.area.y + (imageStart.y() - windowStart.y()));
_rubberBand->show();
}

View File

@@ -4,21 +4,40 @@
#include <QDialog>
#include <QLabel>
#include <QScrollArea>
#include <QTimer>
#include <QThread>
#include <QMouseEvent>
#include <QRubberBand>
#include <QPoint>
#include <thread>
#include <mutex>
#include <condition_variable>
enum class PreviewType {
SHOW_MATCH,
SELECT_AREA,
};
class PreviewImage : public QObject {
Q_OBJECT
public slots:
void CreateImage(const VideoInput &, PreviewType,
const PatternMatchParameters &,
const PatternImageData &, ObjDetectParamerts,
const AreaParamters &, VideoCondition);
signals:
void ImageReady(const QPixmap &);
void StatusUpdate(const QString &);
private:
void MarkMatch(QImage &screenshot, const PatternMatchParameters &,
const PatternImageData &, ObjDetectParamerts &,
VideoCondition);
};
class PreviewDialog : public QDialog {
Q_OBJECT
public:
PreviewDialog(QWidget *parent, int delay);
PreviewDialog(QWidget *parent);
virtual ~PreviewDialog();
void ShowMatch();
void SelectArea();
@@ -32,14 +51,17 @@ public slots:
void AreaParamtersChanged(const AreaParamters &);
void ConditionChanged(int cond);
private slots:
void ResizeImageLabel();
void UpdateImage(const QPixmap &);
void UpdateStatus(const QString &);
signals:
void SelectionAreaChanged(QRect area);
void NeedImage(const VideoInput &, PreviewType,
const PatternMatchParameters &, const PatternImageData &,
ObjDetectParamerts, const AreaParamters &,
VideoCondition);
private:
void Start();
void CheckForMatchLoop();
void MarkMatch(QImage &screenshot);
void DrawFrame();
void mousePressEvent(QMouseEvent *event);
@@ -56,21 +78,13 @@ private:
QScrollArea *_scrollArea;
QLabel *_statusLabel;
QLabel *_imageLabel;
QTimer _timer;
QPoint _origin;
QRubberBand *_rubberBand = nullptr;
std::atomic_bool _selectingArea = {false};
std::mutex _mtx;
std::condition_variable _cv;
std::thread _thread;
std::atomic_bool _stop = {true};
PreviewType _type = PreviewType::SHOW_MATCH;
enum class Type {
SHOW_MATCH,
SELECT_AREA,
};
Type _type = Type::SHOW_MATCH;
int _delay = 300;
std::mutex _mtx;
QThread _thread;
};

View File

@@ -90,7 +90,7 @@ struct SwitcherData {
std::condition_variable cv;
std::mutex m;
std::unique_lock<std::mutex> *mainLoopLock;
std::unique_lock<std::mutex> *mainLoopLock = nullptr;
bool transitionActive = false;
bool waitForTransition = false;

View File

@@ -39,9 +39,8 @@ ScreenshotHelper::~ScreenshotHelper()
gs_stagesurface_destroy(stagesurf);
gs_texrender_destroy(texrender);
obs_leave_graphics();
obs_remove_tick_callback(ScreenshotTick, this);
}
obs_remove_tick_callback(ScreenshotTick, this);
if (_saveThread.joinable()) {
_saveThread.join();
}

View File

@@ -36,7 +36,7 @@ public:
std::chrono::high_resolution_clock::time_point time;
private:
bool _initDone = false;
std::atomic_bool _initDone = false;
bool _blocking = false;
std::thread _saveThread;
bool _saveToFile = false;