Compare commits

...

7 Commits

Author SHA1 Message Date
WarmUpTill
c8f4e524ba Make deps/exprtk dependency optional 2026-09-28 21:10:59 +02:00
WarmUpTill
b104f98d02 Fix Twitch token status not reflecting reality when cached user ID empty 2026-09-25 19:43:24 +02:00
WarmUpTill
abc8c3708f Fix Twitch token being treated as invalid after load with broken config
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-09-23 09:32:30 +02:00
TheRealAlexV
3b63db2bde Fix crash in SendWebsocketVendorEvent during OBS shutdown
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Guard the vendor event emission with obs_get_module("obs-websocket").
During obs_shutdown() modules are unloaded one-by-one; obs-websocket is
unloaded before advanced-scene-switcher, so the proc handler pointer (_ph)
cached in obs-websocket-api.h is already stale when FreeSceneSwitcher()
runs its stop steps. proc_handler_call() then dereferences freed memory
(use-after-free in pthread_mutex_unlock).

The existing OBSIsShuttingDown() guard does not catch this path: the
forced-exit that follows a "Source Cleanup Error" (scene collection
switch) never fires the SCRIPTING_SHUTDOWN frontend event, so the
plugin's obsIsShuttingDown flag is not set. obs_get_module() returns NULL
once the module is unloaded, which is the reliable signal that
obs-websocket's proc handler is gone.

Refs: #1404
2026-09-13 11:08:04 +02:00
WarmUpTill
fdc806be40 Convert dock window name field into a combo box
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Should make it easier to combine docks into a single dock window without
having to know the exact dock window titles beforehand
2026-09-08 22:52:43 +02:00
WarmUpTill
01107ee858 Fix crash on shutdown 2026-09-08 22:48:13 +02:00
WarmUpTill
be8bc4a63f Add mono option 2026-09-08 22:48:13 +02:00
11 changed files with 191 additions and 40 deletions

View File

@@ -383,8 +383,18 @@ target_precompile_headers(${LIB_NAME} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/pch.hpp") "${CMAKE_CURRENT_SOURCE_DIR}/lib/pch.hpp")
target_include_directories( target_include_directories(
${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib" ${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib")
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk/exprtk.hpp")
target_include_directories(${LIB_NAME}
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
target_compile_definitions(${LIB_NAME} PRIVATE EXPRTK_SUPPORT=1)
else()
message(
STATUS
"${PROJECT_NAME}: deps/exprtk not found - math expression support disabled"
)
endif()
if(NOT nlohmann_json_DIR if(NOT nlohmann_json_DIR
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/json/CMakeLists.txt") AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/json/CMakeLists.txt")

View File

@@ -267,6 +267,7 @@ AdvSceneSwitcher.macroTab.pauseStateSaveBehavior.unpause="Unpaused"
AdvSceneSwitcher.macroTab.currentRegisterDock="Register dock widget to control the pause state of selected macro or run it manually" AdvSceneSwitcher.macroTab.currentRegisterDock="Register dock widget to control the pause state of selected macro or run it manually"
AdvSceneSwitcher.macroTab.currentIsStandaloneDock="Is standalone dock" AdvSceneSwitcher.macroTab.currentIsStandaloneDock="Is standalone dock"
AdvSceneSwitcher.macroTab.currentDockWindowName="Add to macro dock with name:" AdvSceneSwitcher.macroTab.currentDockWindowName="Add to macro dock with name:"
AdvSceneSwitcher.macroTab.currentDockWindowName.addNew="Add new dock..."
AdvSceneSwitcher.macroTab.currentDockAddRunButton="Add button to run the macro" AdvSceneSwitcher.macroTab.currentDockAddRunButton="Add button to run the macro"
AdvSceneSwitcher.macroTab.currentDockAddPauseButton="Add button to pause or unpause the macro" AdvSceneSwitcher.macroTab.currentDockAddPauseButton="Add button to pause or unpause the macro"
AdvSceneSwitcher.macroTab.currentDockAddStatusLabel="Add status label" AdvSceneSwitcher.macroTab.currentDockAddStatusLabel="Add status label"
@@ -985,6 +986,7 @@ AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not avail
AdvSceneSwitcher.action.playAudio.tracks="Tracks" AdvSceneSwitcher.action.playAudio.tracks="Tracks"
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}" AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)" AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
AdvSceneSwitcher.action.playAudio.mono="Mono"
AdvSceneSwitcher.action.recording="Recording" AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording" AdvSceneSwitcher.action.recording.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording" AdvSceneSwitcher.action.recording.type.start="Start recording"

View File

@@ -1,5 +1,6 @@
#include "macro-settings.hpp" #include "macro-settings.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp" #include "macro.hpp"
#include "obs-module-helper.hpp" #include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp" #include "plugin-state-helpers.hpp"
@@ -8,11 +9,48 @@
#include <QScrollArea> #include <QScrollArea>
#include <QScrollBar> #include <QScrollBar>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <set>
namespace advss { namespace advss {
static GlobalMacroSettings macroSettings; static GlobalMacroSettings macroSettings;
static constexpr int addNewDockWindowRole = Qt::UserRole + 1;
static QStringList GetExistingDockWindowNames()
{
std::set<std::string> names;
for (const auto &macro : GetAllMacros()) {
if (!macro) {
continue;
}
const auto &dockSettings = macro->GetDockSettings();
if (dockSettings.IsStandaloneDock()) {
continue;
}
names.insert(dockSettings.DockWindowName());
}
QStringList result;
for (const auto &name : names) {
result << QString::fromStdString(name);
}
return result;
}
static void populateDockWindowNameCombo(QComboBox *combo)
{
combo->clear();
auto names = GetExistingDockWindowNames();
combo->addItems(names);
if (!names.isEmpty()) {
combo->insertSeparator(combo->count());
}
combo->addItem(obs_module_text(
"AdvSceneSwitcher.macroTab.currentDockWindowName.addNew"));
combo->setItemData(combo->count() - 1, true, addNewDockWindowRole);
}
void GlobalMacroSettings::Save(obs_data_t *obj) const void GlobalMacroSettings::Save(obs_data_t *obj) const
{ {
auto data = obs_data_create(); auto data = obs_data_create();
@@ -96,7 +134,7 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
"AdvSceneSwitcher.macroTab.currentRegisterDock"))), "AdvSceneSwitcher.macroTab.currentRegisterDock"))),
_currentMacroIsStandaloneDock(new QCheckBox(obs_module_text( _currentMacroIsStandaloneDock(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.macroTab.currentIsStandaloneDock"))), "AdvSceneSwitcher.macroTab.currentIsStandaloneDock"))),
_currentMacroDockWindowName(new QLineEdit(this)), _currentMacroDockWindowName(new QComboBox(this)),
_currentMacroDockAddRunButton(new QCheckBox(obs_module_text( _currentMacroDockAddRunButton(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.macroTab.currentDockAddRunButton"))), "AdvSceneSwitcher.macroTab.currentDockAddRunButton"))),
_currentMacroDockAddPauseButton(new QCheckBox(obs_module_text( _currentMacroDockAddPauseButton(new QCheckBox(obs_module_text(
@@ -143,6 +181,17 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
"AdvSceneSwitcher.macroTab.pauseStateSaveBehavior.unpause"), "AdvSceneSwitcher.macroTab.pauseStateSaveBehavior.unpause"),
static_cast<int>(Macro::PauseStateSaveBehavior::UNPAUSE)); static_cast<int>(Macro::PauseStateSaveBehavior::UNPAUSE));
_currentMacroDockWindowName->setEditable(true);
populateDockWindowNameCombo(_currentMacroDockWindowName);
connect(_currentMacroDockWindowName,
qOverload<int>(&QComboBox::activated), this, [this](int index) {
if (_currentMacroDockWindowName
->itemData(index, addNewDockWindowRole)
.toBool()) {
_currentMacroDockWindowName->setCurrentText("");
}
});
auto highlightOptions = new QGroupBox( auto highlightOptions = new QGroupBox(
obs_module_text("AdvSceneSwitcher.macroTab.highlightSettings")); obs_module_text("AdvSceneSwitcher.macroTab.highlightSettings"));
auto highlightLayout = new QVBoxLayout; auto highlightLayout = new QVBoxLayout;
@@ -348,7 +397,7 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
_currentMacroRegisterDock->setChecked(dockEnabled); _currentMacroRegisterDock->setChecked(dockEnabled);
_currentMacroIsStandaloneDock->setChecked( _currentMacroIsStandaloneDock->setChecked(
dockSettings.IsStandaloneDock()); dockSettings.IsStandaloneDock());
_currentMacroDockWindowName->setText( _currentMacroDockWindowName->setCurrentText(
QString::fromStdString(dockSettings.DockWindowName())); QString::fromStdString(dockSettings.DockWindowName()));
_currentMacroDockAddRunButton->setChecked(dockSettings.HasRunButton()); _currentMacroDockAddRunButton->setChecked(dockSettings.HasRunButton());
_currentMacroDockAddPauseButton->setChecked( _currentMacroDockAddPauseButton->setChecked(
@@ -486,7 +535,7 @@ bool MacroSettingsDialog::AskForSettings(QWidget *parent,
dockSettings.SetIsStandaloneDock( dockSettings.SetIsStandaloneDock(
dialog._currentMacroIsStandaloneDock->isChecked()); dialog._currentMacroIsStandaloneDock->isChecked());
dockSettings.SetDockWindowName( dockSettings.SetDockWindowName(
dialog._currentMacroDockWindowName->text().toStdString()); dialog._currentMacroDockWindowName->currentText().toStdString());
dockSettings.SetHasRunButton( dockSettings.SetHasRunButton(
dialog._currentMacroDockAddRunButton->isChecked()); dialog._currentMacroDockAddRunButton->isChecked());
dockSettings.SetHasPauseButton( dockSettings.SetHasPauseButton(

View File

@@ -6,6 +6,7 @@
#include <QWidget> #include <QWidget>
#include <QDialog> #include <QDialog>
#include <QCheckBox> #include <QCheckBox>
#include <QComboBox>
#include <QGroupBox> #include <QGroupBox>
#include <QLineEdit> #include <QLineEdit>
#include <QGridLayout> #include <QGridLayout>
@@ -71,7 +72,7 @@ private:
MacroInputSelection *_currentInputs; MacroInputSelection *_currentInputs;
QCheckBox *_currentMacroRegisterDock; QCheckBox *_currentMacroRegisterDock;
QCheckBox *_currentMacroIsStandaloneDock; QCheckBox *_currentMacroIsStandaloneDock;
QLineEdit *_currentMacroDockWindowName; QComboBox *_currentMacroDockWindowName;
QCheckBox *_currentMacroDockAddRunButton; QCheckBox *_currentMacroDockAddRunButton;
QCheckBox *_currentMacroDockAddPauseButton; QCheckBox *_currentMacroDockAddPauseButton;
QCheckBox *_currentMacroDockAddStatusLabel; QCheckBox *_currentMacroDockAddStatusLabel;

View File

@@ -2,13 +2,16 @@
#include "obs-module-helper.hpp" #include "obs-module-helper.hpp"
#include <climits> #include <climits>
#ifdef EXPRTK_SUPPORT
#include <exprtk.hpp> #include <exprtk.hpp>
#include <random> #include <random>
#endif
namespace advss { namespace advss {
std::variant<double, std::string> EvalMathExpression(const std::string &expr) std::variant<double, std::string> EvalMathExpression(const std::string &expr)
{ {
#ifdef EXPRTK_SUPPORT
static auto randomFunc = []() { static auto randomFunc = []() {
thread_local std::mt19937 gen(std::random_device{}()); thread_local std::mt19937 gen(std::random_device{}());
thread_local std::uniform_real_distribution<double> dis(0.0, thread_local std::uniform_real_distribution<double> dis(0.0,
@@ -26,6 +29,7 @@ std::variant<double, std::string> EvalMathExpression(const std::string &expr)
if (parser.compile(expr, expression)) { if (parser.compile(expr, expression)) {
return expression.value(); return expression.value();
} }
#endif
return std::string(obs_module_text( return std::string(obs_module_text(
"AdvSceneSwitcher.math.expressionFail")) + "AdvSceneSwitcher.math.expressionFail")) +
" \"" + expr + "\""; " \"" + expr + "\"";

View File

@@ -127,9 +127,10 @@ void RegisterWebsocketRequest(
void SendWebsocketVendorEvent(const std::string &eventName, obs_data_t *data) void SendWebsocketVendorEvent(const std::string &eventName, obs_data_t *data)
{ {
if (OBSIsShuttingDown()) { if (OBSIsShuttingDown() || !obs_get_module("obs-websocket")) {
return; return;
} }
obs_websocket_vendor_emit_event(vendor, eventName.c_str(), data); obs_websocket_vendor_emit_event(vendor, eventName.c_str(), data);
} }

View File

@@ -2,18 +2,64 @@
#include "audio-helpers.hpp" #include "audio-helpers.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "macro-helpers.hpp" #include "macro-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "sync-helpers.hpp" #include "sync-helpers.hpp"
#include <obs-frontend-api.h>
#include <chrono> #include <chrono>
#include <mutex>
#include <set>
#include <QFileInfo> #include <QFileInfo>
#include <QLabel> #include <QLabel>
namespace advss { namespace advss {
// Use a high output channel index that is unlikely to be claimed by OBS or
// other plugins. OBS supports channels 0-63; channel 0 is the main scene.
static constexpr uint32_t kPlaybackOutputChannel = 63; static constexpr uint32_t kPlaybackOutputChannel = 63;
// Tracks rawSource handles held by background cleanup threads so they can be
// stopped early when OBS begins shutdown, before audio is freed.
static std::mutex g_activeSourcesMutex;
static std::set<obs_source_t *> g_activeSources;
static void registerActiveSource(obs_source_t *source)
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
g_activeSources.insert(source);
}
static void unregisterActiveSource(obs_source_t *source)
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
g_activeSources.erase(source);
}
static void stopAllActiveSources()
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
for (auto *source : g_activeSources) {
obs_source_media_stop(source);
}
}
static void handleObsEvent(enum obs_frontend_event event, void *)
{
if (event == OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN ||
event == OBS_FRONTEND_EVENT_EXIT) {
stopAllActiveSources();
}
}
static bool setup()
{
AddPluginPostLoadStep([]() {
obs_frontend_add_event_callback(handleObsEvent, nullptr);
});
return true;
}
static bool setupDone = setup();
const std::string MacroActionPlayAudio::id = "play_audio"; const std::string MacroActionPlayAudio::id = "play_audio";
bool MacroActionPlayAudio::_registered = MacroActionFactory::Register( bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
@@ -23,6 +69,7 @@ bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
static void deactivatePlayback(obs_source_t *source, bool wantsOutput) static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
{ {
obs_source_media_stop(source);
if (wantsOutput) { if (wantsOutput) {
obs_set_output_source(kPlaybackOutputChannel, nullptr); obs_set_output_source(kPlaybackOutputChannel, nullptr);
} else { } else {
@@ -38,8 +85,7 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
std::unique_lock<std::mutex> lock(*GetMutex()); std::unique_lock<std::mutex> lock(*GetMutex());
SetMacroAbortWait(false); SetMacroAbortWait(false);
// The media source needs time to open and decode before reaching // Poll until the source starts playing or the macro is stopped.
// PLAYING state. Poll until it starts (or the macro is stopped).
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) { while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (obs_source_media_get_state(source) == if (obs_source_media_get_state(source) ==
OBS_MEDIA_STATE_PLAYING) { OBS_MEDIA_STATE_PLAYING) {
@@ -48,9 +94,8 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
GetMacroWaitCV().wait_for(lock, 10ms); GetMacroWaitCV().wait_for(lock, 10ms);
} }
// Now wait for playback to end. Require two consecutive non-playing // Wait for playback to end. Two consecutive non-playing samples are
// samples to avoid false positives on brief state transitions. // required to avoid false positives on brief state transitions.
// If maxMs > 0, also stop once that many milliseconds have elapsed.
const auto playbackStart = std::chrono::steady_clock::now(); const auto playbackStart = std::chrono::steady_clock::now();
static const int kStopThreshold = 2; static const int kStopThreshold = 2;
int stoppedCount = 0; int stoppedCount = 0;
@@ -95,7 +140,6 @@ bool MacroActionPlayAudio::PerformAction()
obs_data_set_string(settings, "local_file", path.c_str()); obs_data_set_string(settings, "local_file", path.c_str());
obs_data_set_bool(settings, "is_local_file", true); obs_data_set_bool(settings, "is_local_file", true);
obs_data_set_bool(settings, "looping", false); obs_data_set_bool(settings, "looping", false);
// Disable automatic restart on activate so we control start explicitly.
obs_data_set_bool(settings, "restart_on_activate", false); obs_data_set_bool(settings, "restart_on_activate", false);
obs_data_set_bool(settings, "close_when_inactive", true); obs_data_set_bool(settings, "close_when_inactive", true);
@@ -113,10 +157,11 @@ bool MacroActionPlayAudio::PerformAction()
DecibelToPercent(static_cast<float>(_volumeDB.GetValue())); DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
obs_source_set_volume(source, vol); obs_source_set_volume(source, vol);
obs_source_set_monitoring_type(source, _monitorType); obs_source_set_monitoring_type(source, _monitorType);
if (_mono) {
obs_source_set_flags(source, OBS_SOURCE_FLAG_FORCE_MONO);
}
// Fall back to monitor-only if all output tracks are deselected — // Fall back to monitor-only if all output tracks are deselected.
// there is no point routing through the output channel if the mixer
// mask would silence every track.
const bool wantsOutput = const bool wantsOutput =
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) && (_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
(_audioMixers != 0); (_audioMixers != 0);
@@ -128,10 +173,8 @@ bool MacroActionPlayAudio::PerformAction()
if (wantsOutput) { if (wantsOutput) {
obs_source_set_audio_mixers(source, _audioMixers); obs_source_set_audio_mixers(source, _audioMixers);
// Route through a private scene so we can position the scene // Route through a private scene positioned off-screen so the
// item far off-screen. This keeps the item "visible" (so audio // audio mixes into the output without video appearing on screen.
// is still mixed) while ensuring its video never intersects the
// output frame.
OBSSceneAutoRelease audioScene = OBSSceneAutoRelease audioScene =
obs_scene_create_private("advss_audio_scene"); obs_scene_create_private("advss_audio_scene");
obs_sceneitem_t *item = obs_scene_add(audioScene, source); obs_sceneitem_t *item = obs_scene_add(audioScene, source);
@@ -141,8 +184,7 @@ bool MacroActionPlayAudio::PerformAction()
} }
obs_set_output_source(kPlaybackOutputChannel, obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene)); obs_scene_get_source(audioScene));
// audioScene released here; the output channel holds the // audioScene is released here; the output channel keeps the scene alive.
// remaining reference and keeps the scene alive.
} else { } else {
obs_source_set_audio_mixers(source, 0); obs_source_set_audio_mixers(source, 0);
obs_source_inc_active(source); obs_source_inc_active(source);
@@ -167,15 +209,15 @@ bool MacroActionPlayAudio::PerformAction()
return true; return true;
} }
// Keep the source alive in a background thread that cleans up // Keep the source alive until playback ends via a background thread.
// once playback finishes. Grab an extra strong reference so the
// source survives beyond this stack frame.
auto rawSource = obs_source_get_ref(source); auto rawSource = obs_source_get_ref(source);
registerActiveSource(rawSource);
auto macro = GetMacro(); auto macro = GetMacro();
std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() { std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
waitForPlaybackToEnd(macro, rawSource, maxMs); waitForPlaybackToEnd(macro, rawSource, maxMs);
deactivatePlayback(rawSource, wantsOutput); deactivatePlayback(rawSource, wantsOutput);
unregisterActiveSource(rawSource);
obs_source_release(rawSource); obs_source_release(rawSource);
}); });
AddMacroHelperThread(macro, std::move(cleanupThread)); AddMacroHelperThread(macro, std::move(cleanupThread));
@@ -204,6 +246,7 @@ bool MacroActionPlayAudio::Save(obs_data_t *obj) const
obs_data_set_bool(obj, "useDuration", _useDuration); obs_data_set_bool(obj, "useDuration", _useDuration);
_playbackDuration.Save(obj, "playbackDuration"); _playbackDuration.Save(obj, "playbackDuration");
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion); obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
obs_data_set_bool(obj, "mono", _mono);
return true; return true;
} }
@@ -221,6 +264,7 @@ bool MacroActionPlayAudio::Load(obs_data_t *obj)
_useDuration = obs_data_get_bool(obj, "useDuration"); _useDuration = obs_data_get_bool(obj, "useDuration");
_playbackDuration.Load(obj, "playbackDuration"); _playbackDuration.Load(obj, "playbackDuration");
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion"); _waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
_mono = obs_data_get_bool(obj, "mono");
return true; return true;
} }
@@ -262,7 +306,9 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
_useDuration(new QCheckBox(this)), _useDuration(new QCheckBox(this)),
_playbackDuration(new DurationSelection(this, true, 0.0)), _playbackDuration(new DurationSelection(this, true, 0.0)),
_waitForCompletion(new QCheckBox( _waitForCompletion(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.wait"))) obs_module_text("AdvSceneSwitcher.action.playAudio.wait"))),
_mono(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.mono")))
{ {
_volumeDB->setMinimum(-100.0); _volumeDB->setMinimum(-100.0);
_volumeDB->setMaximum(0.0); _volumeDB->setMaximum(0.0);
@@ -303,6 +349,8 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
SLOT(PlaybackDurationChanged(const Duration &))); SLOT(PlaybackDurationChanged(const Duration &)));
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this, QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
SLOT(WaitChanged(int))); SLOT(WaitChanged(int)));
QWidget::connect(_mono, SIGNAL(stateChanged(int)), this,
SLOT(MonoChanged(int)));
auto tracksLayout = new QHBoxLayout; auto tracksLayout = new QHBoxLayout;
tracksLayout->setContentsMargins(0, 0, 0, 0); tracksLayout->setContentsMargins(0, 0, 0, 0);
@@ -346,6 +394,7 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
mainLayout->addLayout(startOffsetLayout); mainLayout->addLayout(startOffsetLayout);
mainLayout->addLayout(playbackDurationLayout); mainLayout->addLayout(playbackDurationLayout);
mainLayout->addWidget(_waitForCompletion); mainLayout->addWidget(_waitForCompletion);
mainLayout->addWidget(_mono);
setLayout(mainLayout); setLayout(mainLayout);
_entryData = entryData; _entryData = entryData;
@@ -375,6 +424,7 @@ void MacroActionPlayAudioEdit::UpdateEntryData()
_playbackDuration->SetDuration(_entryData->_playbackDuration); _playbackDuration->SetDuration(_entryData->_playbackDuration);
_playbackDuration->setEnabled(_entryData->_useDuration); _playbackDuration->setEnabled(_entryData->_useDuration);
_waitForCompletion->setChecked(_entryData->_waitForCompletion); _waitForCompletion->setChecked(_entryData->_waitForCompletion);
_mono->setChecked(_entryData->_mono);
} }
void MacroActionPlayAudioEdit::FilePathChanged(const QString &path) void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
@@ -443,4 +493,10 @@ void MacroActionPlayAudioEdit::WaitChanged(int value)
_entryData->_waitForCompletion = value; _entryData->_waitForCompletion = value;
} }
void MacroActionPlayAudioEdit::MonoChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_mono = value;
}
} // namespace advss } // namespace advss

View File

@@ -37,6 +37,7 @@ public:
bool _useDuration = false; bool _useDuration = false;
Duration _playbackDuration; Duration _playbackDuration;
bool _waitForCompletion = false; bool _waitForCompletion = false;
bool _mono = false;
static bool _registered; static bool _registered;
static const std::string id; static const std::string id;
@@ -68,6 +69,7 @@ private slots:
void UseDurationChanged(int); void UseDurationChanged(int);
void PlaybackDurationChanged(const Duration &); void PlaybackDurationChanged(const Duration &);
void WaitChanged(int value); void WaitChanged(int value);
void MonoChanged(int value);
signals: signals:
void HeaderInfoChanged(const QString &); void HeaderInfoChanged(const QString &);
@@ -83,6 +85,7 @@ private:
QCheckBox *_useDuration; QCheckBox *_useDuration;
DurationSelection *_playbackDuration; DurationSelection *_playbackDuration;
QCheckBox *_waitForCompletion; QCheckBox *_waitForCompletion;
QCheckBox *_mono;
std::shared_ptr<MacroActionPlayAudio> _entryData; std::shared_ptr<MacroActionPlayAudio> _entryData;
bool _loading = true; bool _loading = true;

View File

@@ -233,7 +233,12 @@ void TwitchToken::Load(obs_data_t *obj)
{ {
Item::Load(obj); Item::Load(obj);
_token = obs_data_get_string(obj, "token"); _token = obs_data_get_string(obj, "token");
_userID = obs_data_get_string(obj, "userID"); if (obs_data_has_user_value(obj, "userID") &&
*obs_data_get_string(obj, "userID") != '\0') {
_userID = obs_data_get_string(obj, "userID");
} else {
_userID = {};
}
obs_data_set_default_bool(obj, "validateEventSubTimestamps", true); obs_data_set_default_bool(obj, "validateEventSubTimestamps", true);
_validateEventSubTimestamps = _validateEventSubTimestamps =
obs_data_get_bool(obj, "validateEventSubTimestamps"); obs_data_get_bool(obj, "validateEventSubTimestamps");
@@ -255,7 +260,7 @@ void TwitchToken::Save(obs_data_t *obj) const
{ {
Item::Save(obj); Item::Save(obj);
obs_data_set_string(obj, "token", _token.c_str()); obs_data_set_string(obj, "token", _token.c_str());
if (_userID) { if (_userID && !_userID->empty()) {
obs_data_set_string(obj, "userID", _userID->c_str()); obs_data_set_string(obj, "userID", _userID->c_str());
} }
obs_data_set_bool(obj, "validateEventSubTimestamps", obs_data_set_bool(obj, "validateEventSubTimestamps",
@@ -310,7 +315,7 @@ bool TwitchToken::AnyOptionIsEnabled(
return false; return false;
} }
void TwitchToken::SetToken(const std::string &value) bool TwitchToken::SetToken(const std::string &value)
{ {
_token = value; _token = value;
auto res = auto res =
@@ -318,21 +323,34 @@ void TwitchToken::SetToken(const std::string &value)
if (res.status != 200) { if (res.status != 200) {
blog(LOG_WARNING, "failed to get Twitch user id from token!"); blog(LOG_WARNING, "failed to get Twitch user id from token!");
_userID = {}; _userID = {};
return; return false;
} }
bool gotUserID = false;
OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data"); OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data");
size_t count = obs_data_array_count(array); size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i); OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
_userID = obs_data_get_string(arrayObj, "id"); std::string id = obs_data_get_string(arrayObj, "id");
if (id.empty()) {
blog(LOG_WARNING,
"Twitch /helix/users response did not contain a user id!");
continue;
}
_userID = id;
_name = obs_data_get_string(arrayObj, "display_name"); _name = obs_data_get_string(arrayObj, "display_name");
gotUserID = true;
}
if (!gotUserID) {
return false;
} }
// Trigger resubscribes with new token // Trigger resubscribes with new token
if (_eventSub) { if (_eventSub) {
_eventSub->ClearActiveSubscriptions(); _eventSub->ClearActiveSubscriptions();
} }
return true;
} }
std::optional<std::string> TwitchToken::GetToken() const std::optional<std::string> TwitchToken::GetToken() const
@@ -388,7 +406,7 @@ bool TwitchToken::IsValid(bool forceUpdate) const
return false; return false;
} }
if (_userID && _userID != id) { if (_userID && !_userID->empty() && _userID != id) {
blog(LOG_INFO, blog(LOG_INFO,
"Twitch token %s does not match expected user (got %s, expected %s)!", "Twitch token %s does not match expected user (got %s, expected %s)!",
_name.c_str(), id, _userID->c_str()); _name.c_str(), id, _userID->c_str());
@@ -396,6 +414,10 @@ bool TwitchToken::IsValid(bool forceUpdate) const
return false; return false;
} }
if (!_userID || _userID->empty()) {
_userID = id;
}
_lastValidityCheckResult = true; _lastValidityCheckResult = true;
return true; return true;
}; };
@@ -819,9 +841,9 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
return; return;
} }
_currentToken.SetToken(value.value().toStdString()); bool success = _currentToken.SetToken(value.value().toStdString());
auto name = QString::fromStdString(_currentToken._name); auto name = QString::fromStdString(_currentToken._name);
if (name.isEmpty()) { if (!success || name.isEmpty()) {
_tokenStatus->setText(obs_module_text( _tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.fail")); "AdvSceneSwitcher.twitchToken.request.fail"));
_name->setText(""); _name->setText("");

View File

@@ -48,7 +48,7 @@ public:
bool OptionIsActive(const TokenOption &option) const; bool OptionIsActive(const TokenOption &option) const;
bool OptionIsEnabled(const TokenOption &option) const; bool OptionIsEnabled(const TokenOption &option) const;
bool AnyOptionIsEnabled(const std::vector<TokenOption> &options) const; bool AnyOptionIsEnabled(const std::vector<TokenOption> &options) const;
void SetToken(const std::string &); bool SetToken(const std::string &);
bool IsEmpty() const { return _token.empty(); } bool IsEmpty() const { return _token.empty(); }
std::optional<std::string> GetToken() const; std::optional<std::string> GetToken() const;
std::optional<std::string> GetUserID() const { return _userID; } std::optional<std::string> GetUserID() const { return _userID; }
@@ -65,7 +65,7 @@ private:
mutable std::string _lastValidityCheckValue; mutable std::string _lastValidityCheckValue;
mutable bool _lastValidityCheckResult = false; mutable bool _lastValidityCheckResult = false;
mutable std::chrono::system_clock::time_point _lastValidityCheckTime; mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
std::optional<std::string> _userID; mutable std::optional<std::string> _userID;
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions(); std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
std::shared_ptr<EventSub> _eventSub; std::shared_ptr<EventSub> _eventSub;
bool _validateEventSubTimestamps = false; bool _validateEventSubTimestamps = false;

View File

@@ -85,8 +85,11 @@ target_sources(
${PROJECT_NAME} ${PROJECT_NAME}
PRIVATE test-math.cpp ${ADVSS_SOURCE_DIR}/lib/utils/math-helpers.cpp PRIVATE test-math.cpp ${ADVSS_SOURCE_DIR}/lib/utils/math-helpers.cpp
${ADVSS_SOURCE_DIR}/lib/utils/obs-module-helper.cpp) ${ADVSS_SOURCE_DIR}/lib/utils/obs-module-helper.cpp)
target_include_directories(${PROJECT_NAME} if(EXISTS "${ADVSS_SOURCE_DIR}/deps/exprtk/exprtk.hpp")
PRIVATE ${ADVSS_SOURCE_DIR}/deps/exprtk) target_include_directories(${PROJECT_NAME}
PRIVATE ${ADVSS_SOURCE_DIR}/deps/exprtk)
target_compile_definitions(${PROJECT_NAME} PRIVATE EXPRTK_SUPPORT=1)
endif()
if(MSVC) if(MSVC)
target_compile_options(${PROJECT_NAME} PUBLIC /MP /d2FH4- /wd4267 /bigobj) target_compile_options(${PROJECT_NAME} PUBLIC /MP /d2FH4- /wd4267 /bigobj)