<% const controlledUrl = canViewSecrets ? `${baseUrl}/overlay/${overlay.public_token}` : ""; const value = (config, key, fallback = "") => config && config[key] !== undefined ? config[key] : fallback; const pickerColor = (input, fallback = "#ffffff") => { const normalized = String(input || "").trim(); if (/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(normalized)) return normalized.slice(0, 7); if (/^#[0-9a-f]{3,4}$/i.test(normalized)) return `#${normalized.slice(1, 4).split("").map((digit) => digit + digit).join("")}`; return fallback; }; const anchors = [ ["top-left", "Top left"], ["top-center", "Top middle"], ["top-right", "Top right"], ["center-left", "Middle left"], ["center", "Center"], ["center-right", "Middle right"], ["bottom-left", "Bottom left"], ["bottom-center", "Bottom middle"], ["bottom-right", "Bottom right"] ]; %> <%- include("partials/layout-top", { title }) %>
<%- include("partials/page-header", { eyebrow: "Stream overlay", pageTitle: overlay.name, description: "Arrange what viewers see, switch scenes, and copy the finished overlay into OBS.", actions: 'Back to overlays' }) %>
Overlay name and canvas size

Use this overlay in OBS

<% if (!canViewSecrets) { %>
Only administrators can view or copy the private OBS links.
<% } else { %>

Add this as a Browser Source in OBS. It follows the live scene selected below. Keep the link private. If the overlay uses video, audio, or alert sounds, enable Control audio via OBS in the Browser Source properties.

OBS Browser Source setup

Use Lumi's canvas size for the Browser Source so websites, text, and alerts use the same layout in OBS as they do in this preview.

Width<%= overlay.canvas_width || 1920 %>
Height<%= overlay.canvas_height || 1080 %>
Open clean preview
<% } %>

Scenes and sources

Scenes are layouts. Sources are the text, images, and websites inside them.

<% if (overlay.scenes.length > 1) { %>
<% overlay.scenes.forEach((scene) => { %><% }) %>

Select a scene to move it one place to the right.

<% } %> <% overlay.scenes.forEach((scene) => { const fixedUrl = canViewSecrets ? `${controlledUrl}/scene/${scene.public_token}` : ""; %>
> <%= scene.name %> · <%= scene.enabled ? "On" : "Off" %><%= scene.id === overlay.active_scene_id ? " · Live" : "" %>
<% if (canViewSecrets) { %> <% } %>

Sources

<% if (!scene.modules.length) { %>
Nothing has been added to this scene yet.
<% } %> <% scene.modules.forEach((module) => { const config = module.config || {}; %>
<%= module.name %><%= moduleTypes.find((item) => item.id === module.type)?.label || module.type %> · <%= module.enabled ? "Visible" : "Hidden" %>
Text color
" pattern="#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})" title="Use #RGB, #RGBA, #RRGGBB, or #RRGGBBAA" aria-label="Text color value" data-color-text />" aria-label="Choose text color" data-color-picker />
Background
" pattern="(?:transparent|#[0-9a-fA-F]{3}|#[0-9a-fA-F]{4}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{8})" title="Use transparent, #RGB, #RGBA, #RRGGBB, or #RRGGBBAA" aria-label="Background color value" data-color-text />" aria-label="Choose background color" data-color-picker />
Chats to include
<% [["twitch","Twitch"],["youtube","YouTube"],["discord","Discord"]].forEach(([id,label]) => { %><% }) %>
Text color
" aria-label="Chat text color value" data-color-text />" aria-label="Choose chat text color" data-color-picker />
Name color
" aria-label="Default username color value" data-color-text />" aria-label="Choose default username color" data-color-picker />
Source background
" aria-label="Chat background color value" data-color-text />" aria-label="Choose chat background" data-color-picker />
Message background
" aria-label="Message background color value" data-color-text />" aria-label="Choose message background" data-color-picker />
Message details
Sound
Player controls

Enable “Control audio via OBS” on the Lumi Browser Source to mix this sound separately in OBS.

Player controls

Enable “Control audio via OBS” on the Lumi Browser Source to place this sound in the OBS mixer.

Website CSS

Recommended. Turn this off only if a website will not load through CSS injection.

Transparent page

Recommended for alerts. Lumi reapplies transparency if the website changes its page after loading.

Automatic recovery
Check website replies
Position and size
Saved settings
<% }) %> <% if (scene.modules.length > 1) { %>
<% scene.modules.forEach((module) => { %><% }) %>
<% } %>
Add a source

Lumi combines live messages from the services you select. You can adjust the appearance, animations, timeout, and CSS after adding it.

Chats to include
<% }) %>
<% if (canManageObs) { %>
Connect Lumi to OBS Optional

This can switch scenes in OBS and keep OBS scenes matched with Lumi scenes. Most users can leave it off.

Connection<%= obsStatus.state %>
Current OBS scene<%= obsStatus.current_scene || "Unknown" %>
Page permission<%= obsStatus.control_level_label || "Not reported" %>
Current transition<%= obsStatus.current_transition || "Unknown" %>
OBS canvas<%= obsStatus.canvas_width && obsStatus.canvas_height ? `${obsStatus.canvas_width} × ${obsStatus.canvas_height}` : "Unknown" %>
Outputs<%= obsStatus.outputs?.streaming ? "Streaming" : obsStatus.outputs?.recording ? "Recording" : "Idle or unavailable" %>
Profile<%= obsStatus.profiles_supported === false ? "Not exposed by Browser Bridge" : (obsStatus.current_profile || "Unknown") %>
Scene collection<%= obsStatus.scene_collections_supported === false ? "Not exposed by Browser Bridge" : (obsStatus.current_scene_collection || "Unknown") %>
Browser Source<%= obsStatus.source_active === true ? "Active" : obsStatus.source_visible === true ? "Visible" : "Unknown" %>
Problem<%= obsStatus.error || "None" %>

Applying the detected setup adds missing OBS scenes, matches same-name scenes, uses the OBS canvas size, and selects the current scene. It never deletes Lumi scenes.

<% } %>
<%- include("partials/layout-bottom") %>