Skip to main content

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.

PieceFlagWhat it is
OBS Remote feature umbrellafeature:obs_remoteGates 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 transportintegration:obs_websocketExternal 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 capabilitywidget:obs_browser_sourceFlag 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_remote off/dashboard/obs and /popout/obs are server-side gated via <FeatureDisabledPage>. Both pages use the WebSocket transport, so this flag is the single page-gate for both.
  • integration:obs_websocket off → write endpoints for the WebSocket integration (PUT /v1/integrations/obs, DELETE /v1/integrations/obs, GraphQL saveObsConfig, 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_source off → the ObsBrowserSourceWidget entry in the overlay editor picker is hidden. The ObsBrowserSourceProvider itself still mounts at the overlay root unconditionally (it gates internally on window.obsstudio availability, not on the feature flag). Individual overlay widgets that consume useObsBrowserSource() should additionally check useFeature("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_enabled toggle, remote_host (only when remote_enabled is true).
  • Password encryption: AES-256 via crypto::encrypt(). Stored in the JSONB config column of integration_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 to localhost; 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:

  1. Detects window.obsstudio presence at mount time (SSR-safe: no-op on the server).
  2. Fetches initial state in parallel: control level, stream/recording/replay-buffer/virtualcam status, current scene, scene list, transition list, current transition.
  3. Subscribes to all 19 OBS Browser Source events (scene changes, streaming/recording/replay-buffer/virtualcam state transitions, source visibility/active changes).
  4. Runs status polling every 2 000 ms as a drift-correction backup.
  5. 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:

FieldTypeDescription
availablebooleanTrue when window.obsstudio was detected
pluginVersionstring | nullobs-browser plugin version
controlLevelOBSControlLevelCurrent control permissions (NONE/READ_OBS/READ_USER/BASIC/ADVANCED/ALL)
statusOBSStatus | nullstreaming / recording / replayBuffer / virtualcam booleans
currentScenestring | nullActive scene name
currentSceneInfoOBSScene | nullActive scene object including canvas width and height (useful when canvas dimensions are needed)
scenesstring[]All scene names
transitionsstring[]All available transition names
currentTransitionstring | nullActive transition name
canRead()() => booleancontrolLevel >= READ_OBS (1)
canReadUser()() => booleancontrolLevel >= READ_USER (2)
canControl()() => booleancontrolLevel >= BASIC (3)
canModify()() => booleancontrolLevel >= ADVANCED (4)
canFullControl()() => booleancontrolLevel == 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

PathDescription
apps/web/src/contexts/obs-browser-source-context.tsxProvider + context + useObsBrowserSource hook
apps/web/src/components/editor/widget-renderers/ObsBrowserSourceWidget.tsxHeadless widget catalogue entry (renders null)
apps/web/src/components/editor/widget-settings/ObsBrowserSourceSettings.tsxSettings panel showing live OBS state
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsxMounts ObsBrowserSourceProvider for the live overlay
apps/web/src/components/overlay/overlay-preview.tsxMounts ObsBrowserSourceProvider for the editor preview
shared/obs/src/client.tsObsBrowserSource class (typed window.obsstudio wrapper)
shared/obs/src/types.tsOBSStatus, 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 accounts
  • integration: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 use window.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}/features to 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 on feature:obs_remote. Shows current WebSocket configuration + status, scene switcher, stream/recording controls. Requires integration:obs_websocket to 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). Same feature:obs_remote gate, same WebSocket transport. NOT designed to be loaded as a Browser Source inside OBS.
  • /dashboard/connections — the Integrations modal's "OBS WebSocket" entry. Gated on integration:obs_websocket. Where the user configures port/password/remote host.

What the user sees when a flag is off

  • feature:obs_remote off → both /dashboard/obs and /popout/obs show the generic FeatureDisabledPage with reason (global_off / plan_locked / account_override).
  • integration:obs_websocket off → "OBS WebSocket" entry disappears from the Integrations Add modal; attempts to save/delete config via direct API call return Feature 'integration:obs_websocket' is not available. Existing saved config is still readable.
  • widget:obs_browser_source off → no user-facing impact today (no overlay widget currently uses the window.obsstudio API). 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 via FeatureGuard::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) and obsRemoteStatus query 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/) — ObsRemoteClient handles WebSocket connections to OBS Studio, including authentication with the encrypted password.
  • Configuration storage — OBS config is stored as JSONB in the integration_configs table 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 same ObsWebSocketClient WebSocket 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 use window.obsstudio.
  • /dashboard/connections — Integrations modal with "OBS WebSocket" entry gated by useFeatureStatus("integration:obs_websocket").

API

GraphQL Queries

QueryPermissionDescription
obsConfigobs:readFull OBS config: port, remote_enabled, remote_host, has_password flag, timestamps. Never exposes the actual password.
obsStatusobs:readLightweight status: configured flag, port, remote settings.
obsRemoteStatus (STUB)obs:readRemote connection status from OBS worker: connected flag, stream/recording status, scene list, current scene. Currently always returns disconnected/empty — WorkerManager integration pending.

GraphQL Mutations

MutationGuardsDescription
saveObsConfig(port?, password?, remoteEnabled?, remoteHost?)feature:obs_remote + integration:obs_websocket + obs:editSave (upsert) OBS config. Port defaults to 4455. Password is encrypted before storage.
deleteObsConfigfeature:obs_remote + integration:obs_websocket + obs:deleteDelete OBS integration config.
testObsConnectionfeature:obs_remote + obs:editTest WebSocket connection to OBS. Only available when remote mode is enabled. 10-second timeout.
obsControlStream(action) (STUB)feature:obs_remote + obs:editControl streaming: "start" or "stop". Currently always returns success — WorkerManager integration pending.
obsControlRecording(action) (STUB)feature:obs_remote + obs:editControl recording: "start" or "stop". Currently always returns success — WorkerManager integration pending.
obsSwitchScene(sceneName) (STUB)feature:obs_remote + obs:editSwitch the active OBS scene. Currently always returns success — WorkerManager integration pending.

REST Endpoints

MethodPathGuardsDescription
GET/v1/integrations/obsobs:read + feature:obs_remoteGet OBS config.
PUT/v1/integrations/obsobs:edit + feature:obs_remote + integration:obs_websocketSave OBS config.
DELETE/v1/integrations/obsobs:delete + feature:obs_remote + integration:obs_websocketDelete OBS config.
GET/v1/integrations/obs/credentialsobs:read + feature:obs_remoteGet OBS credentials (decrypted, for internal use).
GET/v1/integrations/obs/statusobs:read + feature:obs_remoteGet OBS connection status.
POST/v1/integrations/obs/testobs:edit + feature:obs_remoteTest 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

PermissionDescription
obs:readRead OBS configuration and status
obs:editEdit OBS configuration, test connection, control stream/recording/scenes
obs:deleteDelete OBS configuration

Database

TableDatabaseDescription
integration_configsPostgreSQLid, 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 enabled
  • remote_host — Remote host address (only used when remote_enabled is true)

Data Flow (WebSocket path)

  1. User configures OBS settings (port, optional password, optional remote host) in /dashboard/connections → Integrations → OBS WebSocket.
  2. Password is encrypted via crypto::encrypt() and stored as part of the JSONB config.
  3. User can test the connection, which attempts a WebSocket handshake to ws://{host}:{port} with the decrypted password.
  4. Stream/recording/scene commands from /dashboard/obs or /popout/obs are sent via the WorkerManager to the OBS Remote Client.
  5. 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

PathDescription
apps/api/src/graphql/obs.rsGraphQL queries and mutations with three-guard chain
apps/api/src/routes/obs_integration.rsREST endpoints with identical guard chain
apps/api/src/routes/obs_remote.rsRemote 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.tsTyped wrapper around window.obsstudio
shared/obs/src/ws-client.tsObsWebSocketClient — typed obs-websocket v5 client used by the dashboard and popout
shared/obs/src/types.tsOBSStatus, OBSScene, OBSTransition, OBSEventType, OBSControlLevel
apps/web/src/hooks/use-obs-websocket.tsDashboard/popout hook for OBS WebSocket state and control commands
apps/api/src/crypto.rsPassword encryption/decryption
apps/web/src/contexts/obs-browser-source-context.tsxObsBrowserSourceProvider + useObsBrowserSource hook
apps/web/src/components/editor/widget-renderers/ObsBrowserSourceWidget.tsxHeadless widget catalogue entry (renders null)
apps/web/src/components/editor/widget-settings/ObsBrowserSourceSettings.tsxSettings panel showing live OBS state
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsxMounts ObsBrowserSourceProvider for the live overlay
apps/web/src/components/overlay/overlay-preview.tsxMounts ObsBrowserSourceProvider for the editor preview
apps/web/src/app/(app)/dashboard/obs/page.tsxOBS Remote Settings page (server-side gated on feature:obs_remote)
apps/web/src/app/(popout)/popout/obs/page.tsxCompact OBS Remote dashboard popout (server-side gated on feature:obs_remote). Uses the same WebSocket transport as /dashboard/obs. NOT a Browser Source.