Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/locale/en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,7 @@ AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not avail
AdvSceneSwitcher.action.playAudio.tracks="Tracks"
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
AdvSceneSwitcher.action.playAudio.mono="Mono"
AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording"
Expand Down
98 changes: 77 additions & 21 deletions plugins/base/macro-action-play-audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,64 @@
#include "audio-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "sync-helpers.hpp"

#include <obs-frontend-api.h>

#include <chrono>
#include <mutex>
#include <set>
#include <QFileInfo>
#include <QLabel>

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;

// 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";

bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
Expand All @@ -23,6 +69,7 @@ bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(

static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
{
obs_source_media_stop(source);
if (wantsOutput) {
obs_set_output_source(kPlaybackOutputChannel, nullptr);
} else {
Expand All @@ -38,8 +85,7 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
std::unique_lock<std::mutex> lock(*GetMutex());
SetMacroAbortWait(false);

// The media source needs time to open and decode before reaching
// PLAYING state. Poll until it starts (or the macro is stopped).
// Poll until the source starts playing or the macro is stopped.
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (obs_source_media_get_state(source) ==
OBS_MEDIA_STATE_PLAYING) {
Expand All @@ -48,9 +94,8 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
GetMacroWaitCV().wait_for(lock, 10ms);
}

// Now wait for playback to end. Require two consecutive non-playing
// samples to avoid false positives on brief state transitions.
// If maxMs > 0, also stop once that many milliseconds have elapsed.
// Wait for playback to end. Two consecutive non-playing samples are
// required to avoid false positives on brief state transitions.
const auto playbackStart = std::chrono::steady_clock::now();
static const int kStopThreshold = 2;
int stoppedCount = 0;
Expand Down Expand Up @@ -95,7 +140,6 @@ bool MacroActionPlayAudio::PerformAction()
obs_data_set_string(settings, "local_file", path.c_str());
obs_data_set_bool(settings, "is_local_file", true);
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, "close_when_inactive", true);

Expand All @@ -113,10 +157,11 @@ bool MacroActionPlayAudio::PerformAction()
DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
obs_source_set_volume(source, vol);
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 —
// there is no point routing through the output channel if the mixer
// mask would silence every track.
// Fall back to monitor-only if all output tracks are deselected.
const bool wantsOutput =
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
(_audioMixers != 0);
Expand All @@ -128,10 +173,8 @@ bool MacroActionPlayAudio::PerformAction()

if (wantsOutput) {
obs_source_set_audio_mixers(source, _audioMixers);
// Route through a private scene so we can position the scene
// item far off-screen. This keeps the item "visible" (so audio
// is still mixed) while ensuring its video never intersects the
// output frame.
// Route through a private scene positioned off-screen so the
// audio mixes into the output without video appearing on screen.
OBSSceneAutoRelease audioScene =
obs_scene_create_private("advss_audio_scene");
obs_sceneitem_t *item = obs_scene_add(audioScene, source);
Expand All @@ -141,8 +184,7 @@ bool MacroActionPlayAudio::PerformAction()
}
obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene));
// audioScene released here; the output channel holds the
// remaining reference and keeps the scene alive.
// audioScene is released here; the output channel keeps the scene alive.
} else {
obs_source_set_audio_mixers(source, 0);
obs_source_inc_active(source);
Expand All @@ -167,15 +209,15 @@ bool MacroActionPlayAudio::PerformAction()
return true;
}

// 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.
// Keep the source alive until playback ends via a background thread.
auto rawSource = obs_source_get_ref(source);
registerActiveSource(rawSource);
auto macro = GetMacro();

std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
waitForPlaybackToEnd(macro, rawSource, maxMs);
deactivatePlayback(rawSource, wantsOutput);
unregisterActiveSource(rawSource);
obs_source_release(rawSource);
});
AddMacroHelperThread(macro, std::move(cleanupThread));
Expand Down Expand Up @@ -204,6 +246,7 @@ bool MacroActionPlayAudio::Save(obs_data_t *obj) const
obs_data_set_bool(obj, "useDuration", _useDuration);
_playbackDuration.Save(obj, "playbackDuration");
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
obs_data_set_bool(obj, "mono", _mono);
return true;
}

Expand All @@ -221,6 +264,7 @@ bool MacroActionPlayAudio::Load(obs_data_t *obj)
_useDuration = obs_data_get_bool(obj, "useDuration");
_playbackDuration.Load(obj, "playbackDuration");
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
_mono = obs_data_get_bool(obj, "mono");
return true;
}

Expand Down Expand Up @@ -262,7 +306,9 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
_useDuration(new QCheckBox(this)),
_playbackDuration(new DurationSelection(this, true, 0.0)),
_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->setMaximum(0.0);
Expand Down Expand Up @@ -303,6 +349,8 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
SLOT(PlaybackDurationChanged(const Duration &)));
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
SLOT(WaitChanged(int)));
QWidget::connect(_mono, SIGNAL(stateChanged(int)), this,
SLOT(MonoChanged(int)));

auto tracksLayout = new QHBoxLayout;
tracksLayout->setContentsMargins(0, 0, 0, 0);
Expand Down Expand Up @@ -346,6 +394,7 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
mainLayout->addLayout(startOffsetLayout);
mainLayout->addLayout(playbackDurationLayout);
mainLayout->addWidget(_waitForCompletion);
mainLayout->addWidget(_mono);
setLayout(mainLayout);

_entryData = entryData;
Expand Down Expand Up @@ -375,6 +424,7 @@ void MacroActionPlayAudioEdit::UpdateEntryData()
_playbackDuration->SetDuration(_entryData->_playbackDuration);
_playbackDuration->setEnabled(_entryData->_useDuration);
_waitForCompletion->setChecked(_entryData->_waitForCompletion);
_mono->setChecked(_entryData->_mono);
}

void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
Expand Down Expand Up @@ -443,4 +493,10 @@ void MacroActionPlayAudioEdit::WaitChanged(int value)
_entryData->_waitForCompletion = value;
}

void MacroActionPlayAudioEdit::MonoChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_mono = value;
}

} // namespace advss
3 changes: 3 additions & 0 deletions plugins/base/macro-action-play-audio.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class MacroActionPlayAudio : public MacroAction {
bool _useDuration = false;
Duration _playbackDuration;
bool _waitForCompletion = false;
bool _mono = false;

static bool _registered;
static const std::string id;
Expand Down Expand Up @@ -68,6 +69,7 @@ private slots:
void UseDurationChanged(int);
void PlaybackDurationChanged(const Duration &);
void WaitChanged(int value);
void MonoChanged(int value);

signals:
void HeaderInfoChanged(const QString &);
Expand All @@ -83,6 +85,7 @@ private slots:
QCheckBox *_useDuration;
DurationSelection *_playbackDuration;
QCheckBox *_waitForCompletion;
QCheckBox *_mono;

std::shared_ptr<MacroActionPlayAudio> _entryData;
bool _loading = true;
Expand Down
Loading