Skip to content

feat: Add individual start/stop control for streaming, recording, and replay buffer#130

Merged
hanatyan128 merged 18 commits into
devfrom
feat-output-individual-start-stop
Apr 5, 2026
Merged

feat: Add individual start/stop control for streaming, recording, and replay buffer#130
hanatyan128 merged 18 commits into
devfrom
feat-output-individual-start-stop

Conversation

@hanatyan128

@hanatyan128 hanatyan128 commented Mar 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Add per-output individual start/stop methods (startStreamingIndividual, startRecordingIndividual, startReplayBufferIndividual and their stop counterparts)
  • Add INTERLOCK_TYPE_INDIVIDUAL mode that maps OBS streaming/recording/replay buffer states to their Branch Output counterparts independently
  • Add OutputCell widget to status dock with per-output enable/disable checkbox (works in all interlock modes), preserving folder-open click for recording/replay buffer rows
  • Refactor ensureInfrastructure() / releaseInfrastructureIfIdle() to separate shared resource lifecycle from individual output lifecycle, allowing outputs to start/stop independently while sharing the same view, encoders, and audio context

Thread Safety Improvements

  • Add audioMutex to protect audio capture pointers in audioFilterCallback against concurrent release in releaseInfrastructureIfIdle
  • Switch all three mutexes (pluginMutex, outputMutex, audioMutex) to PTHREAD_MUTEX_RECURSIVE via OBS's pthread_mutex_init_recursive(), eliminating the need for separate *Locked() method variants
  • Make outputStarting (std::atomic<bool>) and reconnectAttemptingAt (std::atomic<uint64_t>) atomic to eliminate data races from OBS signal callbacks that write these fields on core threads
  • Add default member initializers to BranchOutputStreamingContext and change createSreamingOutput() to populate streamings[] in-place, removing move constructor/assignment requirements
  • Add early-return guard in startOutput() when all user-enabled flags are false, preventing infrastructure rebuild loops in ALWAYS_ON mode
  • Unify lock ordering in stopOutputGracefully() to match pluginMutex -> outputMutex convention used elsewhere

User-Facing Changes

  • Per-output user intent flags (streamingUserEnabled[], recordingUserEnabled, replayBufferUserEnabled) are persisted via saveCallback with backward-compatible migration from the legacy streaming_output_enabled key
  • New locale keys added to all 10 locale files: Individual, StreamingToggleTooltip, RecordingToggleTooltip, ReplayBufferToggleTooltip

Files Changed (18 files, +1211 / -490)

  • src/plugin-main.cpp/hpp — Core logic: infrastructure separation, recursive mutexes, individual start/stop orchestration in onIntervalTimerTimeout, save/load of user intent flags
  • src/plugin-streaming.cppstartStreamingIndividual, stopStreamingIndividual, startSingleStreamingIndividual, stopSingleStreamingIndividual, atomic fields, in-place output creation
  • src/plugin-stream-recording.cppstartRecordingIndividual, stopRecordingIndividual
  • src/plugin-replay-buffer.cppstartReplayBufferIndividual, stopReplayBufferIndividual
  • src/plugin-ui.cppINTERLOCK_TYPE_INDIVIDUAL option in interlock dropdown
  • src/UI/output-status-dock.cpp/hppOutputCell class (unified from former RecordingOutputCell/ReplayBufferOutputCell), per-output checkbox sync
  • data/locale/*.ini — New locale keys for all 10 languages

Test Plan

  • Verify INTERLOCK_TYPE_INDIVIDUAL mode: OBS streaming start → only Branch Output streaming starts; OBS recording start → only Branch Output recording starts
  • Verify per-output checkboxes in status dock work in all interlock modes (ALWAYS_ON, Streaming, Recording, etc.)
  • Verify unchecking all outputs in ALWAYS_ON mode does not cause infrastructure rebuild loop
  • Verify enable/disable filter (eye icon) does not crash with individual stopping in progress
  • Verify OBS shutdown does not crash with active individual outputs
  • Verify scene collection switch does not crash
  • Verify no memory leaks on OBS shutdown
  • Test on Windows x64 (primary), macOS and Linux if possible

🤖 Generated with Claude Code

@hanatyan128 hanatyan128 self-assigned this Mar 30, 2026
@hanatyan128 hanatyan128 added this to the 1.1.0 milestone Mar 30, 2026
@hanatyan128 hanatyan128 mentioned this pull request Mar 30, 2026
@hanatyan128
hanatyan128 force-pushed the feat-output-individual-start-stop branch from 2f82f4c to 2942488 Compare April 4, 2026 05:04
hanatyan128 and others added 12 commits April 5, 2026 02:56
… replay buffer

- Refactor startOutput/stopOutput into ensureInfrastructure/releaseInfrastructureIfIdle
  to support shared encoder lifecycle across independently managed outputs
- Add per-output start/stop methods (startStreamingIndividual, stopRecordingIndividual, etc.)
  with internal helpers to eliminate code duplication
- Add INTERLOCK_TYPE_INDIVIDUAL mode that maps OBS streaming/recording/replay buffer
  states to their Branch Output counterparts independently
- Add OutputCell widget to status dock with per-output enable/disable checkbox
  (works in all interlock modes), preserving folder-open click for recording/replay
- Persist per-output enable/disable state in filter settings via save callback
- Add audioMutex to protect audio capture pointers in audioFilterCallback against
  concurrent release in releaseInfrastructureIfIdle
- Add "Individual" locale key to all 10 locale files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix critical resource leak in ensureInfrastructure() on partial failure,
add Individual mode additional output start logic when some outputs are
already active, reset streamingIndividualStopping flag in stopOutput(),
make user-enabled flags thread-safe with std::atomic<bool>, remove unused
RecordingOutputCell/ReplayBufferOutputCell classes, hide checkbox for
ROW_OUTPUT_NONE, improve lock contract documentation, and update Japanese
translation from "インディビデュアル" to "個別連動".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…start

- Change createAndStartStreamingOutputs, createAndStartRecordingOutputChecked,
  createAndStartReplayBufferChecked to return bool (true if output is active)
- Release infrastructure in start*Individual() only when no output started
- Return true when output is already active to avoid false release
- Add filter setting check (isStreamingGroupEnabled, isRecordingEnabled,
  isReplayBufferEnabled) to Individual mode idle path to prevent unconfigured
  output types from blocking subsequent outputs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s dock

Add RecordingToggleTooltip and ReplayBufferToggleTooltip locale keys
to all 10 locale files and set tooltips on the corresponding
checkboxes in the output status dock for consistency with streaming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Separate INDIVIDUAL stop conditions from user toggle (per-output
  checkbox) to clarify responsibility boundaries
- Move extern pluginMutex declaration to plugin-main.hpp to eliminate
  duplicate declarations and type mismatch risk
- Use getter methods in saveCallback for consistent memory_order_relaxed
  access on atomic user-intent flags
- Fix stopStreamingOutputsGracefully() comment to accurately reflect
  that only outputMutex is required
- Add audioMutex lock in setAudioCapturesActive() to prevent race
  condition with releaseInfrastructureIfIdle()
- Release infrastructure only when all outputs fail to start in
  startOutput(), making the cleanup intent explicit
- Add ensureInfrastructure() precondition to helper function doc comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace single streamingUserEnabled flag with per-stream array so each
streaming slot can be independently toggled ON/OFF from the status dock.
Previously all streaming checkboxes controlled a shared flag; now each
checkbox controls its own slot with graceful stop support for
reconnecting streams. Includes backward-compatible settings migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hing, and safety

- Make locking consistent across all individual start/stop functions
  (pluginMutex -> outputMutex order) for streaming, recording, and
  replay buffer
- Document lock ordering (pluginMutex -> outputMutex -> audioMutex)
  in plugin-main.hpp
- Batch individual start/stop operations per tick instead of
  one-output-per-tick serialization, treating streaming slots as
  a single output group
- Change individual start/stop functions to return bool indicating
  whether an operation was performed
- Extract startEligibleStreamings lambda to deduplicate streaming
  start logic with streamingIndividualStopping guard
- Move streamingIndividualStopping retry after per-output toggle
  stops so recording/replay buffer stops are not blocked during
  streaming graceful stop
- Add checkbox update guard in OutputTableRow::update() to skip
  unnecessary setChecked() calls
- Add null-check in createAndStartStreamingOutputs() start loop
- Use OBSWeakSourceAutoRelease in OutputCell for lifetime safety

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Read recordingActive/recordingPending and replayBufferActive under
pluginMutex+outputMutex to eliminate data race on non-atomic booleans
in stopRecordingIndividual() and stopReplayBufferIndividual().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch all three mutexes (pluginMutex, outputMutex, audioMutex) to
PTHREAD_MUTEX_RECURSIVE via OBS's pthread_mutex_init_recursive() helper.
This allows the same thread to re-lock a mutex it already holds, removing
the need for separate stopRecordingOutputLocked() and
stopReplayBufferOutputLocked() methods. Callers no longer need to track
whether they already hold outputMutex before calling stop functions.

Also includes review-driven fixes: typo correction, unused function
removal, return-value comment, user-flag guard in startOutput(), and
lock ordering unification in stopOutputGracefully().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanatyan128
hanatyan128 force-pushed the feat-output-individual-start-stop branch from dd23d58 to 4dbefec Compare April 4, 2026 17:56
hanatyan128 and others added 6 commits April 5, 2026 03:49
- Change outputStarting to std::atomic<bool> and reconnectAttemptingAt
  to std::atomic<uint64_t> to eliminate data races from OBS signal
  callbacks (starting, activate, reconnect, stop) that write these
  fields on OBS core threads.
- Add default member initializers to BranchOutputStreamingContext,
  removing the explicit zero-init loop in the constructor.
- Change createSreamingOutput() to populate streamings[index] in-place
  and return bool, eliminating the need for move constructor/assignment
  operator on the context struct.
- Cache atomic load in reconnectAttemptingTimedOut() to prevent
  inconsistent reads between the two uses of the value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
isAnyStreamingUserEnabled() checked all MAX_SERVICES slots, but
unconfigured slots default to enabled and are not visible in the
status dock, so users cannot toggle them off. This caused ALWAYS_ON
mode to rebuild infrastructure every tick.

Add isAnyStreamingUserEnabled(settings) overload that only checks
slots within service_count that are actually configured. Move all
streaming user-enabled accessors from header inline to
plugin-streaming.cpp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y_buffer in loadRecently

The legacy streaming_output_enabled key (without index) was for
backward compatibility with an unreleased version, so migration is
unnecessary. Remove it from constructor loading, saveCallback, and
getDefaults. Only indexed streaming_output_enabled_N keys are used.

Also erase replay_buffer from recently settings so new filters do
not inherit the replay buffer enabled state from previous filters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nctions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hanatyan128
hanatyan128 merged commit 367017c into dev Apr 5, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to start/stop streaming/stream recording/replay buffer individually

1 participant