Skip to main content

OBS Integration

Overview

Lumio integrates with OBS Studio for remote control (scenes, stream/recording start-stop, status monitoring). Three distinct paths exist, and they do not share a transport:

PathWho connects to obs-websocketState
/dashboard/obs and /popout/obsthe API server's per-account OBS worker (ZAF-1122); the pages call the /api/obs-remote/* proxy routes, which resolve the GraphQL obsRemoteStatus query + obs* mutations (never REST directly)Re-enabled: renders the connection badge, scene grid and stream/recording toggles, all gated on available - falls back to an honest "coming soon" state when no worker is publishing a snapshot. A command to a disconnected worker returns 409 Conflict "OBS remote control is not connected". The worker transport is wired in ZAF-1122
Overlay pages (/overlay/[key])the browser, from inside OBS, using ObsWebSocketClient from @lumio/obsWorking. Credentials arrive in the WebSocket bootstrap payload; obs:action events pushed by automations are executed locally
testObsConnection / POST /v1/integrations/obs/testthe API server, using lo_obs_remote::ObsRemoteClientWorking, but only a connect-and-disconnect probe with a 10-second timeout
PieceFlagWhat it is
OBS Remote feature umbrellafeature:obs_remoteGates every OBS query, mutation and REST route, plus the pages /dashboard/obs and /popout/obs. This is the master switch for the whole feature.
WebSocket transportintegration:obs_websocketThe stored obs-websocket connection details (port + password + optional remote host). Configured by the streamer in the /dashboard/connections Integrations modal. Also gates whether overlay bootstrap payloads carry OBS credentials.
Browser Source overlay capabilitywidget:obs_browser_sourceReserved flag for overlay widgets that would consume the window.obsstudio JS API through useObsBrowserSource(). Seeded and toggleable, but no code path reads it today. Classified under widget:* because it is an overlay-side capability.

Gating semantics per flag:

  • feature:obs_remote off/dashboard/obs (via FeatureRouteGate on its layout.tsx) and /popout/obs (via an SSR feature check) render <FeatureDisabledPage>, and every OBS query, mutation and REST route rejects the request.
  • integration:obs_websocket off → write endpoints for the WebSocket integration (PUT /v1/integrations/obs, DELETE /v1/integrations/obs, GraphQL saveObsConfig, deleteObsConfig) reject the request, and overlay bootstrap payloads omit the obs credentials block. 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 → no observable effect. No widget renderer, settings panel or editor-picker entry consumes this flag, and ObsBrowserSourceProvider mounts unconditionally at the overlay root (it gates internally on window.obsstudio availability). A future overlay widget that reacts to OBS state should check useFeature("widget:obs_browser_source") itself.

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.

The stored obs-websocket connection

integration:obs_websocket covers the connection details Lumio stores for an account. Lumio does not hold a long-lived socket to OBS on the server; it stores the credentials and hands them to whichever surface needs them.

  • 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-GCM via crypto::encrypt_versioned() (writes are v2.-tagged; see OAuth credentials architecture) with a key derived from auth.token_encryption_key. Stored in the JSONB config column of integration_configs.
  • Save semantics: PUT /v1/integrations/obs / saveObsConfig writes the whole config object as an upsert. Submitting an empty or omitted password clears the stored password rather than leaving the previous one in place.
  • Remote-host validation: when remote_host is set it is resolved and rejected if it points at a loopback, private, link-local, unspecified or broadcast address (IPv4 and IPv6, including ULA fc00::/7 and fe80::/10) - an SSRF guard, so remote_host must be a public address.
  • Transport: TCP WebSocket to ws://{host}:{port}. Host defaults to localhost; remote mode requires a publicly reachable host (and port-forwarding in the user's router).

Browser Source overlay capability

widget:obs_browser_source is the reserved flag for overlay widgets that integrate with the window.obsstudio JS API. OBS injects this API into any page loaded as a Browser Source inside OBS. The provider and hook described below exist and are mounted; no widget consumes them yet, and nothing reads the flag.

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: plugin version, control level, stream/recording/replay-buffer/virtualcam status, current scene, scene list, transition list, current transition.
  3. Subscribes to every event in the OBSEventType union except obsReplaybufferSaved (which needs no state change) - scene and scene-list changes, transition and transition-list changes, the four streaming and six recording transitions, the four replay-buffer transitions, virtualcam start/stop, source visibility/active changes, and obsExit (which resets the provider to a disconnected state).
  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 / recordingPaused / 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).

Outbound event relay (not implemented)

Forwarding OBS Browser Source events from the overlay to the Lumio backend is not implemented. The call site is marked with a TODO in apps/web/src/contexts/obs-browser-source-context.tsx, and a matching TODO block in apps/web/src/hooks/use-obs-websocket.ts sketches the intended shape. The reverse direction - backend → overlay → OBS - does work; see below.

Overlay control relay

This is the OBS control path that actually reaches OBS today, and it runs entirely through the overlay:

  1. The account has an OBS integration config and at least one overlay layer of type obs_browser_source (or obs).
  2. When that overlay's WebSocket session starts, OverlayBootstrapProvider (apps/api/src/services/bootstrap_provider.rs) decrypts the stored password and adds an obs block (port, password, remote_enabled, remote_host) to the bootstrap payload - but only when integration:obs_websocket is enabled for the account and the overlay is authenticated with a permanent token. Shared overlay tokens never receive credentials; a shared link may instead carry them explicitly as URL params.
  3. useObsLocalConnect (apps/web/src/hooks/use-obs-local-connect.ts) opens an obs-websocket v5 connection from the browser to the local OBS instance using those credentials, and reconnects automatically if OBS restarts.
  4. An automation's OBS-control action publishes to the Redis channel lumio:obs:action:{account_id} (apps/api/src/dispatch.rs), which reaches the overlay as an obs:action WebSocket event.
  5. handleObsRelayAction (apps/web/src/hooks/use-obs-websocket.ts) dispatches it to the local client. Supported actions: set_current_scene, set_current_transition, set_muted, set_volume, set_filter_enabled, start_stream, stop_stream, start_recording, stop_recording. An unknown action is logged and ignored.

Key files

PathDescription
apps/web/src/contexts/obs-browser-source-context.tsxProvider + context + useObsBrowserSource hook
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsxMounts ObsBrowserSourceProvider, calls useObsLocalConnect, routes obs:action events
apps/web/src/components/overlay/overlay-preview.tsxMounts ObsBrowserSourceProvider for the editor preview
apps/web/src/hooks/use-obs-local-connect.tsBrowser-side obs-websocket connection from bootstrap credentials
shared/obs/src/client.tsObsBrowserSource class (typed window.obsstudio wrapper)
shared/obs/src/types.tsOBSStatus, OBSScene, OBSTransition, OBSEventType, OBSControlLevel

There is no ObsBrowserSourceWidget renderer and no ObsBrowserSourceSettings panel - the overlay editor has no picker entry for this capability, so an obs_browser_source layer cannot currently be created from the editor UI even though the bootstrap provider recognises the layer type.

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 stored WebSocket connection globally (users can't add or edit the OBS integration, and overlays stop receiving credentials)
  • widget:obs_browser_source - reserved; toggling it has no runtime effect today

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 OBS Remote page. Gated on feature:obs_remote via FeatureRouteGate on its layout.tsx. Renders a connection badge, a scene grid, and stream/recording toggle buttons, all driven by GET /api/obs-remote/status and POST /api/obs-remote/{scene,stream,recording}. It contains no port/password form - connection details are edited on /dashboard/connections.
  • /popout/obs - compact version of the same page, 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 REST routes, polling status every 5 s. NOT designed to be loaded as a Browser Source inside OBS.
  • /dashboard/connections - the Integrations modal's "OBS WebSocket" entry, shown only when useFeatureStatus("integration:obs_websocket") reports enabled. Where the user configures port / password / remote toggle / remote host.

Both pages render the interactive control UI - connection badge, scene grid and stream/recording toggles, all driven by GET /api/obs-remote/status (a Next.js proxy route that resolves the GraphQL obsRemoteStatus query) - whenever the status reports available: true, and the dashboard additionally shows the live ingest stats (bitrate, FPS, dropped/total frames). The UI gates every "live" element on available, so it degrades gracefully: when no OBS worker is publishing a snapshot for the account (available: false), both pages fall back to an honest "remote control coming soon" state, and a control click on a disconnected worker returns 409 Conflict ("OBS remote control is not connected") rather than a fake success. The scene/stream/recording controls are additionally disabled while connected is false. The server-side worker transport that publishes those live snapshots and executes the commands is wired in ZAF-1122.

When /popout/obs is opened with a popout token (?token=lm_pop_…), the token is exchanged once for a short-lived popout session cookie (see Tokens). The page then gates on accountFeatures instead of me, and the popout-aware fetch tags requests so the proxy forwards the session cookie and self-heals a lapsed session. The session resolves to the same popout-token authorization context, so retrieving the decrypted OBS WebSocket password (GET /v1/integrations/obs/credentials, popout-token-only) keeps working - a dashboard login still cannot read it. Saved OBS-dock URLs carrying the old ?token= continue to work unchanged.

Both pages fail open on an unreachable API during the SSR feature check: when the feature query throws, the page renders rather than showing the disabled screen.

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. Overlay bootstrap payloads stop carrying OBS credentials.
  • 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 server-side remote-control transport is live (ZAF-1122) and never fabricates state. The obsRemoteStatus query and GET /v1/obs-remote/status read the worker's live Redis snapshot - available: true with the real reading when a worker is publishing for the account, else available: false. The control mutations (obsControlStream, obsControlRecording, obsSwitchScene) and their /v1/obs-remote/* REST twins dispatch to the worker and only report success once it confirms execution against OBS; a missing/disconnected worker returns a 409 Conflict "OBS remote control is not connected" (CONFLICT) on both protocols. Invalid input returns a 400 with an identical message on both surfaces (scene_name is required; Invalid action: expected "start" or "stop").

  • OBS Remote Client (crates/lo-obs-remote/) - ObsRemoteClient handles WebSocket connections to OBS Studio, including authentication with the encrypted password. Used by both the OBS Remote worker (live status + control) and the connection test.
  • OBS Remote worker (apps/api/src/workers/obs_remote.rs) - a per-account worker that holds the OBS connection, polls ingest stats via IngestMonitor and emits events on threshold violations (low bitrate, high dropped frames, connection lost, recovery) with exponential-backoff reconnect. It also publishes a live ObsRemoteStatus snapshot to Redis (lumio:obs:status:{account_id}, TTL ≈ 3× poll interval) each cycle and subscribes to lumio:obs:command:{account_id} to execute scene/stream/recording controls, replying per-request so the handlers can confirm execution. Spawned and kept in sync by the reconcile loop (apps/api/src/workers/obs_reconcile.rs) for every remote_enabled OBS config.
  • Configuration storage - OBS config is stored as JSONB in the integration_configs table with platform = "obs", label = "OBS WebSocket", unique on (account_id, platform). The password field is encrypted using AES-256-GCM.

Frontend

  • /dashboard/obs - Scene grid plus stream/recording toggles and a connection badge, driven by the /api/obs-remote/* proxy routes. No port/password form.
  • /popout/obs - Compact dock version of /dashboard/obs using the same /api/obs-remote/* routes through a popout-aware fetch, polling every 5 s. Designed to be opened as a docked browser window alongside OBS. 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"); the ObsConfigCard holds the port / password / remote-toggle / remote-host form.
  • /overlay/[key] - the only surface that actually drives OBS, via a browser-side obs-websocket connection (see Overlay control relay).

apps/web/src/hooks/use-obs-websocket.ts exports a useObsWebSocket hook that fetches credentials via the popout-token endpoints and manages a client lifecycle. No component calls it - only its handleObsRelayAction export is in use.

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.
obsRemoteStatusobs:read + feature:obs_remoteLive remote connection status from the OBS worker's Redis snapshot: available (a worker is publishing), connected, stream/recording status, scene list, current scene, and ingest stats. Returns available: false with empty fields when no worker is running.

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 the obs-websocket connection from the API server. Requires remote_enabled - otherwise it errors with Connection test is only available when remote mode is enabled. 10-second timeout; connection failures come back as { success: false, error } rather than a GraphQL error.
obsControlStream(action)feature:obs_remote + obs:editControl streaming: "start" or "stop". Dispatched to the OBS worker; returns true only once execution is confirmed, 400 on an invalid action, or 409 CONFLICT "OBS remote control is not connected" when no live worker acknowledges.
obsControlRecording(action)feature:obs_remote + obs:editControl recording: "start" or "stop". Same dispatch/confirmation/error semantics as obsControlStream.
obsSwitchScene(sceneName)feature:obs_remote + obs:editSwitch the active OBS scene. Empty sceneName returns 400 "scene_name is required"; otherwise dispatched to the worker with the same confirmation/409 semantics.

REST Endpoints

MethodPathGuardsDescription
GET/v1/integrations/obsobs:read + feature:obs_remoteGet OBS config (has_password flag only, never the password).
PUT/v1/integrations/obsobs:edit + feature:obs_remote + integration:obs_websocketSave OBS config (upsert).
DELETE/v1/integrations/obsobs:delete + feature:obs_remote + integration:obs_websocketDelete OBS config. Returns 204.
GET/v1/integrations/obs/credentialspopout-token auth only + feature:obs_remoteGet decrypted OBS credentials. Carries no permission check - instead it rejects any auth context that is not a Popout Token with 403 This endpoint requires popout token authentication, so a normal dashboard login can never read the password.
GET/v1/integrations/obs/statusobs:read + feature:obs_remoteGet OBS configuration status (configured, port, remote settings).
POST/v1/integrations/obs/testobs:edit + feature:obs_remoteTest the obs-websocket connection from the API server.
GET/v1/obs-remote/statusobs:read + feature:obs_remoteLive remote connection status from the worker's Redis snapshot: available, connected, stream, recording, scenes, current_scene, ingest. Returns available: false with connected: false and empty scenes when no worker is running.
POST/v1/obs-remote/sceneobs:edit + feature:obs_remoteSwitch scene. Body { "scene_name": "..." }. Empty name → 400 "scene_name is required". Dispatched to the worker; 200 { "ok": true } once confirmed, else 409 Conflict "OBS remote control is not connected".
POST/v1/obs-remote/streamobs:edit + feature:obs_remoteStart/stop streaming. Body { "action": "start" | "stop" }. Invalid action → 400 "Invalid action: expected "start" or "stop"". Dispatched to the worker; 200 { "ok": true } once confirmed, else 409 Conflict "OBS remote control is not connected".
POST/v1/obs-remote/recordingobs:edit + feature:obs_remoteStart/stop recording. Body { "action": "start" | "stop" }. Same validation/confirmation/409 semantics as /v1/obs-remote/stream.

REST payloads are snake_case (remote_enabled, remote_host, has_password, scene_name, current_scene, created_at).

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.

All four /v1/obs-remote/* handlers enforce the plan feature feature:obs_remote (require_feature, fail-closed) - the same guard the GraphQL surface applies via FeatureGuard - before doing anything else. There is no separate require_pro helper (a former no-op placeholder was removed); the feature flag is the plan gate. See Plan gating below.

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

  1. User configures OBS settings (port, optional password, optional remote host) in /dashboard/connections → Integrations → OBS WebSocket.
  2. remote_host is validated against the private/loopback/link-local block-list, the password is encrypted via crypto::encrypt_versioned(), and the whole object is upserted into the integration_configs JSONB config.
  3. User can test the connection: the API server decrypts the password and attempts an obs-websocket handshake to ws://{remote_host}:{port} with a 10-second timeout, then disconnects.
  4. Stream/recording/scene commands from /dashboard/obs or /popout/obs reach /v1/obs-remote/*, which - after the obs:edit + feature:obs_remote gates - publishes the command to the account's OBS worker over Redis (lumio:obs:command:{account_id}) and waits for a per-request reply. The worker executes it against the live OBS connection and confirms; the handler returns success only on confirmation, or 409 Conflict "OBS remote control is not connected" when no live worker acknowledges within the timeout. The worker is spawned per remote_enabled config by the reconcile loop.
  5. For overlays, the credentials travel in the WebSocket bootstrap payload and the browser connects to OBS directly; automation-issued obs:action events are executed there. See Overlay control relay.

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 gating

OBS is a paid-tier feature, and the gating lives entirely in the plan_features matrix: feature:obs_remote, integration:obs_websocket and widget:obs_browser_source are all seeded disabled for the Free plan and enabled for Pro. Free and Pro are the only plans. Because the flags gate every OBS query, mutation and route, a Free account cannot use OBS at all - the restriction is not limited to remote_enabled: true.

There is no separate plan-limit column. The historical account_limits.obs_remote_allowed boolean was migrated into the feature:obs_remote account override and dropped. Enforcement is the feature:obs_remote guard itself - require_feature on every /v1/obs-remote/* route and FeatureGuard on every GraphQL field, both fail-closed. (A former no-op require_pro placeholder in apps/api/src/routes/obs_remote.rs was removed in ZAF-1090; it protected nothing.)

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 REST routes. Read the worker's live status snapshot and dispatch controls through services::obs_remote; 409 "not connected" only when no live worker acknowledges. Each still enforces feature:obs_remote.
apps/api/src/services/obs_remote.rsShared REST+GraphQL seam: read_status_snapshot() + send_command() (Redis request/reply with a bounded timeout) and the shared validation/error constants that keep both protocols parity-clean.
apps/api/src/workers/obs_remote.rsPer-account OBS worker: poll+status loop (live Redis snapshot + ingest monitoring/auto-actions) and command loop (executes scene/stream/recording controls, replies per-request).
apps/api/src/workers/obs_reconcile.rsProtocol-agnostic reconcile loop: spawns/restarts/stops OBS workers to match remote_enabled OBS integration configs; wired into API startup + graceful shutdown.
apps/api/src/services/bootstrap_provider.rsInjects the decrypted obs credentials block into the overlay WebSocket bootstrap
apps/api/src/dispatch.rsAutomation OBS action → Redis lumio:obs:action:{account_id}
crates/lo-obs-remote/Server-side ObsRemoteClient, IngestMonitor, ObsRemoteStatus
crates/lo-obs/Rust type definitions mirroring the Browser Source API. Currently has no dependents; the browser-side implementation lives in @lumio/obs.
shared/obs/src/client.tsTyped wrapper around window.obsstudio
shared/obs/src/ws-client.tsObsWebSocketClient - typed obs-websocket v5 client, used by the overlay
shared/obs/src/types.tsOBSStatus, OBSScene, OBSTransition, OBSEventType, OBSControlLevel
apps/web/src/hooks/use-obs-local-connect.tsBrowser-side obs-websocket connection for overlays
apps/web/src/hooks/use-obs-websocket.tshandleObsRelayAction relay dispatcher (plus an unused useObsWebSocket hook)
apps/api/src/crypto.rsAES-256-GCM password encryption/decryption
apps/web/src/contexts/obs-browser-source-context.tsxObsBrowserSourceProvider + useObsBrowserSource hook
apps/web/src/app/(overlay)/overlay/[key]/overlay-client.tsxMounts ObsBrowserSourceProvider, connects to local OBS, routes obs:action
apps/web/src/components/overlay/overlay-preview.tsxMounts ObsBrowserSourceProvider for the editor preview
apps/web/src/app/(main)/(app)/dashboard/obs/layout.tsxFeatureRouteGate feature="feature:obs_remote" on the whole segment
apps/web/src/app/(main)/(app)/dashboard/obs/obs-dashboard.tsxOBS Remote page (scene grid + stream/recording controls)
apps/web/src/app/(popout)/popout/obs/page.tsxCompact OBS Remote popout (SSR-gated on feature:obs_remote, popout-session aware). NOT a Browser Source.
apps/web/src/app/(main)/(app)/dashboard/connections/obs-config.tsxPort / password / remote-host form