Architecture¶
How obs-streamelements-core — the SE.Live native plug-in for OBS Studio —
is put together, and why it is put together that way.
This is the companion to the JavaScript OBS API reference, which documents the surface a page sees. These pages document what is behind it: who creates what, in which order, on which thread, under which lock, and which of those arrangements are load-bearing rather than incidental.
The ten-minute version¶
The plug-in is a single shared library that OBS loads like any other
(obs-streamelements-core.dll / .so / .dylib), built inside an
obs-studio tree. It does three things:
- It hosts web pages inside OBS — docks, a central widget, pop-up
windows, modal and non-modal dialogs, and off-screen background workers.
Each is a CEF browser widget borrowed from the sibling
obs-browserplug-in. - It exposes OBS to those pages as
window.host, built from 208 registered handler blocks plus one hand-written registration, reached over a local WebSocket — not over CEF IPC. A few blocks sit behind feature switches, so the live count is build-specific:Object.keys(window.host)is 210 in a WYVRN-enabled Windows build, methods plus four properties. - It adds capabilities OBS does not have — most visibly multiple simultaneous video compositions ("canvases") with their own scenes, transitions, encoders and streaming outputs, alongside OBS's own single native one.
Everything else follows from those three.
window.host call takes this path, top to bottom. The two labelled hops are where the plug-in's shape is decided: a local WebSocket rather than CEF IPC, and a container handed to the handler rather than a global reached from it.Three facts about this diagram are worth more than the diagram:
- The root is not a singleton.
SEMainis created byobs-streamelements-core-plugin.cppand owned there; a class that needs a service is handed it. There is noGetInstance()and noIsInstanceAvailable()anywhere in the plug-in, and two source invariants keep it that way. - The arrows point one way. Services never own their consumers, and
anything whose lifetime a service controls — a browser widget, a dialog, an
API handler — holds the container through a
weak_ptr. That rule is the only reason shutdown terminates; see Services and injection. - The container is reached through interfaces.
SEApiServicescarriesISE*contracts, not concrete classes, so the 2,800-line translation unit holding every host API body compiles in a test binary that links neither libobs nor Qt. See Testing.
Where to start, by what you are doing¶
Adding a window.host method |
The host API bridge, then the /add-host-api skill |
| Adding a subsystem | Services and injection |
| Chasing a shutdown hang or a leak | Module lifecycle, Diagnostics |
| Touching scenes or transitions | Compositions and scenes, Threading and locks |
| Touching streaming, recording or encoders | Outputs and encoders |
| Touching docks, dialogs or menus | UI and widgets |
| Working out why the build or CI did that | Build |
| Working out whether a change is safe | Testing, Enforced invariants |
The chapters¶
- Module lifecycle — the four module entry points,
what happens at
FINISHED_LOADING, thereadygate, and the two ways OBS can ask the plug-in to shut down (one of which is a trap). - Services and injection — the root, the container, the interfaces, the weak back-edge rule, the teardown audit, and the recipe for adding a service.
- The host API bridge — the WebSocket transport, the
handshake that builds
window.host, registration and dispatch, the two deliberate lock domains, async completion, and argument type-checking. - The data model —
CefValuehere is not CEF, what that changes, and the serialization conventions. - Compositions and scenes — native versus custom compositions, scenes and items, transitions, and the lock discipline around them.
- Outputs and encoders — the output family, lazy encoder providers and their reference counting, output settings.
- UI and widgets — widget and dock management, browser widgets, dialogs, pop-ups, menus, OBS's own UI controls, and state persistence.
- Threading and locks — which threads exist, the Qt posting macros, and the re-entrancy hazards that have actually bitten.
- Diagnostics —
SETracereference accounting, the crash-reporting backends, logging and analytics. - Build — how the plug-in attaches to an obs-studio tree, the CMake switches, and why the OBS version matters.
- Testing — the two kinds of test, mutation testing, and the live end-to-end harness.
- Enforced invariants — the 74 architectural rules the test suite enforces, grouped by what they protect.
How to read the code¶
The source carries long comments at the places where the reasoning matters, and they are the authority — these pages summarise and connect them, they do not replace them. When the two disagree, the code is right and a page needs fixing.
Two conventions worth knowing before reading anything:
- Naming. One class per file, file name == class name,
SE<Thing>.{hpp,cpp}; interfaces areISE<Thing>.hppbeside their implementation. Members arem_camelCase, file staticss_, globalsg_. CefRefPtrisstd::shared_ptr.deps/cef-stub/reimplements the CEF value types as a plain JSON-ish data model. None of CEF's refcounting or thread-affinity rules apply to them. See The data model.