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