OBS Integration
Overview
Lumio integrates with OBS Studio for remote control (scenes, stream/recording start-stop, status monitoring). The user-facing feature has two pages (/dashboard/obs and /popout/obs) that both talk to OBS via the WebSocket transport (obs-websocket protocol). A separate forward-looking flag widget:obs_browser_source exists for future overlay-widget integration with the OBS Browser Source JS API.
| Piece | Flag | What it is |
|---|---|---|
| OBS Remote feature umbrella | feature:obs_remote | Gates the end-user pages: /dashboard/obs (full OBS control dashboard) and /popout/obs (compact version of the same dashboard — e.g. used as a dock window next to OBS, NOT embedded as a Browser Source). This is the master switch for the whole remote-control feature. |
| WebSocket transport | integration:obs_websocket | External connection from Lumio to OBS via the obs-websocket protocol (port + password). Configured by the streamer in the /dashboard/connections Integrations modal. Used by both the dashboard and the popout to issue control commands. |
| Browser Source overlay capability | widget:obs_browser_source | Flag for overlay widgets (/overlay/[key] pages — chat boxes, event lists, sub goals, etc.) that integrate with the window.obsstudio JS API when loaded inside OBS as Browser Sources. The ObsBrowserSourceProvider is mounted at the overlay root and provides live OBS state (current scene, stream/recording/replay-buffer/virtualcam status, control level, scenes list) to all child widgets via useObsBrowserSource(). Classified under widget:* because it is an overlay-side capability. |
Gating semantics per flag:
feature:obs_remoteoff →/dashboard/obsand/popout/obsare server-side gated via<FeatureDisabledPage>. Both pages use the WebSocket transport, so this flag is the single page-gate for both.integration:obs_websocketoff → write endpoints for the WebSocket integration (PUT /v1/integrations/obs,DELETE /v1/integrations/obs, GraphQLsaveObsConfig,deleteObsConfig) reject the request. Read endpoints still return existing config for visibility. The user can't add/save/delete the WebSocket connection in the Integrations modal.widget:obs_browser_sourceoff → theObsBrowserSourceWidgetentry in the overlay editor picker is hidden. TheObsBrowserSourceProvideritself still mounts at the overlay root unconditionally (it gates internally onwindow.obsstudioavailability, not on the feature flag). Individual overlay widgets that consumeuseObsBrowserSource()should additionally checkuseFeature("widget:obs_browser_source")before reacting to OBS state changes.
Admins toggle all three flags on /admin/feature-flags. Per-plan or per-account overrides work the same as any other feature flag — see the admin feature-flags doc.
WebSocket transport — how the dashboard + popout talk to OBS
integration:obs_websocket is the transport both /dashboard/obs and /popout/obs use. Lumio's backend opens a WebSocket to OBS Studio via the obs-websocket protocol.
- Requires: the user installs OBS, enables the built-in obs-websocket server (Tools → obs-websocket Settings), notes the port and optional password.
- User configuration:
/dashboard/connections→ Integrations section → Add integration → OBS WebSocket. Fields:port(default 4455),password(optional),remote_enabledtoggle,remote_host(only when remote_enabled is true). - Password encryption: AES-256 via
crypto::encrypt(). Stored in the JSONB config column ofintegration_configs. - Capability: full remote control — scene switching, stream/recording start/stop, mute/volume, source visibility, filters, hotkeys, etc. (limited only by obs-websocket protocol coverage).
- Transport: TCP WebSocket to
ws://{host}:{port}. Host defaults tolocalhost; remote mode allows an external IP (requires port-forwarding in the user's router).
Browser Source overlay capability
widget:obs_browser_source is the flag for overlay widgets (not the OBS Remote popout) that integrate with the window.obsstudio JS API. OBS injects this API into any page loaded as a Browser Source inside OBS.
ObsBrowserSourceProvider
The ObsBrowserSourceProvider is mounted unconditionally at the overlay root (OverlayClient for the live overlay, OverlayPreview for editor preview). It:
- Detects
window.obsstudiopresence at mount time (SSR-safe: no-op on the server). - Fetches initial state in parallel: control level, stream/recording/replay-buffer/virtualcam status, current scene, scene list, transition list, current transition.
- Subscribes to all 19 OBS Browser Source events (scene changes, streaming/recording/replay-buffer/virtualcam state transitions, source visibility/active changes).
- Runs status polling every 2 000 ms as a drift-correction backup.
- Cleans up all listeners and polling on unmount.
When OBS is not detected, the Provider supplies no-op defaults — all methods are safe to call unconditionally.
useObsBrowserSource()
Consumer widgets import useObsBrowserSource from @/contexts/obs-browser-source-context to read live OBS state:
import { useObsBrowserSource } from "@/contexts/obs-browser-source-context";
function MyWidget() {
const { available, currentScene, status, setScene } = useObsBrowserSource();
// ...
}
Available fields:
| Field | Type | Description |
|---|---|---|
available | boolean | True when window.obsstudio was detected |
pluginVersion | string | null | obs-browser plugin version |
controlLevel | OBSControlLevel | Current control permissions (NONE/READ_OBS/READ_USER/BASIC/ADVANCED/ALL) |
status | OBSStatus | null | streaming / recording / replayBuffer / virtualcam booleans |
currentScene | string | null | Active scene name |
currentSceneInfo | OBSScene | null | Active scene object including canvas width and height (useful when canvas dimensions are needed) |
scenes | string[] | All scene names |
transitions | string[] | All available transition names |
currentTransition | string | null | Active transition name |
canRead() | () => boolean | controlLevel >= READ_OBS (1) |
canReadUser() | () => boolean | controlLevel >= READ_USER (2) |
canControl() | () => boolean | controlLevel >= BASIC (3) |
canModify() | () => boolean | controlLevel >= ADVANCED (4) |
canFullControl() | () => boolean | controlLevel == ALL (5) |
Control methods (setScene, setTransition, startStreaming, stopStreaming, startRecording, stopRecording, pauseRecording, unpauseRecording, startReplayBuffer, stopReplayBuffer, saveReplayBuffer, startVirtualcam, stopVirtualcam) are no-ops when OBS is not available. Capability helpers canReadUser() and canControl() are also available (see table above).
Lumio WebSocket relay (TODO)
When the overlay gains a Lumio WebSocket connection (for receiving real-time automation events), OBS Browser Source events should be forwarded to the backend from inside ObsBrowserSourceProvider. The call-site is stubbed with a TODO comment at apps/web/src/contexts/obs-browser-source-context.tsx. See apps/web/src/hooks/use-obs-websocket.ts for the existing relay pattern.
Key files
| Path | Description |
|---|---|
apps/web/src/contexts/obs-browser-source-context.tsx | Provider + context + useObsBrowserSource hook |
apps/web/src/components/editor/widget-renderers/ObsBrowserSourceWidget.tsx | Headless widget catalogue entry (renders null) |
apps/web/src/components/editor/widget-settings/ObsBrowserSourceSettings.tsx | Settings panel showing live OBS state |
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsx | Mounts ObsBrowserSourceProvider for the live overlay |
apps/web/src/components/overlay/overlay-preview.tsx | Mounts ObsBrowserSourceProvider for the editor preview |
shared/obs/src/client.ts | ObsBrowserSource class (typed window.obsstudio wrapper) |
shared/obs/src/types.ts | OBSStatus, OBSScene, OBSTransition, OBSEventType, OBSControlLevel |
Admin configuration
Toggle transports globally
/admin/feature-flags — flip any of:
feature:obs_remote— kills the entire OBS remote feature for all accountsintegration:obs_websocket— kills the WebSocket transport globally (users can't add the WebSocket integration)widget:obs_browser_source— kills the Browser Source transport globally (overlays can't usewindow.obsstudio)
Per-plan / per-account overrides
- Plan overrides: use the plan's feature-settings in
/admin/plans/{slug}to disable any of the three flags for specific tiers. - Account overrides: use
/admin/accounts/{id}/featuresto set a per-account override.
See the admin feature-flags doc for the resolution chain.
User configuration
Dashboard UI
/dashboard/obs— the full OBS Remote dashboard page. Gated onfeature:obs_remote. Shows current WebSocket configuration + status, scene switcher, stream/recording controls. Requiresintegration:obs_websocketto be enabled for actual control actions./popout/obs— compact version of the same dashboard, designed to be opened as a docked browser window next to OBS (e.g. via OBS's "Custom Browser Docks" or a standalone browser window). Samefeature:obs_remotegate, same WebSocket transport. NOT designed to be loaded as a Browser Source inside OBS./dashboard/connections— the Integrations modal's "OBS WebSocket" entry. Gated onintegration:obs_websocket. Where the user configures port/password/remote host.
What the user sees when a flag is off
feature:obs_remoteoff → both/dashboard/obsand/popout/obsshow the genericFeatureDisabledPagewith reason (global_off/plan_locked/account_override).integration:obs_websocketoff → "OBS WebSocket" entry disappears from the Integrations Add modal; attempts to save/delete config via direct API call returnFeature 'integration:obs_websocket' is not available. Existing saved config is still readable.widget:obs_browser_sourceoff → no user-facing impact today (no overlay widget currently uses thewindow.obsstudioAPI). Reserved for future overlay-widget features that integrate with the Browser Source JS API.
Architecture
Backend
- GraphQL (
apps/api/src/graphql/obs.rs) — queries for config, status, remote connection status; mutations for save/delete config, test connection, and stream/recording/scene control. Write-mutations are gated by three guards chained viaFeatureGuard::new("feature:obs_remote").and(FeatureGuard::new("integration:obs_websocket")).and(PermissionGuard::new("obs:edit"))so BOTH feature flags AND the permission must be satisfied. - REST (
apps/api/src/routes/obs_integration.rs) — mirrors the GraphQL mutations with identical guard order:require_permission("obs:edit")→require_feature("feature:obs_remote")→require_feature("integration:obs_websocket"). Same error messages and codes as the GraphQL side per the parity rule.
Note: the control mutations (
obsControlStream,obsControlRecording,obsSwitchScene) andobsRemoteStatusquery are stubbed — they accept requests and return success / disconnected responses without actually dispatching to the OBS worker. The WorkerManager integration is pending. Dashboard callers should not rely on these return values until the integration ships.
- OBS Remote Client (
crates/lo-obs-remote/) —ObsRemoteClienthandles WebSocket connections to OBS Studio, including authentication with the encrypted password. - Configuration storage — OBS config is stored as JSONB in the
integration_configstable with platform ="obs", label ="OBS WebSocket". The password field is encrypted using AES-256.
Frontend
/dashboard/obs— Settings page with port/password form, stream/recording control buttons, scene switcher, connection status indicator./popout/obs— Compact dock version of/dashboard/obs. Uses the sameObsWebSocketClientWebSocket transport as the dashboard. Designed to be opened as a docked browser window alongside OBS (e.g. via OBS's "Custom Browser Docks" or a standalone browser window). NOT loaded as a Browser Source and does NOT usewindow.obsstudio./dashboard/connections— Integrations modal with "OBS WebSocket" entry gated byuseFeatureStatus("integration:obs_websocket").
API
GraphQL Queries
| Query | Permission | Description |
|---|---|---|
obsConfig | obs:read | Full OBS config: port, remote_enabled, remote_host, has_password flag, timestamps. Never exposes the actual password. |
obsStatus | obs:read | Lightweight status: configured flag, port, remote settings. |
obsRemoteStatus (STUB) | obs:read | Remote connection status from OBS worker: connected flag, stream/recording status, scene list, current scene. Currently always returns disconnected/empty — WorkerManager integration pending. |
GraphQL Mutations
| Mutation | Guards | Description |
|---|---|---|
saveObsConfig(port?, password?, remoteEnabled?, remoteHost?) | feature:obs_remote + integration:obs_websocket + obs:edit | Save (upsert) OBS config. Port defaults to 4455. Password is encrypted before storage. |
deleteObsConfig | feature:obs_remote + integration:obs_websocket + obs:delete | Delete OBS integration config. |
testObsConnection | feature:obs_remote + obs:edit | Test WebSocket connection to OBS. Only available when remote mode is enabled. 10-second timeout. |
obsControlStream(action) (STUB) | feature:obs_remote + obs:edit | Control streaming: "start" or "stop". Currently always returns success — WorkerManager integration pending. |
obsControlRecording(action) (STUB) | feature:obs_remote + obs:edit | Control recording: "start" or "stop". Currently always returns success — WorkerManager integration pending. |
obsSwitchScene(sceneName) (STUB) | feature:obs_remote + obs:edit | Switch the active OBS scene. Currently always returns success — WorkerManager integration pending. |
REST Endpoints
| Method | Path | Guards | Description |
|---|---|---|---|
GET | /v1/integrations/obs | obs:read + feature:obs_remote | Get OBS config. |
PUT | /v1/integrations/obs | obs:edit + feature:obs_remote + integration:obs_websocket | Save OBS config. |
DELETE | /v1/integrations/obs | obs:delete + feature:obs_remote + integration:obs_websocket | Delete OBS config. |
GET | /v1/integrations/obs/credentials | obs:read + feature:obs_remote | Get OBS credentials (decrypted, for internal use). |
GET | /v1/integrations/obs/status | obs:read + feature:obs_remote | Get OBS connection status. |
POST | /v1/integrations/obs/test | obs:edit + feature:obs_remote | Test OBS WebSocket connection. |
Read endpoints are intentionally NOT gated on integration:obs_websocket — existing configurations remain visible in the UI even when the transport flag is switched off for an account. Only WRITE paths (create/update/delete) are blocked so admins can disable new adoption without breaking existing users' views.
Permissions
| Permission | Description |
|---|---|
obs:read | Read OBS configuration and status |
obs:edit | Edit OBS configuration, test connection, control stream/recording/scenes |
obs:delete | Delete OBS configuration |
Database
| Table | Database | Description |
|---|---|---|
integration_configs | PostgreSQL | id, account_id, platform ("obs"), label ("OBS WebSocket"), enabled, config (JSONB), created_at, updated_at. Unique on (account_id, platform). |
Config JSONB Structure
{
"port": 4455,
"password": "encrypted_string_or_null",
"remote_enabled": false,
"remote_host": "192.168.1.100"
}
port— OBS WebSocket port (default 4455)password— Encrypted OBS WebSocket password (null if not set)remote_enabled— Whether remote (non-localhost) connections are enabledremote_host— Remote host address (only used when remote_enabled is true)
Data Flow (WebSocket path)
- User configures OBS settings (port, optional password, optional remote host) in
/dashboard/connections→ Integrations → OBS WebSocket. - Password is encrypted via
crypto::encrypt()and stored as part of the JSONB config. - User can test the connection, which attempts a WebSocket handshake to
ws://{host}:{port}with the decrypted password. - Stream/recording/scene commands from
/dashboard/obsor/popout/obsare sent via the WorkerManager to the OBS Remote Client. - OBS Remote Client communicates with OBS Studio over the obs-websocket protocol.
Overlay-widget Browser Source integration
Overlay widgets under /overlay/[key] can consume useObsBrowserSource() to react to OBS state (e.g. auto-hide when a specific scene is active). The ObsBrowserSourceProvider is already mounted at the overlay root — no additional setup is needed. To gate a widget's OBS-reactive behaviour on the feature flag, check useFeature("widget:obs_browser_source") inside the widget component.
Plan Restriction
OBS remote mode (remote_enabled: true) is only available on the Pro plan and above, enforced via PlanLimits.obs_remote_allowed. The integration:obs_websocket flag is independent of this — it can be disabled globally or per-account even on Pro plans.
Key Files
| Path | Description |
|---|---|
apps/api/src/graphql/obs.rs | GraphQL queries and mutations with three-guard chain |
apps/api/src/routes/obs_integration.rs | REST endpoints with identical guard chain |
apps/api/src/routes/obs_remote.rs | Remote control routes (stub — route handlers exist but are not yet wired to WorkerManager/Redis; all responses are hardcoded) |
crates/lo-obs-remote/ | OBS WebSocket client |
crates/lo-obs/ | Browser Source API types and obs-websocket vendor bridge (Rust) |
shared/obs/src/client.ts | Typed wrapper around window.obsstudio |
shared/obs/src/ws-client.ts | ObsWebSocketClient — typed obs-websocket v5 client used by the dashboard and popout |
shared/obs/src/types.ts | OBSStatus, OBSScene, OBSTransition, OBSEventType, OBSControlLevel |
apps/web/src/hooks/use-obs-websocket.ts | Dashboard/popout hook for OBS WebSocket state and control commands |
apps/api/src/crypto.rs | Password encryption/decryption |
apps/web/src/contexts/obs-browser-source-context.tsx | ObsBrowserSourceProvider + useObsBrowserSource hook |
apps/web/src/components/editor/widget-renderers/ObsBrowserSourceWidget.tsx | Headless widget catalogue entry (renders null) |
apps/web/src/components/editor/widget-settings/ObsBrowserSourceSettings.tsx | Settings panel showing live OBS state |
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsx | Mounts ObsBrowserSourceProvider for the live overlay |
apps/web/src/components/overlay/overlay-preview.tsx | Mounts ObsBrowserSourceProvider for the editor preview |
apps/web/src/app/(app)/dashboard/obs/page.tsx | OBS Remote Settings page (server-side gated on feature:obs_remote) |
apps/web/src/app/(popout)/popout/obs/page.tsx | Compact OBS Remote dashboard popout (server-side gated on feature:obs_remote). Uses the same WebSocket transport as /dashboard/obs. NOT a Browser Source. |