# OBS overlays

Admins manage overlays at **Admin → OBS overlays**. Management pages use the
normal Lumi session, role checks, layout, forms, lists, modals, and confirmation
flow. Browser Source pages use a separate transparent document with no Lumi
navigation or authenticated website data.

## Editing an overlay

The editor uses the same basic model as OBS: scenes contain ordered sources, and
the selected source has a red box with resize handles. On a wide screen the
preview stays beside the settings; on smaller screens it moves above them.

The preview and clean OBS page now render one fixed, full-resolution canvas and
scale that finished canvas to the available display area. Embedded alert pages
therefore receive the same CSS-pixel viewport in the editor and in OBS instead
of switching responsive layouts when the management preview is smaller. Set the
OBS Browser Source width and height to the Lumi canvas dimensions shown in the
editor (normally 1920 by 1080).

- Drag a source to move it.
- Drag any corner or edge handle to resize it.
- Use arrow keys for small moves, or Shift plus an arrow for larger moves.
- Snapping uses canvas edges, center lines, and other source edges/midpoints.
- Hold Ctrl on Windows/Linux or Cmd on macOS to temporarily reverse the Snap
  setting.
- Center, Fit to canvas, and Reset box provide familiar transform shortcuts.

Every source can use any corner, edge midpoint, or center as its anchor. Text
sources have separate horizontal and vertical alignment plus wrapping, shrinking,
clipping, single-line, and scrolling ticker behavior. The editor only shows fields
that apply to the selected source type.

## Private OBS links

Each overlay has one server-controlled URL. It follows the active overlay scene.
Each scene also has a fixed URL that always renders that scene. Both URL forms
contain 256-bit random bearer tokens instead of database IDs. Tokens are stored
encrypted for admin copy actions and as SHA-256 hashes for public lookup.
Regenerating a URL immediately revokes the old token.

Treat every Browser Source URL as a password. Only admins can view, copy, preview,
or regenerate it. Public state responses contain render data only; they do not
contain other URLs, tokens, OBS credentials, or website session data.

Browser Sources receive scoped live-change events through Lumi's existing event
service. The render page fetches a fresh authorized snapshot after each event and
after every reconnect, so temporary network loss does not require an OBS refresh.

## Scenes and sources

An overlay always keeps at least one scene. Admins can create, edit, duplicate,
order, enable, activate, and delete scenes. Current built-in source types are:

- Text
- Image URL
- Video / media URL
- Audio URL
- Lumi chat
- Website or alert overlay

Module instances can be created, edited, duplicated, ordered, enabled, and
deleted. New module types can call `registerOverlayModuleType(...)` in
`src/services/overlay-modules.js`; providers normalize their configuration and
select the safe built-in `text`, `image`, `video`, `audio`, `chat`, or `web` render type while the overlay
service retains the same instance model.

### Lumi chat

The built-in **Lumi chat** source combines messages already received by Lumi
from Twitch, YouTube, and Discord. Administrators can select any combination of
those services and optionally filter by channel name or ID. Channel filters
accept one value per line and may be prefixed with the service, such as
`twitch:cozycarnage` or `discord:community-chat`.

Chat sources support transparent source and message backgrounds, font family,
size, weight and whole-message alignment, name/text colors, avatars, badges, platform labels,
timestamps, message spacing, rounded corners, maximum visible messages,
top-or-bottom placement, timeout removal, and separate entry and exit
animations. Message cards fit their contents up to the source boundary rather
than filling its width. Entry and exit slides can travel left, right, up, or
down. Native Twitch emotes, global and channel BetterTTV emotes, and Discord
custom or animated emoji are displayed as images. BetterTTV channel and shared
emotes override a same-named global emote, matching normal channel behavior.
Discord GIF links, GIF attachments, GIFV embeds, and stickers are shown
as bounded inline media at roughly two text lines tall. The editor continuously
cycles several short and wrapped sample messages so both static appearance and
entry/exit behavior can be configured before the stream is live.

Lumi includes messages sent by its own connected bot accounts. Platform marks
are shown as icons, and chat badges are rendered as images. Twitch badge artwork
is resolved from Twitch's global and channel badge catalogs; a local image
fallback is used when a provider does not expose badge artwork. Twitch profile
images are resolved through the Twitch user catalog and cached to avoid a lookup
for every message. BetterTTV catalogs are likewise cached, and API failures fall
back to ordinary message text without interrupting chat delivery.

The optional hidden-user list accepts one rule per line in the form
`platform:username|user-id`. Twitch, YouTube, and Discord rules match only that
platform. A `lumi:internal-name|user-id` rule uses Lumi's linked identity model,
so one rule hides every connected platform identity belonging to that Lumi user.
Filtering is enforced before a message enters an overlay's scoped event stream
and is also checked by the renderer.

Advanced CSS is isolated to that chat source and is appended after the visual
settings. Stable selectors include `.chat`, `.message`, `.avatar`, `.content`,
`.meta`, `.platform`, `.author`, `.badge`, `.timestamp`, `.text`, `.emote`,
`.media-row`, and `.media`.
Messages arrive through the same token-scoped overlay event connection as scene
updates. Lumi keeps a small, short-lived in-memory replay window so a temporary
Browser Source reconnection can restore recent messages without writing chat
content to the database.

Every saved Lumi chat source also provides an **OBS chat dock** link to
administrators. Add it from **OBS → Docks → Custom Browser Docks**. This
standalone page uses that source's saved services, channel and hidden-user
filters, typography, colors, message details, limits, timeout, and custom CSS.
It intentionally ignores overlay position, source width/height, and entry/exit
animations. The page fills the available dock width, grows and scrolls like a
normal webpage, follows new messages while at the bottom, and leaves the scroll
position alone while an operator reviews older messages.

Treat a chat dock link as a password. It is signed and shown only to admins.
Replacing the overlay or scene private links also revokes affected dock links;
deleting or disabling the chat source makes its dock unavailable. Lumi also
revalidates credentials on open live-event streams and closes them after
revocation.

## OBS replay commands

The core `!clip [optional label]` command can save the active replay buffer
through either operational OBS connector while also requesting a clip from the
current streaming platform. Local OBS WebSocket uses `SaveReplayBuffer`. The
Browser Bridge uses OBS's native page binding and needs **Basic access to OBS**
or higher in Browser Source properties. The replay buffer must already be
running. Lumi selects one connected connector with an active replay buffer so a
setup with several overlays does not save duplicate replay files.

### Video and audio

Video sources support browser-playable media such as WebM and MP4; audio sources
support browser-playable formats such as Ogg, MP3, and WAV. Actual codec support
follows the Chromium/CEF build used by the browser or OBS.

Playback can start once when the overlay scene loads, loop continuously, or wait
for manual playback. Sources also provide volume, start offset, playback speed,
optional player controls, and a live **Restart playback** action. Video adds mute
and contain/cover/stretch sizing. Activating another Lumi scene removes the old
scene's media elements, which stops their playback; returning to the scene applies
its configured start behavior again.

Website overlays can play their own alert audio because their iframe receives
autoplay permission. To route video, audio, and website-alert sound through an
independent OBS mixer channel, enable **Control audio via OBS** in the Lumi Browser
Source properties, then configure monitoring/output in OBS as desired.

## Event alerts and sound alerts

Open an overlay and use **Event alerts** to connect a live service event to a
source. **Play media once** restarts an Audio or Video source and waits for that
playback to finish before the next alert for the same source begins. For a sound
alert, set the Audio source playback option to **Wait for manual playback**.
**Show source temporarily** keeps its Text or Image source hidden in clean OBS
output until an event arrives, displays it for the selected duration, and hides
it again. The editor still shows the source so it can be positioned normally.

The built-in event inputs are Twitch follows, incoming raids, subscriptions,
gifted subscriptions, and Discord member joins. Use **Connect Twitch events**
once as an administrator to grant the follower/subscription scopes. The token is
stored only on the server, refreshes automatically, and is never included in an
overlay page. Twitch events reconnect after temporary network loss. Discord
member joins use the existing bot connection and require the Server Members
Intent to be enabled for the bot in the Discord Developer Portal.

Use **Test alert** while either the pop-out preview or OBS Browser Source is open.
Lumi reports when no matching Browser Source is connected. An event only affects
the source when its scene is the one currently rendered; fixed scene links
therefore remain isolated. Plugins can add event types through the shared
`events.registerType(...)` and `events.emit(...)` API received during plugin
initialization.

### External website and alert overlays

Website sources support third-party browser overlays such as alert and chat
overlays. They run in a sandbox and do not receive the Lumi admin page or its
login data. The **Refresh now** action sends a live refresh command to every open
OBS copy of that source.

Automatic recovery is optional. When enabled it watches for a page that never
finishes loading, can check public website replies for HTTP errors, empty content,
unexpected content types, and obviously malformed pages, retries at 5, 15, and 45
second-style bounded intervals, and periodically reloads the source to avoid stale
content. Retry count and refresh interval are configurable. Local/private website
addresses are periodically refreshed but are not fetched by the Lumi server for
security; browser load-timeout recovery still applies.

Website sources support per-edge percentage cropping, zoom, and document-level
Website CSS. Lumi retrieves the public website document through a scoped,
revocable source URL and appends the stylesheet to its `<head>`, matching how OBS
injects Custom CSS after the main document loads. The proxied document runs in a
sandboxed opaque origin, receives no Lumi login/session data, does not expose the
overlay token in its location, and cannot use Lumi as a private-network proxy.

CSS injection is enabled by default and can target the website's own elements,
for example `body`, `.alert`, or provider-specific containers. Direct
compatibility mode is available for private/local pages and unusual sites that
cannot run through document injection; direct mode intentionally cannot change
cross-origin page content.

**Keep the website background transparent** is enabled by default. It protects
the page, body, and common application roots with a transparent background and
reapplies that protection when a late-loading alert application changes its DOM
or inline styles. It can be disabled for a website that intentionally needs an
opaque page background.

Lumi applies OBS's standard transparent Browser Source CSS to every clean render
page and shows the same copyable value in the Browser Source setup section:

```css
body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }
```

The Lumi document, canvas stage, embedded frame surface, and injected website
body are transparent by default. Administrators can extend or override the
default stylesheet per source just as they do in OBS Browser Source properties.

## Visual editing workflow

Existing sources use a draft preview workflow. Changing a field sends the draft
through the same server normalizer used for saving and updates the canvas after a
short delay without changing the stored source. Website URLs and CSS receive a
short-lived, access-controlled preview document, so their unsaved appearance is
also visible. Each changed card is marked as unsaved and offers **Save source**
or **Undo unsaved changes**.

The preview can switch between transparency checkerboard, dark, and light
backgrounds to expose unwanted opaque areas or low-contrast text. An optional
grid adds five-percent lines and canvas midpoints. Source save controls remain
sticky at the bottom of long settings panels, while drag, resize, keyboard
nudge, anchors, snapping, crop, zoom, and precise number fields remain available.

## OBS WebSocket

The connector is disabled by default. Enable OBS WebSocket in OBS, keep its
password enabled, and use the default local endpoint
`ws://127.0.0.1:4455` when Lumi runs on the streaming computer. If Lumi runs on
another trusted machine, use a private LAN/VPN address and firewall rules. Do not
port-forward or publicly expose OBS WebSocket.

The admin page can test the connection, read OBS scenes, change the current OBS
scene, and map OBS scene names to overlay scenes. Sync can run in either direction
or bidirectionally. Source tagging and a short outbound suppression window prevent
sync loops. Connector errors never stop overlay rendering, and optional reconnect
uses bounded exponential backoff.

OBS passwords use authenticated AES-256-GCM encryption derived from Lumi's stored
session secret. They are never returned by public APIs or rendered for
non-admins.

## OBS Browser Bridge

The optional **OBS Browser Bridge** uses the `window.obsstudio` bindings that OBS
injects into Browser Sources. It does not need an OBS WebSocket address or
password. Select the provider, save it, then load the private Lumi overlay URL in
OBS. The render page reports a heartbeat every five seconds and immediately after
relevant OBS events.

Use the minimum Page permission needed:

- **Read access to user information** reads the current scene, scene list,
  transitions, canvas dimensions, output status, and Browser Source activity.
- **Advanced access to OBS** is needed only when Lumi should change OBS scenes.
- Full access is not requested or used by the current bridge.

Lumi elects one recently active Browser Source instance per overlay as the bridge,
targets commands to that instance, and deduplicates command IDs. Existing
scene-direction checks and outbound suppression prevent synchronization loops. If
the source stops reporting, Lumi marks the bridge as waiting instead of breaking
overlay rendering. Leave OBS's **Shutdown source when not visible** option off if
the bridge must remain available outside the source's active scene.

The **Apply detected OBS setup** action adds missing OBS scenes, maps same-name
Lumi scenes, adopts the detected OBS canvas size, and selects the current OBS
scene. It does not delete existing Lumi scenes. OBS Browser bindings do not expose
profile or scene-collection names, so those fields are explicitly reported as
unavailable; the OBS WebSocket provider can read them when that metadata is
required.

## Connector extension point

`src/services/overlay-connectors.js` owns the provider registry. A provider
creates a connector with these operations:

- `connect()` and `disconnect()`
- `status()`
- `listScenes()` and `getActiveScene()`
- `setActiveScene(sceneName)`
- `onSceneChanged(listener)` and `onClosed(listener)`

The reserved `trusted_lumi_client` provider is intentionally unavailable. A
future trusted desktop client can implement this interface to transport OBS state
and commands without changing overlay routes, mappings, or synchronization rules.
Plugins can register providers, module types, or future scoped access providers
through `global.lumiFrameworks.overlays`.

Run focused verification with `npm run verify:overlays`.
