Compare commits

..

1 Commits

Author SHA1 Message Date
WarmUpTill
bc17f0c708 Adaptitions for kWin support on Flatpak 2026-09-02 21:20:15 +02:00
20 changed files with 231 additions and 554 deletions

View File

@@ -267,7 +267,6 @@ 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"
@@ -343,9 +342,7 @@ AdvSceneSwitcher.condition.audio.type.volume="Configured volume level"
AdvSceneSwitcher.condition.audio.type.syncOffset="Sync offset" AdvSceneSwitcher.condition.audio.type.syncOffset="Sync offset"
AdvSceneSwitcher.condition.audio.type.monitor="Audio monitoring" AdvSceneSwitcher.condition.audio.type.monitor="Audio monitoring"
AdvSceneSwitcher.condition.audio.type.balance="Audio balance" AdvSceneSwitcher.condition.audio.type.balance="Audio balance"
AdvSceneSwitcher.condition.audio.type.track="Audio track"
AdvSceneSwitcher.condition.audio.entry="{{checkType}}of{{audioSources}}is{{condition}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}" AdvSceneSwitcher.condition.audio.entry="{{checkType}}of{{audioSources}}is{{condition}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}"
AdvSceneSwitcher.condition.audio.layout.track="{{audioSources}}is enabled on{{checkType}}{{track}}"
AdvSceneSwitcher.condition.cursor="Cursor" AdvSceneSwitcher.condition.cursor="Cursor"
AdvSceneSwitcher.condition.cursor.type.region="is in region" AdvSceneSwitcher.condition.cursor.type.region="is in region"
AdvSceneSwitcher.condition.cursor.type.moving="is moving" AdvSceneSwitcher.condition.cursor.type.moving="is moving"
@@ -986,7 +983,6 @@ 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"
@@ -2378,7 +2374,6 @@ AdvSceneSwitcher.tempVar.audio.muted="Source muted"
AdvSceneSwitcher.tempVar.audio.sync_offset="Source audio sync offset" AdvSceneSwitcher.tempVar.audio.sync_offset="Source audio sync offset"
AdvSceneSwitcher.tempVar.audio.monitor="Source audio monitor type" AdvSceneSwitcher.tempVar.audio.monitor="Source audio monitor type"
AdvSceneSwitcher.tempVar.audio.balance="Source audio balance" AdvSceneSwitcher.tempVar.audio.balance="Source audio balance"
AdvSceneSwitcher.tempVar.audio.trackEnabled="Audio track enabled"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary="Primary clipboard item MIME type" AdvSceneSwitcher.tempVar.clipboard.mimeType.primary="Primary clipboard item MIME type"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description="Highest priority MIME type of the current item stored in clipboard, if available." AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description="Highest priority MIME type of the current item stored in clipboard, if available."
@@ -3128,3 +3123,6 @@ AdvSceneSwitcher.sceneGroupTab.defaultname="Scene Group %1"
AdvSceneSwitcher.sceneGroupTab.exists="Scene Group or Scene name exists already" AdvSceneSwitcher.sceneGroupTab.exists="Scene Group or Scene name exists already"
AdvSceneSwitcher.sceneGroupTab.help="Scene Groups can be selected as a target just like a regular scene.\n\nAs the name suggests a scene group is a collection of multiple scenes.\nThe scene group will advance through the list of its assigned scenes depending on the configured settings, which can be found on the right side.\n\nYou can configure the scene group to advance to the next scene in the list:\nAfter a number of times the scene group is selected as a target.\nAfter a certain amount of time has passed.\nOr randomly.\n\nFor example, a scene group containing the scenes ...\nScene 1\nScene 2\nScene 3 \n... will activate \"Scene 1\" the first time it is selected as a target.\nThe second time it will activate \"Scene 2\".\nThe remaining times \"Scene 3\" will be activated.\n\nClick the highlighted plus symbol below to add a new scene group." AdvSceneSwitcher.sceneGroupTab.help="Scene Groups can be selected as a target just like a regular scene.\n\nAs the name suggests a scene group is a collection of multiple scenes.\nThe scene group will advance through the list of its assigned scenes depending on the configured settings, which can be found on the right side.\n\nYou can configure the scene group to advance to the next scene in the list:\nAfter a number of times the scene group is selected as a target.\nAfter a certain amount of time has passed.\nOr randomly.\n\nFor example, a scene group containing the scenes ...\nScene 1\nScene 2\nScene 3 \n... will activate \"Scene 1\" the first time it is selected as a target.\nThe second time it will activate \"Scene 2\".\nThe remaining times \"Scene 3\" will be activated.\n\nClick the highlighted plus symbol below to add a new scene group."
AdvSceneSwitcher.sceneGroupTab.scenes.help="Select the scene group you want to modify on the left.\n\nSelect a scene to add to this scene group by selecting the scene above and clicking the plus symbol below.\n\nA scene can be added multiple times to the same scene group." AdvSceneSwitcher.sceneGroupTab.scenes.help="Select the scene group you want to modify on the left.\n\nSelect a scene to add to this scene group by selecting the scene above and clicking the plus symbol below.\n\nA scene can be added multiple times to the same scene group."
AdvSceneSwitcher.kwin.flatpakPermissionWarning="Window detection via KWin is unavailable.\nThe OBS Studio Flatpak sandbox is missing the 'org.kde.KWin' D-Bus permission this feature needs.\nRun the following, then restart OBS:"
AdvSceneSwitcher.wayland.flatpakPermissionWarning="Window detection is unavailable.\nThe OBS Studio Flatpak has no X11 access on this Wayland session, and no compatible integration exists yet for this desktop.\nRunning OBS under an X11 session, or a non-Flatpak install, allows this feature to work."
AdvSceneSwitcher.flatpakWindowDetectionWarning.doNotShowAgain="Don't show this warning again"

View File

@@ -1,5 +1,16 @@
#include "platform-funcs.hpp" #include "platform-funcs.hpp"
#include "log-helper.hpp" #include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h>
#include <QCheckBox>
#include <QFontDatabase>
#include <QGridLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <X11/Xlib.h> #include <X11/Xlib.h>
#include <X11/Xatom.h> #include <X11/Xatom.h>
@@ -628,27 +639,108 @@ int ignoreXerror(Display *d, XErrorEvent *e)
return 0; return 0;
} }
static bool warnAboutWindowDetectionUnavailable = true;
static bool setupWindowDetectionWarningPersistence = []() {
AddSaveStep([](obs_data_t *obj) {
obs_data_set_bool(obj, "warnAboutFlatpakWindowDetection",
warnAboutWindowDetectionUnavailable);
});
AddLoadStep([](obs_data_t *obj) {
obs_data_set_default_bool(
obj, "warnAboutFlatpakWindowDetection", true);
warnAboutWindowDetectionUnavailable = obs_data_get_bool(
obj, "warnAboutFlatpakWindowDetection");
});
return true;
}();
static void showWindowDetectionWarning(const char *messageId,
const char *command)
{
auto mainWindow =
static_cast<QWidget *>(obs_frontend_get_main_window());
QMessageBox msgBox(QMessageBox::Warning,
obs_module_text("AdvSceneSwitcher.pluginName"),
obs_module_text(messageId), QMessageBox::Ok,
mainWindow);
if (command) {
auto commandLineEdit = new QLineEdit(command, &msgBox);
commandLineEdit->setReadOnly(true);
commandLineEdit->setFont(
QFontDatabase::systemFont(QFontDatabase::FixedFont));
commandLineEdit->setCursorPosition(0);
auto layout = qobject_cast<QGridLayout *>(msgBox.layout());
if (layout) {
layout->addWidget(commandLineEdit, layout->rowCount(),
0, 1, layout->columnCount());
}
}
auto checkbox = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.flatpakWindowDetectionWarning.doNotShowAgain"));
msgBox.setCheckBox(checkbox);
msgBox.exec();
warnAboutWindowDetectionUnavailable = !checkbox->isChecked();
}
static void handleWindowDetectionUnavailable(bool hasX11)
{
if (hasX11 || KWin) {
return;
}
if (!qEnvironmentVariableIsSet("FLATPAK_ID")) {
return;
}
const bool onKDE = qEnvironmentVariable("XDG_CURRENT_DESKTOP")
.contains("KDE", Qt::CaseInsensitive);
const char *messageId =
onKDE ? "AdvSceneSwitcher.kwin.flatpakPermissionWarning"
: "AdvSceneSwitcher.wayland.flatpakPermissionWarning";
const char *command =
onKDE ? "flatpak override --user --talk-name=org.kde.KWin com.obsproject.Studio"
: nullptr;
blog(LOG_WARNING,
"window detection unavailable: OBS Flatpak sandbox has no X11 "
"access on this Wayland session and %s",
onKDE ? "the KWin D-Bus compat call was blocked (missing "
"'org.kde.KWin' talk-name permission)"
: "no compatible native Wayland integration exists for "
"this desktop");
AddFinishedLoadingStep([messageId, command]() {
if (!warnAboutWindowDetectionUnavailable) {
return;
}
showWindowDetectionWarning(messageId, command);
});
}
void PlatformInit() void PlatformInit()
{ {
const bool kwinAvailable = isKWinAvailable();
KWin = kwinAvailable && registerKWinDBusListener(&notifier) &&
startKWinScript(KWinScriptObjectPath);
if (KWin) {
blog(LOG_INFO, "using KWin compat");
} else {
blog(LOG_INFO, "not using KWin compat");
}
initProcps();
initProc2();
auto display = disp(); auto display = disp();
handleWindowDetectionUnavailable(display != nullptr);
if (!display) { if (!display) {
return; return;
} }
KWin = isKWinAvailable();
if (!(KWin && startKWinScript(KWinScriptObjectPath) &&
registerKWinDBusListener(&notifier))) {
// something bad happened while trying to initialize
// the KWin script/dbus so disable it
KWin = false;
blog(LOG_INFO, "not using KWin compat");
} else {
blog(LOG_INFO, "using KWin compat");
}
initXss(); initXss();
initProcps();
initProc2();
XSetErrorHandler(ignoreXerror); XSetErrorHandler(ignoreXerror);
} }

View File

@@ -5,6 +5,7 @@
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileDevice> #include <QFileDevice>
#include <QStandardPaths>
#include <QTextStream> #include <QTextStream>
#include <QtDBus/QDBusConnectionInterface> #include <QtDBus/QDBusConnectionInterface>
#include <QtDBus/QDBusInterface> #include <QtDBus/QDBusInterface>
@@ -96,12 +97,16 @@ bool isKWinAvailable()
bool startKWinScript(QString &scriptObjectPath) bool startKWinScript(QString &scriptObjectPath)
{ {
// Not RuntimeLocation: under Flatpak, $XDG_RUNTIME_DIR is private to
// the sandbox, so the unsandboxed KWin process can't read it back.
const QString scriptPath = const QString scriptPath =
"/tmp/AdvancedSceneSwitcher/KWinFocusNotifier.js"; QStandardPaths::writableLocation(
QStandardPaths::CacheLocation) +
"/AdvancedSceneSwitcher/KWinFocusNotifier.js";
const QString script = const QString script =
R"(var adss = "com.github.AdvancedSceneSwitcher"; R"(var adss = "com.obsproject.Studio.Plugin.SceneSwitcher";
var adssPath = "/com/github/AdvancedSceneSwitcher"; var adssPath = "/com/obsproject/Studio/Plugin/SceneSwitcher";
function trackWindow(window) { function trackWindow(window) {
var id = window.internalId.toString(); var id = window.internalId.toString();
@@ -129,7 +134,8 @@ workspace.windowActivated.connect(function(client) {
}))"; }))";
if (const QDir dir; !dir.mkpath(QFileInfo(scriptPath).absolutePath())) { if (const QDir dir; !dir.mkpath(QFileInfo(scriptPath).absolutePath())) {
blog(LOG_ERROR, "error creating /tmp/AdvancedSceneSwitcher"); blog(LOG_ERROR,
"error creating KWinFocusNotifier script directory");
return false; return false;
} }
@@ -198,8 +204,10 @@ bool stopKWinScript(const QString &scriptObjectPath)
bool registerKWinDBusListener(FocusNotifier *notifier) bool registerKWinDBusListener(FocusNotifier *notifier)
{ {
static const QString serviceName = "com.github.AdvancedSceneSwitcher"; static const QString serviceName =
static const QString objectPath = "/com/github/AdvancedSceneSwitcher"; "com.obsproject.Studio.Plugin.SceneSwitcher";
static const QString objectPath =
"/com/obsproject/Studio/Plugin/SceneSwitcher";
auto bus = QDBusConnection::sessionBus(); auto bus = QDBusConnection::sessionBus();
if (bus.objectRegisteredAt(objectPath)) { if (bus.objectRegisteredAt(objectPath)) {

View File

@@ -12,7 +12,8 @@ namespace advss {
class FocusNotifier final : public QObject { class FocusNotifier final : public QObject {
Q_OBJECT Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "com.github.AdvancedSceneSwitcher") Q_CLASSINFO("D-Bus Interface",
"com.obsproject.Studio.Plugin.SceneSwitcher")
static std::mutex _mutex; static std::mutex _mutex;
static int activePID; static int activePID;

View File

@@ -1,6 +1,5 @@
#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"
@@ -9,48 +8,11 @@
#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();
@@ -134,7 +96,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 QComboBox(this)), _currentMacroDockWindowName(new QLineEdit(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(
@@ -181,17 +143,6 @@ 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;
@@ -397,7 +348,7 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
_currentMacroRegisterDock->setChecked(dockEnabled); _currentMacroRegisterDock->setChecked(dockEnabled);
_currentMacroIsStandaloneDock->setChecked( _currentMacroIsStandaloneDock->setChecked(
dockSettings.IsStandaloneDock()); dockSettings.IsStandaloneDock());
_currentMacroDockWindowName->setCurrentText( _currentMacroDockWindowName->setText(
QString::fromStdString(dockSettings.DockWindowName())); QString::fromStdString(dockSettings.DockWindowName()));
_currentMacroDockAddRunButton->setChecked(dockSettings.HasRunButton()); _currentMacroDockAddRunButton->setChecked(dockSettings.HasRunButton());
_currentMacroDockAddPauseButton->setChecked( _currentMacroDockAddPauseButton->setChecked(
@@ -535,7 +486,7 @@ bool MacroSettingsDialog::AskForSettings(QWidget *parent,
dockSettings.SetIsStandaloneDock( dockSettings.SetIsStandaloneDock(
dialog._currentMacroIsStandaloneDock->isChecked()); dialog._currentMacroIsStandaloneDock->isChecked());
dockSettings.SetDockWindowName( dockSettings.SetDockWindowName(
dialog._currentMacroDockWindowName->currentText().toStdString()); dialog._currentMacroDockWindowName->text().toStdString());
dockSettings.SetHasRunButton( dockSettings.SetHasRunButton(
dialog._currentMacroDockAddRunButton->isChecked()); dialog._currentMacroDockAddRunButton->isChecked());
dockSettings.SetHasPauseButton( dockSettings.SetHasPauseButton(

View File

@@ -6,7 +6,6 @@
#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>
@@ -72,7 +71,7 @@ private:
MacroInputSelection *_currentInputs; MacroInputSelection *_currentInputs;
QCheckBox *_currentMacroRegisterDock; QCheckBox *_currentMacroRegisterDock;
QCheckBox *_currentMacroIsStandaloneDock; QCheckBox *_currentMacroIsStandaloneDock;
QComboBox *_currentMacroDockWindowName; QLineEdit *_currentMacroDockWindowName;
QCheckBox *_currentMacroDockAddRunButton; QCheckBox *_currentMacroDockAddRunButton;
QCheckBox *_currentMacroDockAddPauseButton; QCheckBox *_currentMacroDockAddPauseButton;
QCheckBox *_currentMacroDockAddStatusLabel; QCheckBox *_currentMacroDockAddStatusLabel;

View File

@@ -420,10 +420,11 @@ static bool queueWithNameExists(const std::string &name)
return !GetWeakActionQueueByName(name).expired(); return !GetWeakActionQueueByName(name).expired();
} }
static void static void signalImportedQueues(void *varsPtr)
signalImportedQueues(const std::vector<std::shared_ptr<Item>> &queues)
{ {
for (const auto &queue : queues) { auto queues = std::unique_ptr<std::vector<std::shared_ptr<Item>>>(
static_cast<std::vector<std::shared_ptr<Item>> *>(varsPtr));
for (const auto &queue : *queues) {
ActionQueueSignalManager::Instance()->Add( ActionQueueSignalManager::Instance()->Add(
QString::fromStdString(queue->Name())); QString::fromStdString(queue->Name()));
} }
@@ -435,7 +436,7 @@ void ImportQueues(obs_data_t *data)
obs_data_get_array(data, "actionQueues"); obs_data_get_array(data, "actionQueues");
size_t count = obs_data_array_count(array); size_t count = obs_data_array_count(array);
std::vector<std::shared_ptr<Item>> importedQueues; auto importedQueues = new std::vector<std::shared_ptr<Item>>;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayElement = obs_data_array_item(array, i); OBSDataAutoRelease arrayElement = obs_data_array_item(array, i);
@@ -445,11 +446,10 @@ void ImportQueues(obs_data_t *data)
continue; continue;
} }
queues.emplace_back(queue); queues.emplace_back(queue);
importedQueues.emplace_back(queue); importedQueues->emplace_back(queue);
} }
QueueUITask( QueueUITask(signalImportedQueues, importedQueues);
[importedQueues]() { signalImportedQueues(importedQueues); });
} }
std::weak_ptr<ActionQueue> GetWeakActionQueueByName(const std::string &name) std::weak_ptr<ActionQueue> GetWeakActionQueueByName(const std::string &name)

View File

@@ -734,10 +734,12 @@ TempVarSignalManager *TempVarSignalManager::Instance()
void NotifyUIAboutTempVarChange(MacroSegment *segment) void NotifyUIAboutTempVarChange(MacroSegment *segment)
{ {
IncrementTempVarInUseGeneration(); IncrementTempVarInUseGeneration();
QueueUITask([segment]() { QueueUITask(
TempVarSignalManager::Instance()->SegmentTempVarsChanged( [](void *segment) {
segment); TempVarSignalManager::Instance()->SegmentTempVarsChanged(
}); (MacroSegment *)segment);
},
segment);
} }
TempVarOutputMappingsWidget::TempVarOutputMappingsWidget(QWidget *parent) TempVarOutputMappingsWidget::TempVarOutputMappingsWidget(QWidget *parent)

View File

@@ -184,9 +184,9 @@ std::string GetThemeTypeName()
#endif #endif
} }
void QueueUITaskRaw(void (*task)(void *param), void *param, bool wait) void QueueUITask(void (*task)(void *param), void *param)
{ {
obs_queue_task(OBS_TASK_UI, task, param, wait); obs_queue_task(OBS_TASK_UI, task, param, false);
} }
bool IsCursorInWidgetArea(QWidget *widget) bool IsCursorInWidgetArea(QWidget *widget)

View File

@@ -6,10 +6,7 @@
#include <QIcon> #include <QIcon>
#include <QString> #include <QString>
#include <memory>
#include <string> #include <string>
#include <type_traits>
#include <utility>
class QAbstractButton; class QAbstractButton;
class QComboBox; class QComboBox;
@@ -41,23 +38,7 @@ EXPORT void DisplayTrayMessage(const QString &title, const QString &msg,
EXPORT std::string GetThemeTypeName(); EXPORT std::string GetThemeTypeName();
EXPORT QWidget *GetSettingsWindow(); EXPORT QWidget *GetSettingsWindow();
EXPORT void QueueUITaskRaw(void (*task)(void *param), void *param, EXPORT void QueueUITask(void (*task)(void *param), void *param);
bool wait = false);
// Runs func on the main/UI thread; blocks if wait is true.
template<typename F> void QueueUITask(F &&func, bool wait = false)
{
using FnType = std::decay_t<F>;
auto *heapFunc = new FnType(std::forward<F>(func));
QueueUITaskRaw(
[](void *param) {
std::unique_ptr<FnType> fn(
static_cast<FnType *>(param));
(*fn)();
},
heapFunc, wait);
}
bool IsCursorInWidgetArea(QWidget *widget); bool IsCursorInWidgetArea(QWidget *widget);

View File

@@ -504,10 +504,11 @@ void LoadVariables(obs_data_t *obj)
} }
} }
static void static void signalImportedVariables(void *varsPtr)
signalImportedVariables(const std::vector<std::shared_ptr<Item>> &vars)
{ {
for (const auto &var : vars) { auto vars = std::unique_ptr<std::vector<std::shared_ptr<Item>>>(
static_cast<std::vector<std::shared_ptr<Item>> *>(varsPtr));
for (const auto &var : *vars) {
VariableSignalManager::Instance()->Add( VariableSignalManager::Instance()->Add(
QString::fromStdString(var->Name())); QString::fromStdString(var->Name()));
} }
@@ -518,7 +519,7 @@ void ImportVariables(obs_data_t *data)
OBSDataArrayAutoRelease array = obs_data_get_array(data, "variables"); OBSDataArrayAutoRelease array = obs_data_get_array(data, "variables");
size_t count = obs_data_array_count(array); size_t count = obs_data_array_count(array);
std::vector<std::shared_ptr<Item>> importedVars; auto importedVars = new std::vector<std::shared_ptr<Item>>;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayElement = obs_data_array_item(array, i); OBSDataAutoRelease arrayElement = obs_data_array_item(array, i);
@@ -530,11 +531,10 @@ void ImportVariables(obs_data_t *data)
} }
GetVariables().emplace_back(var); GetVariables().emplace_back(var);
importedVars.emplace_back(var); importedVars->emplace_back(var);
} }
QueueUITask( QueueUITask(signalImportedVariables, importedVars);
[importedVars]() { signalImportedVariables(importedVars); });
} }
std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime() std::chrono::high_resolution_clock::time_point GetLastVariableChangeTime()

View File

@@ -2,64 +2,18 @@
#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(
@@ -69,7 +23,6 @@ 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 {
@@ -85,7 +38,8 @@ 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);
// Poll until the source starts playing or the macro is stopped. // The media source needs time to open and decode before reaching
// 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) {
@@ -94,8 +48,9 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
GetMacroWaitCV().wait_for(lock, 10ms); GetMacroWaitCV().wait_for(lock, 10ms);
} }
// Wait for playback to end. Two consecutive non-playing samples are // Now wait for playback to end. Require two consecutive non-playing
// required to avoid false positives on brief state transitions. // samples 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;
@@ -140,6 +95,7 @@ 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);
@@ -157,11 +113,10 @@ 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);
@@ -173,8 +128,10 @@ 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 positioned off-screen so the // Route through a private scene so we can position the scene
// audio mixes into the output without video appearing on screen. // item far off-screen. This keeps the item "visible" (so audio
// 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);
@@ -184,7 +141,8 @@ bool MacroActionPlayAudio::PerformAction()
} }
obs_set_output_source(kPlaybackOutputChannel, obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene)); obs_scene_get_source(audioScene));
// audioScene is released here; the output channel keeps the scene alive. // audioScene released here; the output channel holds the
// 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);
@@ -209,15 +167,15 @@ bool MacroActionPlayAudio::PerformAction()
return true; return true;
} }
// Keep the source alive until playback ends via a background thread. // Keep the source alive in a background thread that cleans up
// 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));
@@ -246,7 +204,6 @@ 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;
} }
@@ -264,7 +221,6 @@ 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;
} }
@@ -306,9 +262,7 @@ 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);
@@ -349,8 +303,6 @@ 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);
@@ -394,7 +346,6 @@ 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;
@@ -424,7 +375,6 @@ 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)
@@ -493,10 +443,4 @@ 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,7 +37,6 @@ 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;
@@ -69,7 +68,6 @@ 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 &);
@@ -85,7 +83,6 @@ 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

@@ -1,9 +1,7 @@
#include "macro-action-scene-collection.hpp" #include "macro-action-scene-collection.hpp"
#include "layout-helpers.hpp" #include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp" #include "plugin-state-helpers.hpp"
#include "selection-helpers.hpp" #include "selection-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
@@ -25,13 +23,7 @@ bool MacroActionSceneCollection::PerformAction()
if (SettingsWindowIsOpened()) { if (SettingsWindowIsOpened()) {
return false; return false;
} }
obs_frontend_set_current_scene_collection(_sceneCollection.c_str());
const auto collectionName = _sceneCollection;
QueueUITask([collectionName]() {
obs_frontend_set_current_scene_collection(
collectionName.c_str());
});
// It does not make sense to continue as the current settings will be // It does not make sense to continue as the current settings will be
// invalid after switching scene collection. // invalid after switching scene collection.
return false; return false;

View File

@@ -185,6 +185,20 @@ static void closeSourceDialog(obs_source_t *source, bool accept,
} }
} }
template<typename F> void QueueUITaskLambda(F &&func)
{
using FnType = std::decay_t<F>;
auto *heapFunc = new FnType(std::forward<F>(func));
QueueUITask(
[](void *param) {
std::unique_ptr<FnType> fn(
static_cast<FnType *>(param));
(*fn)();
},
heapFunc);
}
bool MacroActionSource::PerformAction() bool MacroActionSource::PerformAction()
{ {
OBSSource s = obs_weak_source_get_source(_source.GetSource()); OBSSource s = obs_weak_source_get_source(_source.GetSource());
@@ -257,17 +271,17 @@ bool MacroActionSource::PerformAction()
break; break;
} }
QueueUITask([&]() { QueueUITaskLambda([&]() {
closeSourceDialog(s, true, "OBSBasicInteraction"); closeSourceDialog(s, true, "OBSBasicInteraction");
}); });
break; break;
case Action::CLOSE_FILTER_DIALOG: case Action::CLOSE_FILTER_DIALOG:
QueueUITask([&]() { QueueUITaskLambda([&]() {
closeSourceDialog(s, _acceptDialog, "OBSBasicFilters"); closeSourceDialog(s, _acceptDialog, "OBSBasicFilters");
}); });
break; break;
case Action::CLOSE_PROPERTIES_DIALOG: case Action::CLOSE_PROPERTIES_DIALOG:
QueueUITask([&]() { QueueUITaskLambda([&]() {
closeSourceDialog(s, _acceptDialog, closeSourceDialog(s, _acceptDialog,
"OBSBasicProperties"); "OBSBasicProperties");
}); });

View File

@@ -25,8 +25,6 @@ const static std::map<MacroConditionAudio::Type, std::string> checkTypes = {
"AdvSceneSwitcher.condition.audio.type.monitor"}, "AdvSceneSwitcher.condition.audio.type.monitor"},
{MacroConditionAudio::Type::BALANCE, {MacroConditionAudio::Type::BALANCE,
"AdvSceneSwitcher.condition.audio.type.balance"}, "AdvSceneSwitcher.condition.audio.type.balance"},
{MacroConditionAudio::Type::TRACK,
"AdvSceneSwitcher.condition.audio.type.track"},
}; };
const static std::map<MacroConditionAudio::OutputCondition, std::string> const static std::map<MacroConditionAudio::OutputCondition, std::string>
@@ -247,20 +245,6 @@ bool MacroConditionAudio::CheckBalance()
return ret && source; return ret && source;
} }
bool MacroConditionAudio::CheckTrack()
{
if (!_audioSource.GetSource()) {
return false;
}
OBSSourceAutoRelease source =
obs_weak_source_get_source(_audioSource.GetSource());
uint32_t mixers = obs_source_get_audio_mixers(source);
bool enabled = (mixers >> (uint32_t)(_track - 1)) & 1u;
SetTempVarValue("track_enabled", enabled ? "true" : "false");
return enabled && source;
}
bool MacroConditionAudio::CheckCondition() bool MacroConditionAudio::CheckCondition()
{ {
bool ret = false; bool ret = false;
@@ -280,9 +264,6 @@ bool MacroConditionAudio::CheckCondition()
case Type::BALANCE: case Type::BALANCE:
ret = CheckBalance(); ret = CheckBalance();
break; break;
case Type::TRACK:
ret = CheckTrack();
break;
} }
if (GetVariableValue().empty()) { if (GetVariableValue().empty()) {
@@ -300,7 +281,6 @@ bool MacroConditionAudio::Save(obs_data_t *obj) const
_volumePercent.Save(obj, "volume"); _volumePercent.Save(obj, "volume");
_syncOffset.Save(obj, "syncOffset"); _syncOffset.Save(obj, "syncOffset");
_balance.Save(obj, "balance"); _balance.Save(obj, "balance");
_track.Save(obj, "track");
obs_data_set_int(obj, "checkType", static_cast<int>(_checkType)); obs_data_set_int(obj, "checkType", static_cast<int>(_checkType));
obs_data_set_int(obj, "outputCondition", obs_data_set_int(obj, "outputCondition",
static_cast<int>(_outputCondition)); static_cast<int>(_outputCondition));
@@ -366,7 +346,6 @@ bool MacroConditionAudio::Load(obs_data_t *obj)
_useDb = obs_data_get_bool(obj, "useDb"); _useDb = obs_data_get_bool(obj, "useDb");
_volumeDB.Load(obj, "volumeDB"); _volumeDB.Load(obj, "volumeDB");
} }
_track.Load(obj, "track");
return true; return true;
} }
@@ -442,12 +421,6 @@ void MacroConditionAudio::SetupTempVars()
obs_module_text( obs_module_text(
"AdvSceneSwitcher.tempVar.audio.balance")); "AdvSceneSwitcher.tempVar.audio.balance"));
break; break;
case Type::TRACK:
AddTempvar(
"track_enabled",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.trackEnabled"));
break;
default: default:
break; break;
} }
@@ -495,7 +468,6 @@ static QStringList getAudioSourcesList()
MacroConditionAudioEdit::MacroConditionAudioEdit( MacroConditionAudioEdit::MacroConditionAudioEdit(
QWidget *parent, std::shared_ptr<MacroConditionAudio> entryData) QWidget *parent, std::shared_ptr<MacroConditionAudio> entryData)
: QWidget(parent), : QWidget(parent),
_layout(new QHBoxLayout),
_checkTypes(new QComboBox()), _checkTypes(new QComboBox()),
_sources(new SourceSelectionWidget(this, getAudioSourcesList, true)), _sources(new SourceSelectionWidget(this, getAudioSourcesList, true)),
_condition(new QComboBox()), _condition(new QComboBox()),
@@ -504,8 +476,7 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
_percentDBToggle(new QPushButton), _percentDBToggle(new QPushButton),
_syncOffset(new VariableSpinBox()), _syncOffset(new VariableSpinBox()),
_monitorTypes(new QComboBox), _monitorTypes(new QComboBox),
_balance(new SliderSpinBox(0., 1., "")), _balance(new SliderSpinBox(0., 1., ""))
_track(new VariableSpinBox())
{ {
_volumePercent->setSuffix("%"); _volumePercent->setSuffix("%");
_volumePercent->setMaximum(100); _volumePercent->setMaximum(100);
@@ -520,9 +491,6 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
_syncOffset->setMaximum(20000); _syncOffset->setMaximum(20000);
_syncOffset->setSuffix("ms"); _syncOffset->setSuffix("ms");
_track->setMinimum(1);
_track->setMaximum(32);
QWidget::connect(_checkTypes, SIGNAL(currentIndexChanged(int)), this, QWidget::connect(_checkTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(CheckTypeChanged(int))); SLOT(CheckTypeChanged(int)));
QWidget::connect( QWidget::connect(
@@ -551,16 +519,27 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
this, SLOT(VolumeDBChanged(const NumberVariable<double> &))); this, SLOT(VolumeDBChanged(const NumberVariable<double> &)));
QWidget::connect(_percentDBToggle, SIGNAL(clicked()), this, QWidget::connect(_percentDBToggle, SIGNAL(clicked()), this,
SLOT(PercentDBClicked())); SLOT(PercentDBClicked()));
QWidget::connect(
_track,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(TrackChanged(const NumberVariable<int> &)));
populateCheckTypes(_checkTypes); populateCheckTypes(_checkTypes);
PopulateMonitorTypeSelection(_monitorTypes); PopulateMonitorTypeSelection(_monitorTypes);
QHBoxLayout *switchLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{checkType}}", _checkTypes},
{"{{audioSources}}", _sources},
{"{{volume}}", _volumePercent},
{"{{syncOffset}}", _syncOffset},
{"{{monitorTypes}}", _monitorTypes},
{"{{balance}}", _balance},
{"{{condition}}", _condition},
{"{{volumeDB}}", _volumeDB},
{"{{percentDBToggle}}", _percentDBToggle},
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.audio.entry"),
switchLayout, widgetPlaceholders);
QVBoxLayout *mainLayout = new QVBoxLayout; QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(_layout); mainLayout->addLayout(switchLayout);
mainLayout->addWidget(_balance); mainLayout->addWidget(_balance);
setLayout(mainLayout); setLayout(mainLayout);
@@ -629,12 +608,6 @@ void MacroConditionAudioEdit::BalanceChanged(const NumberVariable<double> &value
_entryData->_balance = value; _entryData->_balance = value;
} }
void MacroConditionAudioEdit::TrackChanged(const NumberVariable<int> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_track = value;
}
void MacroConditionAudioEdit::VolumeDBChanged( void MacroConditionAudioEdit::VolumeDBChanged(
const NumberVariable<double> &value) const NumberVariable<double> &value)
{ {
@@ -719,8 +692,6 @@ void MacroConditionAudioEdit::CheckTypeChanged(int idx)
} else if (_entryData->GetType() == } else if (_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME) { MacroConditionAudio::Type::CONFIGURED_VOLUME) {
populateVolumeConditionSelection(_condition); populateVolumeConditionSelection(_condition);
} else if (_entryData->GetType() == MacroConditionAudio::Type::TRACK) {
_condition->clear();
} }
SetWidgetVisibility(); SetWidgetVisibility();
} }
@@ -737,7 +708,6 @@ void MacroConditionAudioEdit::UpdateEntryData()
_syncOffset->SetValue(_entryData->_syncOffset); _syncOffset->SetValue(_entryData->_syncOffset);
_monitorTypes->setCurrentIndex(_entryData->_monitorType); _monitorTypes->setCurrentIndex(_entryData->_monitorType);
_balance->SetDoubleValue(_entryData->_balance); _balance->SetDoubleValue(_entryData->_balance);
_track->SetValue(_entryData->_track);
_checkTypes->setCurrentIndex( _checkTypes->setCurrentIndex(
_checkTypes->findData(static_cast<int>(_entryData->GetType()))); _checkTypes->findData(static_cast<int>(_entryData->GetType())));
@@ -778,63 +748,26 @@ void MacroConditionAudioEdit::SetWidgetVisibility()
return; return;
} }
_layout->removeWidget(_checkTypes);
_layout->removeWidget(_sources);
_layout->removeWidget(_condition);
_layout->removeWidget(_volumePercent);
_layout->removeWidget(_volumeDB);
_layout->removeWidget(_percentDBToggle);
_layout->removeWidget(_syncOffset);
_layout->removeWidget(_monitorTypes);
_layout->removeWidget(_track);
ClearLayout(_layout);
const std::unordered_map<std::string, QWidget *> placeholders = {
{"{{checkType}}", _checkTypes},
{"{{audioSources}}", _sources},
{"{{condition}}", _condition},
{"{{volume}}", _volumePercent},
{"{{volumeDB}}", _volumeDB},
{"{{percentDBToggle}}", _percentDBToggle},
{"{{syncOffset}}", _syncOffset},
{"{{monitorTypes}}", _monitorTypes},
{"{{track}}", _track},
};
const bool isTrack = _entryData->GetType() ==
MacroConditionAudio::Type::TRACK;
PlaceWidgets(
obs_module_text(
isTrack ? "AdvSceneSwitcher.condition.audio.layout.track"
: "AdvSceneSwitcher.condition.audio.entry"),
_layout, placeholders);
_condition->setVisible( _condition->setVisible(
!isTrack && _entryData->GetType() ==
(_entryData->GetType() == MacroConditionAudio::Type::OUTPUT_VOLUME ||
MacroConditionAudio::Type::OUTPUT_VOLUME || _entryData->GetType() ==
_entryData->GetType() == MacroConditionAudio::Type::CONFIGURED_VOLUME ||
MacroConditionAudio::Type::CONFIGURED_VOLUME || _entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE || _entryData->GetType() ==
_entryData->GetType() == MacroConditionAudio::Type::SYNC_OFFSET);
MacroConditionAudio::Type::SYNC_OFFSET)); _syncOffset->setVisible(_entryData->GetType() ==
_syncOffset->setVisible(!isTrack && MacroConditionAudio::Type::SYNC_OFFSET);
_entryData->GetType() == _monitorTypes->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET); MacroConditionAudio::Type::MONITOR);
_monitorTypes->setVisible(!isTrack &&
_entryData->GetType() ==
MacroConditionAudio::Type::MONITOR);
_track->setVisible(isTrack);
_volumePercent->setVisible(!isTrack && HasVolumeControl() &&
!_entryData->_useDb);
_volumeDB->setVisible(!isTrack && HasVolumeControl() &&
_entryData->_useDb);
_percentDBToggle->setText(_entryData->_useDb ? "dB" : "%");
_percentDBToggle->setVisible(!isTrack && HasVolumeControl());
_balance->setVisible(_entryData->GetType() == _balance->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::BALANCE); MacroConditionAudio::Type::BALANCE);
_volMeter->setVisible(_entryData->GetType() == _volMeter->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME); MacroConditionAudio::Type::OUTPUT_VOLUME);
_volumePercent->setVisible(HasVolumeControl() && !_entryData->_useDb);
_volumeDB->setVisible(HasVolumeControl() && _entryData->_useDb);
_percentDBToggle->setText(_entryData->_useDb ? "dB" : "%");
_percentDBToggle->setVisible(HasVolumeControl());
adjustSize(); adjustSize();
} }

View File

@@ -5,7 +5,6 @@
#include "source-selection.hpp" #include "source-selection.hpp"
#include <limits> #include <limits>
#include <QHBoxLayout>
#include <QWidget> #include <QWidget>
#include <QComboBox> #include <QComboBox>
#include <chrono> #include <chrono>
@@ -37,7 +36,6 @@ public:
SYNC_OFFSET, SYNC_OFFSET,
MONITOR, MONITOR,
BALANCE, BALANCE,
TRACK,
}; };
void SetType(const Type &); void SetType(const Type &);
Type GetType() const { return _checkType; } Type GetType() const { return _checkType; }
@@ -62,7 +60,6 @@ public:
IntVariable _syncOffset = 0; IntVariable _syncOffset = 0;
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_NONE; obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_NONE;
DoubleVariable _balance = 0.5; DoubleVariable _balance = 0.5;
IntVariable _track = 1;
OutputCondition _outputCondition = OutputCondition::ABOVE; OutputCondition _outputCondition = OutputCondition::ABOVE;
VolumeCondition _volumeCondition = VolumeCondition::ABOVE; VolumeCondition _volumeCondition = VolumeCondition::ABOVE;
obs_volmeter_t *_volmeter = nullptr; obs_volmeter_t *_volmeter = nullptr;
@@ -73,7 +70,6 @@ private:
bool CheckSyncOffset(); bool CheckSyncOffset();
bool CheckMonitor(); bool CheckMonitor();
bool CheckBalance(); bool CheckBalance();
bool CheckTrack();
void SetupTempVars(); void SetupTempVars();
float GetVolumePeak(); float GetVolumePeak();
@@ -115,7 +111,6 @@ private slots:
void VolumeDBChanged(const NumberVariable<double> &value); void VolumeDBChanged(const NumberVariable<double> &value);
void PercentDBClicked(); void PercentDBClicked();
void SyncSliderAndValueSelection(bool sliderMoved); void SyncSliderAndValueSelection(bool sliderMoved);
void TrackChanged(const NumberVariable<int> &value);
signals: signals:
void HeaderInfoChanged(const QString &); void HeaderInfoChanged(const QString &);
@@ -124,7 +119,6 @@ private:
void SetWidgetVisibility(); void SetWidgetVisibility();
bool HasVolumeControl() const; bool HasVolumeControl() const;
QHBoxLayout *_layout;
QComboBox *_checkTypes; QComboBox *_checkTypes;
SourceSelectionWidget *_sources; SourceSelectionWidget *_sources;
QComboBox *_condition; QComboBox *_condition;
@@ -134,7 +128,6 @@ private:
VariableSpinBox *_syncOffset; VariableSpinBox *_syncOffset;
QComboBox *_monitorTypes; QComboBox *_monitorTypes;
SliderSpinBox *_balance; SliderSpinBox *_balance;
VariableSpinBox *_track;
VolControl *_volMeter = nullptr; VolControl *_volMeter = nullptr;
std::shared_ptr<MacroConditionAudio> _entryData; std::shared_ptr<MacroConditionAudio> _entryData;

View File

@@ -5,74 +5,8 @@
#include <QFileDialog> #include <QFileDialog>
#ifdef __APPLE__
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <csignal>
#include <cerrno>
#include <chrono>
#include <thread>
#include <vector>
extern char **environ;
#endif
namespace advss { namespace advss {
#ifdef __APPLE__
// QProcess uses fork(), which is unsafe to call in OBS's multi-threaded,
// CEF/XPC-using process (see fork(2)). posix_spawn() avoids fork() entirely.
namespace {
std::vector<char *> BuildArgv(const std::string &path, const QStringList &args,
std::vector<std::string> &storage)
{
storage.push_back(path);
for (auto &arg : args) {
storage.push_back(arg.toStdString());
}
std::vector<char *> argv;
argv.reserve(storage.size() + 1);
for (auto &arg : storage) {
argv.push_back(const_cast<char *>(arg.c_str()));
}
argv.push_back(nullptr);
return argv;
}
bool DrainPipe(int fd, std::string &buffer)
{
char chunk[4096];
while (true) {
ssize_t n = read(fd, chunk, sizeof(chunk));
if (n > 0) {
buffer.append(chunk, static_cast<size_t>(n));
continue;
}
if (n == 0) {
return false; // EOF
}
if (errno == EINTR) {
continue;
}
return true; // EAGAIN/EWOULDBLOCK
}
}
std::string TrimTrailingNewline(const std::string &s)
{
static const QRegularExpression regex("(\\r\\n|\\r|\\n)$");
return QString::fromStdString(s).remove(regex).toStdString();
}
} // namespace
#endif
bool ProcessConfig::Save(obs_data_t *obj) const bool ProcessConfig::Save(obs_data_t *obj) const
{ {
auto data = obs_data_create(); auto data = obs_data_create();
@@ -116,44 +50,11 @@ QStringList ProcessConfig::Args() const
return result; return result;
} }
#ifdef __APPLE__
bool ProcessConfig::StartProcessDetached() const
{
auto path = Path();
auto workDir = WorkingDir();
std::vector<std::string> argStorage;
auto argv = BuildArgv(path, Args(), argStorage);
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
if (!workDir.empty()) {
posix_spawn_file_actions_addchdir_np(&actions, workDir.c_str());
}
pid_t pid = 0;
int rc = posix_spawn(&pid, path.c_str(), &actions, nullptr, argv.data(),
environ);
posix_spawn_file_actions_destroy(&actions);
if (rc != 0) {
return false;
}
std::thread([pid]() {
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {
}
}).detach();
return true;
}
#else
bool ProcessConfig::StartProcessDetached() const bool ProcessConfig::StartProcessDetached() const
{ {
return QProcess::startDetached(QString::fromStdString(Path()), Args(), return QProcess::startDetached(QString::fromStdString(Path()), Args(),
QString::fromStdString(WorkingDir())); QString::fromStdString(WorkingDir()));
} }
#endif
void ProcessConfig::ResolveVariables() void ProcessConfig::ResolveVariables()
{ {
@@ -162,139 +63,6 @@ void ProcessConfig::ResolveVariables()
_args.ResolveVariables(); _args.ResolveVariables();
} }
#ifdef __APPLE__
std::variant<int, ProcessConfig::ProcStartError>
ProcessConfig::StartProcessAndWait(int timeout)
{
ResetFinishedProcessData();
vblog(LOG_INFO, "run \"%s\" with a timeout of %d ms", Path().c_str(),
timeout);
int outPipe[2];
int errPipe[2];
if (pipe(outPipe) != 0 || pipe(errPipe) != 0) {
vblog(LOG_INFO, "failed to start \"%s\"!", Path().c_str());
return ProcStartError::FAILED_TO_START;
}
auto path = Path();
auto workDir = WorkingDir();
std::vector<std::string> argStorage;
auto argv = BuildArgv(path, Args(), argStorage);
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_addclose(&actions, outPipe[0]);
posix_spawn_file_actions_addclose(&actions, errPipe[0]);
posix_spawn_file_actions_adddup2(&actions, outPipe[1], STDOUT_FILENO);
posix_spawn_file_actions_adddup2(&actions, errPipe[1], STDERR_FILENO);
posix_spawn_file_actions_addclose(&actions, outPipe[1]);
posix_spawn_file_actions_addclose(&actions, errPipe[1]);
if (!workDir.empty()) {
posix_spawn_file_actions_addchdir_np(&actions, workDir.c_str());
}
pid_t pid = 0;
int rc = posix_spawn(&pid, path.c_str(), &actions, nullptr, argv.data(),
environ);
posix_spawn_file_actions_destroy(&actions);
close(outPipe[1]);
close(errPipe[1]);
if (rc != 0) {
close(outPipe[0]);
close(errPipe[0]);
vblog(LOG_INFO, "failed to start \"%s\"!", Path().c_str());
return ProcStartError::FAILED_TO_START;
}
SetProcessId(std::to_string(pid));
fcntl(outPipe[0], F_SETFL, O_NONBLOCK);
fcntl(errPipe[0], F_SETFL, O_NONBLOCK);
std::string outBuf;
std::string errBuf;
bool outDone = false;
bool errDone = false;
auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(timeout);
while (!outDone || !errDone) {
auto remaining =
std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now())
.count();
if (remaining <= 0) {
break;
}
struct pollfd fds[2];
int n = 0;
int outIdx = -1;
int errIdx = -1;
if (!outDone) {
fds[n] = {outPipe[0], POLLIN, 0};
outIdx = n++;
}
if (!errDone) {
fds[n] = {errPipe[0], POLLIN, 0};
errIdx = n++;
}
int pr = poll(fds, n, static_cast<int>(remaining));
if (pr < 0) {
if (errno == EINTR) {
continue;
}
break;
}
if (outIdx >= 0 && fds[outIdx].revents != 0) {
if (!DrainPipe(outPipe[0], outBuf)) {
outDone = true;
}
}
if (errIdx >= 0 && fds[errIdx].revents != 0) {
if (!DrainPipe(errPipe[0], errBuf)) {
errDone = true;
}
}
}
close(outPipe[0]);
close(errPipe[0]);
if (!outDone || !errDone) {
vblog(LOG_INFO,
"timeout while running \"%s\"\nAttempting to kill process!",
Path().c_str());
kill(pid, SIGKILL);
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {
}
_processOutputStream = TrimTrailingNewline(outBuf);
_processErrorStream = TrimTrailingNewline(errBuf);
return ProcStartError::TIMEOUT;
}
int status = 0;
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {
}
_processOutputStream = TrimTrailingNewline(outBuf);
_processErrorStream = TrimTrailingNewline(errBuf);
if (WIFEXITED(status)) {
int exitCode = WEXITSTATUS(status);
_processExitCode = std::to_string(exitCode);
return exitCode;
}
vblog(LOG_INFO, "process \"%s\" crashed!", Path().c_str());
return ProcStartError::CRASH;
}
#else
std::variant<int, ProcessConfig::ProcStartError> std::variant<int, ProcessConfig::ProcStartError>
ProcessConfig::StartProcessAndWait(int timeout) ProcessConfig::StartProcessAndWait(int timeout)
{ {
@@ -333,7 +101,6 @@ ProcessConfig::StartProcessAndWait(int timeout)
vblog(LOG_INFO, "process \"%s\" crashed!", Path().c_str()); vblog(LOG_INFO, "process \"%s\" crashed!", Path().c_str());
return ProcStartError::CRASH; return ProcStartError::CRASH;
} }
#endif
void ProcessConfig::SetFinishedProcessData(QProcess &process) void ProcessConfig::SetFinishedProcessData(QProcess &process)
{ {

View File

@@ -31,9 +31,14 @@ static bool setup()
static const auto showInvalidWarnings = [](void *) { static const auto showInvalidWarnings = [](void *) {
const auto invalidTokens = getInvalidTokens(); const auto invalidTokens = getInvalidTokens();
for (const auto &token : invalidTokens) { for (const auto &token : invalidTokens) {
QueueUITask([token]() { QueueUITask(
InvalidTokenDialog::ShowWarning(token); [](void *tokenPtr) {
}); auto tokenName = static_cast<QString *>(
tokenPtr);
InvalidTokenDialog::ShowWarning(
*tokenName);
},
(void *)&token);
} }
}; };

View File

@@ -40,7 +40,7 @@ std::string GetThemeTypeName()
return "Dark"; return "Dark";
} }
void QueueUITaskRaw(void (*task)(void *param), void *, bool) {} void QueueUITask(void (*task)(void *param), void *) {}
QWidget *GetSettingsWindow() QWidget *GetSettingsWindow()
{ {