|
| 1 | +# AGENTS.md — Branch Output Plugin for OBS Studio |
| 2 | + |
| 3 | +## Project Overview |
| 4 | + |
| 5 | +**Branch Output** is an OBS Studio plugin (shared library / module) that adds an effect filter allowing |
| 6 | +individual sources or scenes to stream and/or record independently from the main OBS output. |
| 7 | +It is developed and maintained by **OPENSPHERE Inc.** under the GPLv2+ license. |
| 8 | + |
| 9 | +- Repository language: **C++ 17** with **Qt 6** for UI |
| 10 | +- Build system: **CMake** (≥ 3.16) |
| 11 | +- Based on the official [obs-plugintemplate](https://github.com/obsproject/obs-plugintemplate) |
| 12 | +- Target OBS Studio version: **≥ 30.1.0** (Qt6, x64 / ARM64 / Apple Silicon) |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Repository Layout |
| 17 | + |
| 18 | +``` |
| 19 | +branch-output/ |
| 20 | +├── CMakeLists.txt # Top-level CMake project definition |
| 21 | +├── CMakePresets.json # CMake presets (windows-x64, macos, linux-x86_64, CI variants) |
| 22 | +├── buildspec.json # Build specification (name, version, dependencies, UUIDs) |
| 23 | +├── build.ps1 # Local Windows build script (RelWithDebInfo) |
| 24 | +├── .clang-format # C++ code style (clang-format ≥ 16) |
| 25 | +├── .cmake-format.json # CMake code style |
| 26 | +├── .github/ |
| 27 | +│ ├── actions/ # Reusable format-check actions |
| 28 | +│ └── workflows/ # CI workflows (build, format check, release) |
| 29 | +├── cmake/ # CMake helpers & platform-specific modules |
| 30 | +├── build-aux/ # Auxiliary build scripts (format runners) |
| 31 | +├── data/ |
| 32 | +│ └── locale/ # Translation files (en-US, ja-JP, zh-CN, ko-KR, etc.) |
| 33 | +├── src/ |
| 34 | +│ ├── plugin-main.cpp # Plugin entry point, BranchOutputFilter implementation |
| 35 | +│ ├── plugin-main.hpp # BranchOutputFilter class declaration |
| 36 | +│ ├── plugin-ui.cpp # OBS properties UI, filter settings UI |
| 37 | +│ ├── plugin-support.h # Auto-generated plugin support header |
| 38 | +│ ├── plugin-support.c.in # Template for plugin-support.c |
| 39 | +│ ├── utils.cpp / .hpp # Utility functions, encoder helpers, profile helpers |
| 40 | +│ ├── audio/ |
| 41 | +│ │ ├── audio-capture.cpp / .hpp # Audio capture abstraction |
| 42 | +│ └── UI/ |
| 43 | +│ ├── output-status-dock.cpp / .hpp # Status dock widget |
| 44 | +│ ├── resources.qrc # Qt resource file |
| 45 | +│ └── images/ # Icons and images |
| 46 | +└── release/ # Build output directory |
| 47 | +``` |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Build Instructions |
| 52 | + |
| 53 | +### Prerequisites |
| 54 | + |
| 55 | +| Platform | Toolchain | |
| 56 | +|----------|-----------| |
| 57 | +| Windows | Visual Studio 17 2022, CMake ≥ 3.16, Qt 6 | |
| 58 | +| macOS | Xcode 15+, CMake ≥ 3.16, Qt 6 | |
| 59 | +| Linux | GCC / Clang, CMake ≥ 3.16, Qt 6 | |
| 60 | + |
| 61 | +OBS Studio sources and pre-built dependencies are fetched automatically via `buildspec.json` |
| 62 | +(obs-studio 30.1.2, obs-deps, Qt6). |
| 63 | + |
| 64 | +### Windows (local) |
| 65 | + |
| 66 | +```powershell |
| 67 | +# Configure + Build + Install |
| 68 | +.\build.ps1 |
| 69 | +
|
| 70 | +# With Inno Setup installer |
| 71 | +.\build.ps1 -installer |
| 72 | +``` |
| 73 | + |
| 74 | +Or manually: |
| 75 | + |
| 76 | +```powershell |
| 77 | +cmake --fresh -S . -B build_x64 -G "Visual Studio 17 2022" -A x64 |
| 78 | +cmake --build build_x64 --config RelWithDebInfo |
| 79 | +cmake --install build_x64 --prefix release/Package --config RelWithDebInfo |
| 80 | +``` |
| 81 | + |
| 82 | +### CMake Presets |
| 83 | + |
| 84 | +Use `cmake --preset <name>` with one of: |
| 85 | +- `windows-x64` / `windows-ci-x64` |
| 86 | +- `macos` / `macos-ci` |
| 87 | +- `linux-x86_64` / `linux-ci-x86_64` |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Architecture & Key Concepts |
| 92 | + |
| 93 | +### Plugin Entry Point |
| 94 | + |
| 95 | +The plugin registers an OBS **effect filter** via `obs_source_info` (filter ID: `osi_branch_output`). |
| 96 | +The main class is `BranchOutputFilter` (declared in `plugin-main.hpp`), which is a `QObject` subclass. |
| 97 | + |
| 98 | +### Core Responsibilities of BranchOutputFilter |
| 99 | + |
| 100 | +| Area | Description | |
| 101 | +|------|-------------| |
| 102 | +| **Streaming** | Creates up to `MAX_SERVICES` (8) independent streaming outputs with dedicated services, encoders, and reconnect logic. | |
| 103 | +| **Recording** | Supports file recording with various container formats, time/size-based splitting, pause/unpause, and chapter markers. | |
| 104 | +| **Audio** | Manages up to `MAX_AUDIO_MIXES` audio contexts via `AudioCapture` class. Supports filter audio, per-source audio, and audio track selection. | |
| 105 | +| **Video** | Creates an OBS view (`obs_view_t`) with a private video output for per-filter encoding and resolution control. | |
| 106 | +| **UI** | Properties panel built via OBS properties API (`plugin-ui.cpp`). Status dock (`BranchOutputStatusDock`) shows live statistics for all filters. | |
| 107 | +| **Hotkeys** | Registers hotkey pairs for enable/disable, split recording, pause/unpause, and chapter markers. | |
| 108 | +| **Interlock** | Can link filter activation to OBS streaming, recording, or virtual camera states. | |
| 109 | +| **Blanking** | Uses a private solid-color source to blank output when the parent source is inactive. | |
| 110 | + |
| 111 | +### Threading Model |
| 112 | + |
| 113 | +- OBS callbacks (video render, audio filter, video tick) may run on **different threads** from the UI thread. |
| 114 | +- `pthread_mutex_t outputMutex` protects streaming/recording output state. |
| 115 | +- `QMutex` protects audio buffers in `AudioCapture`. |
| 116 | +- UI updates use `QMetaObject::invokeMethod` with `Qt::QueuedConnection` for thread safety. |
| 117 | +- Settings changes are tracked via revision counters (`storedSettingsRev` / `activeSettingsRev`) to defer restarts. |
| 118 | + |
| 119 | +### OBS API Usage |
| 120 | + |
| 121 | +The plugin heavily uses: |
| 122 | +- `obs-module.h` — Module registration, locale, config paths |
| 123 | +- `obs-frontend-api.h` — Profile config, scene enumeration, frontend events |
| 124 | +- `obs.hpp` — OBS RAII wrappers (`OBSSourceAutoRelease`, `OBSEncoderAutoRelease`, `OBSOutputAutoRelease`, etc.) |
| 125 | +- `util/deque.h`, `util/threading.h`, `util/platform.h` — Low-level utilities |
| 126 | + |
| 127 | +--- |
| 128 | + |
| 129 | +## Code Style & Formatting |
| 130 | + |
| 131 | +### C++ (clang-format) |
| 132 | + |
| 133 | +- **Standard**: C++17 |
| 134 | +- **Column limit**: 120 |
| 135 | +- **Indent**: 4 spaces (tabs not used) |
| 136 | +- **Brace style**: Custom — functions use next-line braces; control statements use same-line braces |
| 137 | +- **clang-format version**: ≥ 16 required |
| 138 | +- Run: `clang-format -i src/**/*.cpp src/**/*.hpp` |
| 139 | + |
| 140 | +### CMake (cmake-format) |
| 141 | + |
| 142 | +- **Line width**: 120 |
| 143 | +- **Tab size**: 2 |
| 144 | +- Config: `.cmake-format.json` |
| 145 | + |
| 146 | +### CI Enforcement |
| 147 | + |
| 148 | +Format is checked in CI via `.github/workflows/check-format.yaml` using reusable actions |
| 149 | +(`run-clang-format`, `run-cmake-format`). PRs and pushes to `master` are validated. |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## Coding Guidelines |
| 154 | + |
| 155 | +### General Rules |
| 156 | + |
| 157 | +1. **Follow OBS plugin conventions** — Use `obs_log()` for logging, `OBS_DECLARE_MODULE()` for entry, locale via `obs_module_text()`. |
| 158 | +2. **RAII wrappers** — Always use `OBSSourceAutoRelease`, `OBSDataAutoRelease`, etc. instead of manual `obs_*_release()`. |
| 159 | +3. **Thread safety** — Any data shared between OBS callbacks and UI must be guarded by mutex. Use `QMetaObject::invokeMethod` with `Qt::QueuedConnection` when calling UI methods from non-UI threads. |
| 160 | +4. **Settings migration** — When changing settings schema, add backward-compatible migration code in the constructor (see `audio_source` migration example in `BranchOutputFilter` constructor). |
| 161 | +5. **Qt MOC** — Classes using `Q_OBJECT` must be in headers. `AUTOMOC`, `AUTOUIC`, `AUTORCC` are enabled. |
| 162 | + |
| 163 | +### Naming Conventions |
| 164 | + |
| 165 | +- **Classes**: PascalCase (`BranchOutputFilter`, `AudioCapture`, `OutputTableRow`) |
| 166 | +- **Methods**: camelCase (`startOutput`, `stopRecordingOutput`, `onIntervalTimerTimeout`) |
| 167 | +- **Constants/Macros**: UPPER_SNAKE_CASE (`MAX_SERVICES`, `FILTER_ID`, `OUTPUT_MAX_RETRIES`) |
| 168 | +- **Member variables**: camelCase, no prefix (`filterSource`, `videoEncoder`, `recordingActive`) |
| 169 | +- **Static callbacks**: camelCase with descriptive prefix (`onEnableFilterHotkeyPressed`, `audioFilterCallback`) |
| 170 | + |
| 171 | +### Ternary Operators |
| 172 | + |
| 173 | +- **Avoid multi-line ternary expressions** — clang-format cannot reliably format nested or multi-line ternary operators (`a ? b : c`) in a readable way. Use `if`/`else if`/`else` statements instead when the expression would span multiple lines. |
| 174 | + |
| 175 | +### Error Handling |
| 176 | + |
| 177 | +- Use `obs_log(LOG_ERROR, ...)` for errors, `LOG_WARNING` for warnings, `LOG_INFO` for lifecycle events, `LOG_DEBUG` for verbose tracing. |
| 178 | +- Check return values from OBS API calls; handle gracefully (do not crash OBS). |
| 179 | +- Never throw C++ exceptions across OBS API boundaries. |
| 180 | + |
| 181 | +### Localization |
| 182 | + |
| 183 | +- All user-facing strings must use `obs_module_text("Key")` or the `QTStr("Key")` helper. |
| 184 | +- Add new keys to `data/locale/en-US.ini` (primary) and ideally to `ja-JP.ini`. |
| 185 | +- Locale files use INI format: `Key="Value"`. |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## Testing & Validation |
| 190 | + |
| 191 | +- There are no automated unit tests in this repository currently. |
| 192 | +- **Manual testing** is required: load the plugin in OBS Studio, add the "Branch Output" filter, and verify streaming/recording behavior. |
| 193 | +- Test on all supported platforms when possible (Windows x64, macOS, Linux). |
| 194 | +- Verify Studio Mode compatibility (Branch Output ignores studio mode's program and outputs from preview). |
| 195 | +- Verify that enabling and then disabling the Branch Output filter does not cause a crash. |
| 196 | +- Verify that shutting down OBS does not crash when the Branch Output filter is inactive or active, respectively. |
| 197 | +- Verify that switching scene collections does not crash when the Branch Output filter is inactive or active, respectively. |
| 198 | +- Verify that no memory leaks are logged on OBS shutdown. |
| 199 | +- Verify that mutex does not cause deadlocks. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## CI / CD |
| 204 | + |
| 205 | +| Workflow | Trigger | Purpose | |
| 206 | +|----------|---------|---------| |
| 207 | +| `push.yaml` | Push to `master`/`main`/`release/**`, tags | Format check + build + release creation | |
| 208 | +| `build-project.yaml` | Called by push/PR workflows | Multi-platform build (Windows, macOS, Linux) | |
| 209 | +| `check-format.yaml` | Called by push workflow | clang-format and cmake-format validation | |
| 210 | +| `pr-pull.yaml` | Pull requests | Build validation | |
| 211 | +| `dispatch.yaml` | Manual dispatch | On-demand builds | |
| 212 | + |
| 213 | +Release tags follow semver: `X.Y.Z` for stable, `X.Y.Z-beta`/`X.Y.Z-rc` for pre-releases. |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## Common Tasks for AI Agents |
| 218 | + |
| 219 | +### Adding a New Setting / Feature |
| 220 | + |
| 221 | +1. Add the setting key to `BranchOutputFilter::getDefaults()` in `plugin-main.cpp`. |
| 222 | +2. Add UI controls in `plugin-ui.cpp` (use OBS properties API: `obs_properties_add_*`). |
| 223 | +3. Handle the setting in `startOutput()` / `stopOutput()` / `updateCallback()` as needed. |
| 224 | +4. 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.ini` is the primary (required); all others should also be updated. |
| 225 | +5. Ensure backward compatibility — existing saved settings must still load correctly. |
| 226 | + |
| 227 | +### Adding a New Streaming Service Slot |
| 228 | + |
| 229 | +- The plugin supports up to `MAX_SERVICES` (8) service slots. |
| 230 | +- Each slot has its own `BranchOutputStreamingContext` with output, service, and signals. |
| 231 | +- Use `getIndexedPropNameFormat()` for indexed property names. |
| 232 | + |
| 233 | +### Modifying Audio Handling |
| 234 | + |
| 235 | +- Audio capture is abstracted through `AudioCapture` class in `src/audio/`. |
| 236 | +- The class manages an audio buffer, supports push/pop patterns, and handles format conversion. |
| 237 | +- Audio mixers and encoder binding are managed in `BranchOutputFilter::startOutput()`. |
| 238 | + |
| 239 | +### Modifying the Status Dock |
| 240 | + |
| 241 | +- `BranchOutputStatusDock` in `src/UI/` is a `QFrame`-based dock widget. |
| 242 | +- It uses a `QTableWidget` with custom cell classes (`OutputTableCellItem`, `LabelCell`, `FilterCell`). |
| 243 | +- Thread-safe updates via `QMetaObject::invokeMethod`. |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +## Important Warnings |
| 248 | + |
| 249 | +- **Do NOT call `obs_filter_get_parent()` in the `BranchOutputFilter` constructor** — it returns `nullptr` at that point. Use `addCallback()` instead. |
| 250 | +- **Private sources** (not visible in frontend) are intentionally excluded from status dock and timer registration. |
| 251 | +- **Settings revisions** (`storedSettingsRev` / `activeSettingsRev`) exist to avoid stopping output during reconnect attempts. Do not bypass this mechanism. |
| 252 | +- **Encoder compatibility** — The plugin maps "simple" encoder names to actual encoder IDs, with version-specific fallbacks (OBS 30 vs OBS 31). See `getSimpleVideoEncoder()` in `utils.hpp`. |
| 253 | +- **Memory management** — Use OBS RAII wrappers. Raw `bfree()` / `obs_data_release()` calls are error-prone. |
| 254 | +- **`.gitignore` uses allowlist pattern** — New top-level files/directories must be explicitly un-ignored with `!` prefix. |
0 commit comments