mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-12 12:18:35 -05:00
Add "Macro Schedule" tab
This commit is contained in:
489
plugins/schedule/macro-schedule.cpp
Normal file
489
plugins/schedule/macro-schedule.cpp
Normal file
@@ -0,0 +1,489 @@
|
||||
#include "macro-schedule.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-export-extensions.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <QUuid>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<MacroScheduleEntry> scheduleEntries;
|
||||
|
||||
static std::thread schedulerThread;
|
||||
static std::atomic<bool> schedulerRunning{false};
|
||||
static std::mutex schedulerWaitMutex;
|
||||
static std::condition_variable schedulerWaitCV;
|
||||
|
||||
static void initScheduler();
|
||||
static void cleanupScheduler();
|
||||
static void saveEntries(obs_data_t *obj);
|
||||
static void loadEntries(obs_data_t *obj);
|
||||
static bool setup();
|
||||
static bool setupDone = setup();
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddSaveStep(saveEntries);
|
||||
AddLoadStep(loadEntries);
|
||||
AddStartStep(initScheduler);
|
||||
AddStopStep(cleanupScheduler);
|
||||
AddPluginCleanupStep([]() {
|
||||
cleanupScheduler();
|
||||
scheduleEntries.clear();
|
||||
});
|
||||
AddMacroExportExtension(
|
||||
{"AdvSceneSwitcher.macroTab.export.macroScheduleEntries",
|
||||
"macroScheduleEntries",
|
||||
[](obs_data_t *data, const QStringList &selectedIds) {
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_array_create();
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
if (!selectedIds.isEmpty() &&
|
||||
!selectedIds.contains(
|
||||
QString::fromStdString(entry.id)))
|
||||
continue;
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
entry.Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(data, "macroScheduleEntries",
|
||||
array);
|
||||
},
|
||||
[](obs_data_t *data, const QStringList &) {
|
||||
OBSDataArrayAutoRelease array = obs_data_get_array(
|
||||
data, "macroScheduleEntries");
|
||||
if (!array) {
|
||||
return;
|
||||
}
|
||||
const size_t count = obs_data_array_count(array);
|
||||
auto lock = LockContext();
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item =
|
||||
obs_data_array_item(array, i);
|
||||
MacroScheduleEntry entry;
|
||||
entry.Load(item);
|
||||
// Skip if an entry with this id already exists.
|
||||
const auto &id = entry.id;
|
||||
const bool exists = std::any_of(
|
||||
scheduleEntries.begin(),
|
||||
scheduleEntries.end(),
|
||||
[&id](const MacroScheduleEntry &e) {
|
||||
return e.id == id;
|
||||
});
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
// Reset runtime state so the entry fires fresh.
|
||||
entry.timesTriggered = 0;
|
||||
entry.lastTriggered = QDateTime();
|
||||
entry.endDateActionApplied = false;
|
||||
scheduleEntries.emplace_back(std::move(entry));
|
||||
}
|
||||
},
|
||||
[]() -> QList<QPair<QString, QString>> {
|
||||
QList<QPair<QString, QString>> items;
|
||||
auto lock = LockContext();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
items.append({QString::fromStdString(entry.id),
|
||||
entry.GetSummary()});
|
||||
}
|
||||
return items;
|
||||
}});
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacroScheduleEntry::MacroScheduleEntry()
|
||||
: id(QUuid::createUuid().toString(QUuid::WithoutBraces).toStdString()),
|
||||
startDateTime(QDateTime::currentDateTime())
|
||||
{
|
||||
}
|
||||
|
||||
static void saveDateTime(obs_data_t *obj, const char *key, const QDateTime &dt)
|
||||
{
|
||||
if (dt.isValid()) {
|
||||
obs_data_set_string(
|
||||
obj, key,
|
||||
dt.toString(Qt::ISODate).toUtf8().constData());
|
||||
}
|
||||
}
|
||||
|
||||
static QDateTime loadDateTime(obs_data_t *obj, const char *key)
|
||||
{
|
||||
const char *str = obs_data_get_string(obj, key);
|
||||
if (!str || !*str) {
|
||||
return QDateTime();
|
||||
}
|
||||
return QDateTime::fromString(QString::fromUtf8(str), Qt::ISODate);
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::Save(obs_data_t *obj) const
|
||||
{
|
||||
obs_data_set_string(obj, "id", id.c_str());
|
||||
obs_data_set_string(obj, "name", name.c_str());
|
||||
macro.Save(obj);
|
||||
obs_data_set_string(obj, "color", color.name().toUtf8().constData());
|
||||
obs_data_set_bool(obj, "checkConditions", checkConditions);
|
||||
obs_data_set_bool(obj, "runElseActionsOnConditionFailure",
|
||||
runElseActionsOnConditionFailure);
|
||||
obs_data_set_bool(obj, "enabled", enabled);
|
||||
|
||||
saveDateTime(obj, "startDateTime", startDateTime);
|
||||
obs_data_set_bool(obj, "hasEndDate", hasEndDate);
|
||||
if (hasEndDate) {
|
||||
saveDateTime(obj, "endDate", endDate);
|
||||
obs_data_set_int(obj, "endDateAction",
|
||||
static_cast<int>(endDateAction));
|
||||
}
|
||||
|
||||
obs_data_set_bool(obj, "doesRepeat", doesRepeat);
|
||||
repeatInterval.Save(obj, "repeatInterval");
|
||||
obs_data_set_int(obj, "repeatEndType", static_cast<int>(repeatEndType));
|
||||
obs_data_set_int(obj, "repeatMaxCount", repeatMaxCount);
|
||||
if (repeatEndType == RepeatEndType::UNTIL_DATE &&
|
||||
repeatUntilDate.isValid()) {
|
||||
saveDateTime(obj, "repeatUntilDate", repeatUntilDate);
|
||||
}
|
||||
|
||||
obs_data_set_int(obj, "timesTriggered", timesTriggered);
|
||||
saveDateTime(obj, "lastTriggered", lastTriggered);
|
||||
obs_data_set_bool(obj, "endDateActionApplied", endDateActionApplied);
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::Load(obs_data_t *obj)
|
||||
{
|
||||
const char *idStr = obs_data_get_string(obj, "id");
|
||||
if (idStr && *idStr) {
|
||||
id = idStr;
|
||||
}
|
||||
const char *nameStr = obs_data_get_string(obj, "name");
|
||||
if (nameStr) {
|
||||
name = nameStr;
|
||||
}
|
||||
macro.Load(obj);
|
||||
const char *colorStr = obs_data_get_string(obj, "color");
|
||||
if (colorStr && *colorStr) {
|
||||
const QColor loaded(QString::fromUtf8(colorStr));
|
||||
if (loaded.isValid()) {
|
||||
color = loaded;
|
||||
}
|
||||
}
|
||||
checkConditions = obs_data_get_bool(obj, "checkConditions");
|
||||
runElseActionsOnConditionFailure =
|
||||
obs_data_get_bool(obj, "runElseActionsOnConditionFailure");
|
||||
obs_data_set_default_bool(obj, "enabled", true);
|
||||
enabled = obs_data_get_bool(obj, "enabled");
|
||||
|
||||
startDateTime = loadDateTime(obj, "startDateTime");
|
||||
hasEndDate = obs_data_get_bool(obj, "hasEndDate");
|
||||
if (hasEndDate) {
|
||||
endDate = loadDateTime(obj, "endDate");
|
||||
endDateAction = static_cast<EndDateAction>(
|
||||
obs_data_get_int(obj, "endDateAction"));
|
||||
}
|
||||
|
||||
doesRepeat = obs_data_get_bool(obj, "doesRepeat");
|
||||
repeatInterval.Load(obj, "repeatInterval");
|
||||
repeatEndType = static_cast<RepeatEndType>(
|
||||
obs_data_get_int(obj, "repeatEndType"));
|
||||
obs_data_set_default_int(obj, "repeatMaxCount", 1);
|
||||
repeatMaxCount = (int)obs_data_get_int(obj, "repeatMaxCount");
|
||||
repeatUntilDate = loadDateTime(obj, "repeatUntilDate");
|
||||
|
||||
timesTriggered = (int)obs_data_get_int(obj, "timesTriggered");
|
||||
lastTriggered = loadDateTime(obj, "lastTriggered");
|
||||
endDateActionApplied = obs_data_get_bool(obj, "endDateActionApplied");
|
||||
}
|
||||
|
||||
QDateTime MacroScheduleEntry::advanceFrom(const QDateTime &base) const
|
||||
{
|
||||
const qint64 ms = qRound64(repeatInterval.Seconds() * 1000.0);
|
||||
if (ms <= 0) {
|
||||
return QDateTime();
|
||||
}
|
||||
return base.addMSecs(ms);
|
||||
}
|
||||
|
||||
QDateTime MacroScheduleEntry::NextTriggerTime() const
|
||||
{
|
||||
if (!lastTriggered.isValid()) {
|
||||
return startDateTime;
|
||||
}
|
||||
if (!doesRepeat) {
|
||||
// If the start was moved beyond the last trigger, treat as not yet fired.
|
||||
if (startDateTime > lastTriggered) {
|
||||
return startDateTime;
|
||||
}
|
||||
return QDateTime(); // one-shot already triggered
|
||||
}
|
||||
|
||||
QDateTime next = startDateTime;
|
||||
while (next <= lastTriggered) {
|
||||
QDateTime candidate = advanceFrom(next);
|
||||
if (!candidate.isValid()) {
|
||||
return QDateTime();
|
||||
}
|
||||
next = candidate;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
bool MacroScheduleEntry::IsExpired() const
|
||||
{
|
||||
if (!doesRepeat && lastTriggered.isValid()) {
|
||||
return lastTriggered >= startDateTime;
|
||||
}
|
||||
switch (repeatEndType) {
|
||||
case RepeatEndType::NEVER:
|
||||
return false;
|
||||
case RepeatEndType::AFTER_N_TIMES:
|
||||
return timesTriggered >= repeatMaxCount;
|
||||
case RepeatEndType::UNTIL_DATE:
|
||||
return repeatUntilDate.isValid() &&
|
||||
QDateTime::currentDateTime() > repeatUntilDate;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroScheduleEntry::ShouldTrigger(const QDateTime &now) const
|
||||
{
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
if (IsExpired()) {
|
||||
return false;
|
||||
}
|
||||
if (!startDateTime.isValid()) {
|
||||
return false;
|
||||
}
|
||||
const QDateTime next = NextTriggerTime();
|
||||
if (!next.isValid()) {
|
||||
return false;
|
||||
}
|
||||
return next <= now;
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::MarkTriggered(const QDateTime &now)
|
||||
{
|
||||
++timesTriggered;
|
||||
lastTriggered = now;
|
||||
}
|
||||
|
||||
void MacroScheduleEntry::FastForwardTo(const QDateTime &now)
|
||||
{
|
||||
timesTriggered = 0;
|
||||
lastTriggered = QDateTime();
|
||||
|
||||
if (!startDateTime.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doesRepeat) {
|
||||
// One-shot: count it as triggered if the start lies in the past.
|
||||
if (startDateTime <= now) {
|
||||
timesTriggered = 1;
|
||||
lastTriggered = startDateTime;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Repeating: walk intervals from startDateTime and count each one
|
||||
// that falls at or before 'now'.
|
||||
QDateTime t = startDateTime;
|
||||
while (t <= now) {
|
||||
++timesTriggered;
|
||||
lastTriggered = t;
|
||||
const QDateTime next = advanceFrom(t);
|
||||
if (!next.isValid()) {
|
||||
break;
|
||||
}
|
||||
t = next;
|
||||
}
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetRepeatDescription() const
|
||||
{
|
||||
if (!doesRepeat) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.once");
|
||||
}
|
||||
return QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.repeat.every"))
|
||||
.arg(QString::fromStdString(repeatInterval.ToString()));
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetSummary() const
|
||||
{
|
||||
if (!name.empty()) {
|
||||
return QString::fromStdString(name);
|
||||
}
|
||||
const std::string macroName = macro.Name();
|
||||
return macroName.empty()
|
||||
? QString(obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleEntry.noMacro"))
|
||||
: QString::fromStdString(macroName);
|
||||
}
|
||||
|
||||
QString MacroScheduleEntry::GetNextTriggerString() const
|
||||
{
|
||||
if (!enabled) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.disabled");
|
||||
}
|
||||
if (IsExpired()) {
|
||||
return obs_module_text(
|
||||
"AdvSceneSwitcher.macroScheduleTab.status.expired");
|
||||
}
|
||||
const QDateTime next = NextTriggerTime();
|
||||
if (!next.isValid()) {
|
||||
return QString("-");
|
||||
}
|
||||
return next.toString("yyyy-MM-dd HH:mm");
|
||||
}
|
||||
|
||||
std::deque<MacroScheduleEntry> &GetScheduleEntries()
|
||||
{
|
||||
return scheduleEntries;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void saveEntries(obs_data_t *obj)
|
||||
{
|
||||
OBSDataArrayAutoRelease array = obs_data_array_create();
|
||||
for (const auto &entry : scheduleEntries) {
|
||||
OBSDataAutoRelease item = obs_data_create();
|
||||
entry.Save(item);
|
||||
obs_data_array_push_back(array, item);
|
||||
}
|
||||
obs_data_set_array(obj, "macroScheduleEntries", array);
|
||||
}
|
||||
|
||||
static void loadEntries(obs_data_t *obj)
|
||||
{
|
||||
scheduleEntries.clear();
|
||||
OBSDataArrayAutoRelease array =
|
||||
obs_data_get_array(obj, "macroScheduleEntries");
|
||||
const size_t count = obs_data_array_count(array);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
OBSDataAutoRelease item = obs_data_array_item(array, i);
|
||||
scheduleEntries.emplace_back();
|
||||
scheduleEntries.back().Load(item);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler thread
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void applyEndDateAction(MacroScheduleEntry &entry)
|
||||
{
|
||||
if (!entry.hasEndDate || !entry.endDate.isValid()) {
|
||||
return;
|
||||
}
|
||||
if (entry.endDateAction == MacroScheduleEntry::EndDateAction::NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (entry.endDateAction) {
|
||||
case MacroScheduleEntry::EndDateAction::DISABLE_ENTRY:
|
||||
entry.enabled = false;
|
||||
break;
|
||||
case MacroScheduleEntry::EndDateAction::PAUSE_MACRO:
|
||||
StopMacro(entry.macro.GetMacro().get());
|
||||
SetMacroPaused(entry.macro.GetMacro().get(), true);
|
||||
break;
|
||||
case MacroScheduleEntry::EndDateAction::STOP_MACRO:
|
||||
StopMacro(entry.macro.GetMacro().get());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void checkAndFireEntries()
|
||||
{
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
auto lock = LockContext();
|
||||
|
||||
for (auto &entry : scheduleEntries) {
|
||||
// Apply end-date action once when the entry transitions to expired.
|
||||
// We detect the transition by checking whether the end date has
|
||||
// just passed while the entry is still nominally enabled.
|
||||
if (entry.hasEndDate && entry.endDate.isValid() &&
|
||||
entry.enabled && now >= entry.endDate &&
|
||||
entry.endDateAction !=
|
||||
MacroScheduleEntry::EndDateAction::NONE &&
|
||||
!entry.endDateActionApplied) {
|
||||
applyEndDateAction(entry);
|
||||
entry.endDateActionApplied = true;
|
||||
}
|
||||
|
||||
if (!entry.ShouldTrigger(now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto macro = entry.macro.GetMacro();
|
||||
if (!macro) {
|
||||
// Advance state so we don't spam-check a missing macro
|
||||
entry.MarkTriggered(now);
|
||||
blog(LOG_WARNING,
|
||||
"[macro-schedule] Scheduled macro '%s' not found, skipping.",
|
||||
entry.macro.Name().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.checkConditions) {
|
||||
if (CheckMacroConditions(macro.get(), true)) {
|
||||
RunMacroActions(macro.get(), true, true);
|
||||
} else if (entry.runElseActionsOnConditionFailure) {
|
||||
RunMacroElseActions(macro.get(), true, true);
|
||||
}
|
||||
} else {
|
||||
RunMacroActions(macro.get(), true, true);
|
||||
}
|
||||
entry.MarkTriggered(now);
|
||||
}
|
||||
}
|
||||
|
||||
static void initScheduler()
|
||||
{
|
||||
if (schedulerRunning.exchange(true)) {
|
||||
return; // already running
|
||||
}
|
||||
schedulerThread = std::thread([]() {
|
||||
while (schedulerRunning) {
|
||||
checkAndFireEntries();
|
||||
std::unique_lock<std::mutex> lock(schedulerWaitMutex);
|
||||
schedulerWaitCV.wait_for(
|
||||
lock, std::chrono::seconds(1),
|
||||
[]() { return !schedulerRunning.load(); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void cleanupScheduler()
|
||||
{
|
||||
schedulerRunning = false;
|
||||
schedulerWaitCV.notify_all();
|
||||
if (schedulerThread.joinable()) {
|
||||
schedulerThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
Reference in New Issue
Block a user