Branch Output is an OBS Studio plugin (shared library / module) that adds an effect filter allowing individual sources or scenes to stream and/or record independently from the main OBS output. It is developed and maintained by OPENSPHERE Inc. under the GPLv2+ license.
- Repository language: C++ 17 with Qt 6 for UI
- Build system: CMake (≥ 3.16)
- Based on the official obs-plugintemplate
- Target OBS Studio version: ≥ 30.1.0 (Qt6, x64 / ARM64 / Apple Silicon)
branch-output/
├── CMakeLists.txt # Top-level CMake project definition
├── CMakePresets.json # CMake presets (windows-x64, macos, linux-x86_64, CI variants)
├── buildspec.json # Build specification (name, version, dependencies, UUIDs)
├── build.ps1 # Local Windows build script (RelWithDebInfo)
├── .clang-format # C++ code style (clang-format ≥ 16)
├── .cmake-format.json # CMake code style
├── .github/
│ ├── actions/ # Reusable format-check actions
│ └── workflows/ # CI workflows (build, format check, release)
├── cmake/ # CMake helpers & platform-specific modules
├── build-aux/ # Auxiliary build scripts (format runners)
├── data/
│ ├── locale/ # Translation files (en-US, ja-JP, zh-CN, ko-KR, etc.)
│ └── scripts/ # Sample Python scripts for OBS Script (file name override, etc.)
├── src/
│ ├── plugin-main.cpp # Plugin entry point, BranchOutputFilter core logic
│ ├── plugin-main.hpp # BranchOutputFilter class declaration
│ ├── plugin-streaming.cpp # Streaming output logic (individual start/stop, per-slot control)
│ ├── plugin-stream-recording.cpp # Stream recording output logic (individual start/stop)
│ ├── plugin-replay-buffer.cpp # Replay buffer output logic (individual start/stop)
│ ├── plugin-ui.cpp # OBS properties UI, filter settings UI
│ ├── plugin-support.h # Auto-generated plugin support header
│ ├── plugin-support.c.in # Template for plugin-support.c
│ ├── utils.cpp / .hpp # Utility functions, encoder helpers, profile helpers
│ ├── audio/
│ │ └── audio-capture.cpp / .hpp # Audio capture abstraction
│ ├── video/
│ │ ├── filter-video-capture.cpp / .hpp # Filter input video capture (GPU texrender proxy)
│ │ └── crop-rect-preview-renderer.cpp / .hpp # Crop rectangle preview overlay renderer
│ └── UI/
│ ├── output-status-dock.cpp / .hpp # Status dock widget
│ ├── resources.qrc # Qt resource file
│ └── images/ # Icons and images
└── release/ # Build output directory
| Platform | Toolchain |
|---|---|
| Windows | Visual Studio 17 2022, CMake ≥ 3.16, Qt 6 |
| macOS | Xcode ≤ 16.4.0 (macOS SDK ≤ 15.5), CMake ≥ 3.16, Qt 6 |
| Linux | GCC / Clang, CMake ≥ 3.16, Qt 6 |
Note (macOS): This project requires Xcode 16.4.0 (macOS 15.5 SDK) or below. Xcode 26 / macOS SDK 26 and later are currently unsupported due to compatibility constraints with OBS Studio's build dependencies.
OBS Studio sources and pre-built dependencies are fetched automatically via buildspec.json
(obs-studio 30.1.2, obs-deps, Qt6).
This project requires clang-format version 16 or later (as specified in .clang-format).
CI (GitHub Actions) enforces version 17.0.3 exactly via build-aux/.run-format.zsh.
| Platform | Install method |
|---|---|
| Windows | winget install -v 17.0.3 LLVM.LLVM or download from the LLVM 17.0.3 release page. Ensure clang-format is on your PATH. |
| macOS | brew install llvm@17 — then use $(brew --prefix llvm@17)/bin/clang-format or add it to PATH. |
| Linux | Install via package manager (e.g., apt install clang-format-17 on Ubuntu/Debian, or dnf install clang-tools-extra-17.0.3 on Fedora). The script also looks for clang-format-17 on PATH. |
Verify installation:
clang-format --version
# Expected: clang-format version 17.0.3 (...)To format all source files locally:
# PowerShell (Windows / cross-platform)
Get-ChildItem -Recurse -Path src -Include *.cpp,*.hpp,*.h,*.c | ForEach-Object { clang-format -i -style=file -fallback-style=none $_.FullName }# Bash / Zsh (macOS / Linux)
find src -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' -o -name '*.c' \) -exec clang-format -i -style=file -fallback-style=none {} +This project requires cmake-format version 0.6.13 or later. Configuration is in .cmake-format.json.
CI (GitHub Actions) enforces this version via build-aux/.run-format.zsh.
Install via pip:
pip install cmake-format>=0.6.13Verify installation:
cmake-format --version
# Expected: 0.6.13 or laterTo format all CMake files locally:
# PowerShell (Windows / cross-platform)
Get-ChildItem -Recurse -Include CMakeLists.txt,*.cmake | Where-Object { $_.FullName -notmatch 'build_' } | ForEach-Object { cmake-format -i $_.FullName }# Bash / Zsh (macOS / Linux)
find . -type f \( -name 'CMakeLists.txt' -o -name '*.cmake' \) -not -path '*/build_*/*' -exec cmake-format -i {} +# Configure + Build + Install
.\build.ps1
# With Inno Setup installer
.\build.ps1 -installerOr manually:
cmake --fresh -S . -B build_x64 -G "Visual Studio 17 2022" -A x64
cmake --build build_x64 --config RelWithDebInfo
cmake --install build_x64 --prefix release/Package --config RelWithDebInfoUse cmake --preset <name> with one of:
windows-x64/windows-ci-x64macos/macos-cilinux-x86_64/linux-ci-x86_64
The plugin registers an OBS effect filter via obs_source_info (filter ID: osi_branch_output).
The main class is BranchOutputFilter (declared in plugin-main.hpp), which is a QObject subclass.
| Area | Description |
|---|---|
| Streaming | Creates up to MAX_SERVICES (8) independent streaming outputs with dedicated services, encoders, and reconnect logic. |
| Recording | Supports file recording with various container formats, time/size-based splitting, pause/unpause, and chapter markers. |
| Replay Buffer | Supports replay buffer output (implemented in plugin-replay-buffer.cpp). Allows saving the last N seconds of encoded output to file on demand via hotkey or UI button. |
| Audio | Manages up to MAX_AUDIO_MIXES audio contexts via AudioCapture class. Supports filter audio, per-source audio, and audio track selection. |
| Video | Creates an OBS view (obs_view_t) with a private video output for per-filter encoding and resolution control. Supports filter input mode via FilterVideoCapture class, which captures the filter's input using GPU gs_texrender and provides a private proxy source for the obs_view, avoiding CPU roundtrips and enabling GPU encoder compatibility (NVENC, QSV, AMF, etc.). |
| UI | Properties panel built via OBS properties API (plugin-ui.cpp). Status dock (BranchOutputStatusDock) shows live statistics for all filters, including replay buffer save buttons. |
| Hotkeys | Registers hotkey pairs for enable/disable, split recording, pause/unpause, chapter markers, save replay buffer, and per-output enable/disable (streaming per-slot, recording, replay buffer). |
| Interlock | Can link filter activation to OBS streaming, recording, virtual camera, replay buffer, or individual (per-output-type) states. The "Individual" mode maps each Branch Output type to its OBS counterpart independently. |
| Individual Start/Stop | Allows streaming, recording, and replay buffer to be started/stopped independently via ensureInfrastructure() / releaseInfrastructureIfIdle() to separate shared resource lifecycle from individual output lifecycle. Per-output user intent is tracked via atomic flags (streamingUserEnabled[], recordingUserEnabled, replayBufferUserEnabled). |
| Cropping | Supports relative (margin) and absolute (region) video cropping with CropRect struct. Live preview via CropRectPreviewRenderer. |
| Blanking | Uses a private solid-color source to blank output when the parent source is inactive. |
- OBS callbacks (video render, audio filter, video tick) may run on different threads from the UI thread.
- Three recursive mutexes (
pluginMutex,outputMutex,audioMutex) initialized viapthread_mutex_init_recursive(). Lock ordering:pluginMutex→outputMutex. audioMutexprotects audio capture pointers against concurrent release inreleaseInfrastructureIfIdle().QMutexprotects audio buffers inAudioCapture.- Atomic fields (
std::atomic<bool>foroutputStarting,streamingUserEnabled[],recordingUserEnabled,replayBufferUserEnabled;std::atomic<uint64_t>forreconnectAttemptingAt) eliminate data races from OBS signal callbacks. - UI updates use
QMetaObject::invokeMethodwithQt::QueuedConnectionfor thread safety. - Settings changes are tracked via revision counters (
storedSettingsRev/activeSettingsRev) to defer restarts.
The plugin heavily uses:
obs-module.h— Module registration, locale, config pathsobs-frontend-api.h— Profile config, scene enumeration, frontend eventsobs.hpp— OBS RAII wrappers (OBSSourceAutoRelease,OBSEncoderAutoRelease,OBSOutputAutoRelease, etc.)util/deque.h,util/threading.h,util/platform.h— Low-level utilities
- Standard: C++17
- Column limit: 120
- Indent: 4 spaces (tabs not used)
- Brace style: Custom — functions use next-line braces; control statements use same-line braces
- clang-format version: ≥ 16 required
- Run:
clang-format -i src/**/*.cpp src/**/*.hpp
- Line width: 120
- Tab size: 2
- Config:
.cmake-format.json
Format is checked in CI via .github/workflows/check-format.yaml using reusable actions
(run-clang-format, run-cmake-format). PRs to master/main/dev and pushes to master/dev are validated.
- Follow OBS plugin conventions — Use
obs_log()for logging,OBS_DECLARE_MODULE()for entry, locale viaobs_module_text(). - RAII wrappers — Always use
OBSSourceAutoRelease,OBSDataAutoRelease, etc. instead of manualobs_*_release(). - Thread safety — Any data shared between OBS callbacks and UI must be guarded by mutex. Use
QMetaObject::invokeMethodwithQt::QueuedConnectionwhen calling UI methods from non-UI threads. - Settings migration — When changing settings schema, add backward-compatible migration code in the constructor (see
audio_sourcemigration example inBranchOutputFilterconstructor). - Qt MOC — Classes using
Q_OBJECTmust be in headers.AUTOMOC,AUTOUIC,AUTORCCare enabled.
- Classes: PascalCase (
BranchOutputFilter,AudioCapture,FilterVideoCapture,CropRectPreviewRenderer,OutputCell,OutputTableRow) - Methods: camelCase (
startOutput,stopRecordingOutput,onIntervalTimerTimeout) - Constants/Macros: UPPER_SNAKE_CASE (
MAX_SERVICES,FILTER_ID,OUTPUT_MAX_RETRIES) - Member variables: camelCase, no prefix (
filterSource,videoEncoder,recordingActive) - Static callbacks: camelCase with descriptive prefix (
onEnableFilterHotkeyPressed,audioFilterCallback)
- Avoid multi-line ternary expressions — clang-format cannot reliably format nested or multi-line ternary operators (
a ? b : c) in a readable way. Useif/else if/elsestatements instead when the expression would span multiple lines.
- Use
obs_log(LOG_ERROR, ...)for errors,LOG_WARNINGfor warnings,LOG_INFOfor lifecycle events,LOG_DEBUGfor verbose tracing. - Check return values from OBS API calls; handle gracefully (do not crash OBS).
- Never throw C++ exceptions across OBS API boundaries.
- All user-facing strings must use
obs_module_text("Key")or theQTStr("Key")helper. - Add new keys to
data/locale/en-US.ini(primary) and ideally toja-JP.ini. - Locale files use INI format:
Key="Value".
- There are no automated unit tests in this repository currently.
- Manual testing is required: load the plugin in OBS Studio, add the "Branch Output" filter, and verify streaming/recording behavior.
- Test on all supported platforms when possible (Windows x64, macOS, Linux).
- Verify Studio Mode compatibility (Branch Output ignores studio mode's program and outputs from preview).
- Verify that enabling and then disabling the Branch Output filter does not cause a crash.
- Verify that shutting down OBS does not crash when the Branch Output filter is inactive or active, respectively.
- Verify that switching scene collections does not crash when the Branch Output filter is inactive or active, respectively.
- Verify that no memory leaks are logged on OBS shutdown.
- Verify that mutex does not cause deadlocks.
- Verify that blanking mode does not leak video or audio unintentionally — when the source is not visible in the main output (Program) and blanking is enabled, the Branch Output must emit a black frame (no source imagery). If audio muting is also enabled, all audio tracks (including master track) must be silenced.
- Verify that Individual interlock mode correctly starts/stops each output type independently following its OBS counterpart.
- Verify that per-output checkboxes in the Status Dock correctly enable/disable individual outputs in all interlock modes.
- Verify that per-output hotkeys toggle the correct output and that the Status Dock checkboxes sync immediately.
- Verify that video cropping (both relative and absolute modes) produces the correct output resolution and content.
- Verify that the crop preview rectangle displays correctly and is hidden when the properties dialog closes.
| Workflow | Trigger | Purpose |
|---|---|---|
push.yaml |
Push to master/main/dev/release/**, tags |
Format check + build + release creation |
build-project.yaml |
Called by push/PR workflows | Multi-platform build (Windows, macOS, Linux) |
check-format.yaml |
Called by push/PR workflows | clang-format and cmake-format validation |
pr-pull.yaml |
Pull requests to master/main/dev |
Format check + build validation |
dispatch.yaml |
Manual dispatch | On-demand builds |
Release tags follow semver: X.Y.Z for stable, X.Y.Z-beta/X.Y.Z-rc for pre-releases.
- Add the setting key to
BranchOutputFilter::getDefaults()inplugin-ui.cpp. - Add UI controls in
plugin-ui.cpp(use OBS properties API:obs_properties_add_*). - Handle the setting in
startOutput()/stopOutput()/updateCallback()as needed. - Add locale strings to all locale files under
data/locale/(en-US.ini,ja-JP.ini,zh-CN.ini,ko-KR.ini,de-DE.ini,fr-FR.ini,ca-ES.ini,ro-RO.ini,ru-RU.ini,uk-UA.ini).en-US.iniis the primary (required); all others should also be updated. - Ensure backward compatibility — existing saved settings must still load correctly.
- The plugin supports up to
MAX_SERVICES(8) service slots. - Each slot has its own
BranchOutputStreamingContextwith output, service, and signals. - Use
getIndexedPropNameFormat()for indexed property names.
- Audio capture is abstracted through
AudioCaptureclass insrc/audio/. - The class manages an audio buffer, supports push/pop patterns, and handles format conversion.
- Audio mixers and encoder binding are managed in
BranchOutputFilter::startOutput().
BranchOutputStatusDockinsrc/UI/is aQFrame-based dock widget.- It uses a
QTableWidgetwith custom cell classes (OutputTableCellItem,LabelCell,FilterCell,OutputCell). OutputCellprovides per-output enable/disable checkboxes (eye icon) and folder-open click for recording/replay buffer rows.- The
outputUserEnabledChangedsignal provides immediate checkbox sync when state changes from hotkeys or other sources. - Thread-safe updates via
QMetaObject::invokeMethod.
- Replay buffer logic is implemented in
src/plugin-replay-buffer.cpp. BranchOutputFiltermanages replay buffer lifecycle (createAndStartReplayBuffer,stopReplayBufferOutput,saveReplayBuffer), plus individual start/stop (startReplayBufferIndividual,stopReplayBufferIndividual).- Settings creation is handled by
createReplayBufferSettings(). - The status dock includes a save button per replay buffer row and a global "Save All Replay Buffers" button.
- Replay buffer can be linked to filter activation via
INTERLOCK_TYPE_REPLAY_BUFFERorINTERLOCK_TYPE_INDIVIDUAL.
FilterVideoCaptureinsrc/video/captures the filter's input via GPUgs_texrender.- It provides a private proxy source (
PROXY_SOURCE_ID: osi_branch_output_proxy) forobs_viewbinding. - The proxy source renders the captured texrender texture directly on the GPU, avoiding CPU roundtrips and enabling GPU encoder compatibility.
- Lifecycle: created/destroyed in
startOutput()/stopOutput()when filter input mode (useFilterInput) is enabled. - Key methods:
captureFilterInput()(called fromvideo_render),renderTexture()(called from proxy source'svideo_render),drawCapturedTexture()(passthrough to main output).
- Cropping is configured via settings (
crop_type,crop_relative_*,crop_absolute_*) inplugin-ui.cpp. CropRectstruct (defined inutils.hpp) holds resolved crop parameters.calculateCrop()inplugin-main.cppresolves relative/absolute crop settings into aCropRect.determineOutputResolution()accounts for cropping when computing the final output resolution.CropRectPreviewRendererinsrc/video/draws a live preview rectangle overlay in the filter'svideo_rendercallback. The preview is transient (not saved to settings) and automatically hidden when the properties dialog is closed.
- Streaming logic is implemented in
src/plugin-streaming.cpp. - Supports up to
MAX_SERVICES(8) independent streaming slots viaBranchOutputStreamingContext. - Individual start/stop per slot via
startSingleStreamingIndividual()/stopSingleStreamingIndividual(), and batch viastartStreamingIndividual()/stopStreamingIndividual(). - Per-slot user intent flags (
streamingUserEnabled[]) are atomic and persisted viasaveCallback.
- Stream recording logic is implemented in
src/plugin-stream-recording.cpp. - Supports individual start/stop via
startRecordingIndividual()/stopRecordingIndividual(). - Proc handlers for file name format override are registered here (
override_recording_file_name_format,clear_recording_file_name_format_override).
- Do NOT call
obs_filter_get_parent()in theBranchOutputFilterconstructor — it returnsnullptrat that point. UseaddCallback()instead. - Private sources (not visible in frontend) are intentionally excluded from status dock and timer registration.
- Settings revisions (
storedSettingsRev/activeSettingsRev) exist to avoid stopping output during reconnect attempts. Do not bypass this mechanism. - Encoder compatibility — The plugin maps "simple" encoder names to actual encoder IDs, with version-specific fallbacks (OBS 30 vs OBS 31). See
getSimpleVideoEncoder()inutils.hpp. - Memory management — Use OBS RAII wrappers. Raw
bfree()/obs_data_release()calls are error-prone. .gitignoreuses allowlist pattern — New top-level files/directories must be explicitly un-ignored with!prefix.- FilterVideoCapture proxy source — The proxy source type (
osi_branch_output_proxy) must be registered at module load viaFilterVideoCapture::createProxySourceInfo(). The proxy source is private and intentionally not visible in the OBS frontend.
- Agents must respond in the same language the user is using (e.g., reply in Japanese if the user writes in Japanese, in English if the user writes in English).
- The team leader must focus exclusively on orchestrating teammates.
-
Team size should be 3–5 members, selected from the cast below based on the task.
-
Each teammate must work on different files to avoid edit conflicts.
-
Start with research and review, then launch the team for parallel execution.
-
Do not use subagents for tasks that can be handled by agent teams.
Exception: Use subagents for parallel reviews.
Expert in C++ language specifications and Windows/macOS/Linux native application development.
- C++ implementation work
- C++ coding advice
- Code review from the perspective of C++ language specifications and coding standards
- Multithreading implementation and thread safety advice
Expert in OBS Studio internals, the OBS Studio API, and OBS Studio plugins.
- OBS Studio specification advice
- OBS Studio API selection
- OBS Studio plugin specification advice
Expert in Qt framework, GUI application design, implementation, and testing.
- Qt specification advice
- Qt API selection
- Qt GUI construction advice
- Qt object design advice
Expert in network programming, TCP/IP, HTTP, SSL/TLS, WebSocket, socket communication, and streaming protocols such as RTMP/SRT/WebRTC.
- Network programming advice
- Network protocol implementation advice
- Streaming protocol implementation advice
- Security advice
- Network code review
Multilingual translator.
- Locale INI file translation
- Document translation
Expert in video, audio, and streaming technologies, media quality, video processing, audio processing, encoders, and broadcast operations.
- Technical advice on video, audio, and streaming
- Quality advice on video, audio, and streaming
- Encoder configuration advice
- Broadcast operations advice
Expert in CI/CD (GitHub Actions), CMake, clang-format, VS Code, Inno Setup, and other development environment and build process tooling.
- GitHub Actions workflow editing and review
- CMake build script editing and review
- Inno Setup build script editing and review
Expert in Python scripting, OBS Studio Script design, implementation, and testing.
- Python implementation work
- Python coding advice
- Code review from the perspective of Python language specifications and coding standards