Skip to content

Commit 7a3b7de

Browse files
authored
Merge pull request #104 from OPENSPHERE-Inc/patch-1.0.8
Patch 1.0.8
2 parents 793a0d0 + 557a832 commit 7a3b7de

30 files changed

Lines changed: 2348 additions & 180 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@
2828
# Except additional project files
2929
!screenshot1.jpg
3030
!screenshot2.jpg
31-
!build-windows-installer.ps1
31+
!build.ps1
3232
!README_ja.md
33+
34+
# Except for AI agent instructions
35+
!AGENTS.md

AGENTS.md

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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.

CMakeLists.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ if(ENABLE_QT)
3535
endif()
3636

3737
target_sources(
38-
${CMAKE_PROJECT_NAME} PRIVATE src/plugin-main.cpp src/plugin-ui.cpp src/utils.cpp src/audio/audio-capture.cpp
39-
src/UI/output-status-dock.cpp src/UI/resources.qrc)
38+
${CMAKE_PROJECT_NAME}
39+
PRIVATE src/plugin-main.cpp
40+
src/plugin-replay-buffer.cpp
41+
src/plugin-ui.cpp
42+
src/utils.cpp
43+
src/audio/audio-capture.cpp
44+
src/video/filter-video-capture.cpp
45+
src/UI/output-status-dock.cpp
46+
src/UI/resources.qrc)
4047

4148
set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name})
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Param (
2+
[switch]$installer
3+
)
4+
15
$BuildSpec = Get-Content -Path ./buildspec.json -Raw | ConvertFrom-Json
26
$ProductName = $BuildSpec.name
37
$ProductVersion = $BuildSpec.version
@@ -8,4 +12,6 @@ cmake --fresh -S . -B build_x64 -Wdev -Wdeprecated -DCMAKE_SYSTEM_VERSION="10.0.
812
cmake --build build_x64 --config RelWithDebInfo --target ALL_BUILD --
913
cmake --install build_x64 --prefix release/Package --config RelWithDebInfo
1014

11-
iscc build_x64/installer-Windows.generated.iss /O"release" /F"${OutputName}-Installer-signed"
15+
if ($installer) {
16+
iscc build_x64/installer-Windows.generated.iss /O"release" /F"${OutputName}-Installer-signed"
17+
}

buildspec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
},
3939
"name": "osi-branch-output",
4040
"displayName": "Branch Output Plugin",
41-
"version": "1.0.7",
41+
"version": "1.0.8",
4242
"author": "OPENSPHERE Inc.",
4343
"website": "https://opensphere.co.jp/",
4444
"email": "info@opensphere.co.jp",

data/locale/ca-ES.ini

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Status.Active="Actiu"
2323
Status.Paused="En pausa"
2424
Status.Pending="Pendent"
2525
Status.Stopping="Aturant"
26+
Status.BlankSuffix=" (Negre)"
27+
Status.BlankMutedSuffix=" (Negre+Mut)"
2628
Reset="Restableix"
2729
ResetAll="Restableix-ho tot"
2830
EnableAll="Activa-ho tot"
@@ -33,7 +35,9 @@ AlwaysOn="Sempre ON"
3335
Streaming="Transmissió"
3436
Recording="Gravació"
3537
StreamingOrRecording="Transmissió o gravació"
38+
ReplayBuffer="Memòria de repetició"
3639
VirtualCam="Càmera virtual"
40+
AlwaysOff="Sempre OFF"
3741
StreamRecording="Gravació en flux"
3842
Path="Ruta del directori"
3943
VideoFormat="Format de contenidor"
@@ -130,6 +134,10 @@ UseProfileRecordingPath="Utilitzeu la ruta de gravació del perfil"
130134
NoSpaceFileName="Genera noms de fitxer sense espai"
131135
KeepOutputBaseResolution="No reinicieu la sortida quan canviï la resolució de la font"
132136
KeepOutputBaseResolutionNote="Si s'activa, la imatge de vídeo pot quedar retallada o tenir espais en blanc quan es canvia la resolució de la font."
137+
BlankWhenNotVisible="Enfosqueix la sortida quan la font no és a la sortida principal"
138+
BlankWhenNotVisibleNote="Manté la transmissió/enregistrament actiu però substitueix el vídeo per negre sempre que la font no sigui visible a la sortida principal activa. Utilitzeu-ho quan no vulgueu enregistrar ni transmetre escenes fora d'antena."
139+
MuteAudioWhenBlank="Silencia l'àudio mentre està enfosquit"
140+
MuteAudioWhenBlankNote="Quan l'enfosquiment està actiu, també silencia les fonts d'àudio seleccionades. Utilitzeu-ho quan no vulgueu enregistrar ni transmetre l'àudio de les escenes fora d'antena."
133141
AudioSourceNote="<strong>Àudio mestre:</strong> Sortida mixta d'OBS (té 6 pistes conegudes)<br /><strong>Àudio de font:</strong> Àudio del conducte de filtre que es passa al filtre Branch Output.<br /><strong>Àudio de font:</strong> Àudio agrupat de la font després de passar per tots els filtres del conducte."
134142
SplitRecordingFileHotkey="Divideix el fitxer d'enregistrament de '%1'"
135143
SplitRecordingAllHotkey="Divideix tots els fitxers d'enregistrament de Sortida de Branca"
@@ -147,3 +155,20 @@ SuspendRecordingWhenSourceCollapsedNote="Si hi ha emissió, l'enregistrament s'a
147155
AddChapterToRecordingFileHotkey="Afegeix capítol al fitxer d'enregistrament de '%1'"
148156
AddChapterToAllRecordings="Afegeix capítol a tots els enregistraments"
149157
AddChapterToRecordingAllHotkey="Afegeix capítol a tots els enregistraments de Sortida de Branca"
158+
VideoSourceType="Font de vídeo"
159+
VideoSourceType.LongDescription="Seleccioneu quin vídeo capturar per a la sortida.\n'Sortida de la font' captura la sortida renderitzada final de la font (després de tots els filtres).\n'Entrada de filtre' captura el vídeo a la posició d'aquest filtre a la cadena (abans d'aquest filtre i qualsevol filtre a sota)."
160+
VideoSourceType.Source="Mescla independent (Per defecte)"
161+
VideoSourceType.FilterInput="Entrada de filtre (Experimental)"
162+
ReplayBuffer="Replay Buffer"
163+
ReplayBufferDuration="Maximum Replay Time (seconds)"
164+
Status.ReplayBuffer="Buffering"
165+
SaveReplayBufferHotkey="Save '%1' Replay Buffer"
166+
SaveReplayBufferAllHotkey="Save all Branch Output replay buffers"
167+
SaveAllReplayBuffers="Save All Replay Buffers"
168+
ReplayBuffer.Saved="Replay Saved"
169+
AdvancedSettings="Advanced Settings"
170+
StreamingDescription="Configureu sortides de transmissió independents a servidors RTMP/SRT (fins a 8 transmissions simultànies)."
171+
RecordingDescription="Enregistreu la sortida de la font en un fitxer independentment de l'enregistrament principal d'OBS."
172+
ReplayBufferDescription="Manteniu les imatges recents en un buffer de reproducció i deseu-les en qualsevol moment mitjançant una drecera de teclat."
173+
ReplayBufferHotkeyNote="Per desar el buffer de reproducció, utilitzeu la drecera de teclat registrada a Configuració d'OBS Studio → Dreceres de teclat."
174+
CustomAudioSourceDescription="Seleccioneu fonts d'àudio específiques en lloc de l'àudio per defecte. Admet àudio multipista amb fins a 6 pistes."

0 commit comments

Comments
 (0)