OBS Overlays
Overlays are vanilla HTML/CSS/JS files located in app/overlays/. They connect to the StreamooS runtime via WebSocket and are used as Browser Sources in OBS.
Available Overlays
Built-in overlays live under app/overlays/builtin/<id>.streamoos/. Each ships a manifest.json (auto-discovered by overlay-discovery.ts), an HTML entry, CSS and JS.
| Overlay | Purpose |
|---|---|
timer.streamoos | Countdown with theme variants |
radio.streamoos | Radio: art + title/artist of the Radio Source picked in Music (KHInsider or the system player) |
twitch-alerts.streamoos | Twitch alerts (follow/sub/raid/cheer/gift_sub) with optional shiny effect |
twitch-events-ribbon.streamoos | Recent events scrolling ribbon - see Position variants below |
twitch-shoutout.streamoos | Shoutout: the channel /shoutout points at (avatar, last category) |
twitch-goal-bar.streamoos | Goal bar filled by the live counters (follows, subs, bits, viewers…) |
spectrum-analyzer.streamoos | Lightweight music visualizer (28 bars, pulse/wave modes) |
dock-obs.streamoos | Minimalist 3-view live dock (Music / Timer / Chat) |
Assets the user uploaded (custom alert images, etc.) are stored in SQLite and served by the overlay HTTP server at http://127.0.0.1:8766/overlays/asset/<id> - the renderer-side streamoos-asset:// protocol is only available inside Electron, so OBS browser sources use the HTTP route instead. The overlay JS rewrites streamoos-asset://<id> URIs to this path automatically (twitch_alerts.js#resolveAssetPath).
Adding an Overlay in OBS
In OBS, click + → Browser
Uncheck "Local file" and enter the absolute path:
file:///C:/Users/you/StreamooS/app/overlays/timer/index.html(Linux:
file:///home/you/StreamooS/app/overlays/timer/index.html)Recommended resolution: 1920×1080 or match your scene size
Enable "Refresh browser when scene becomes active"
The overlay automatically connects to
ws://127.0.0.1:8765on load. No additional configuration needed.
Composite Mode (multiple overlays, one OBS source)
The one-URL-per-overlay setup above still works unchanged and is the default. As an alternative, a composite source embeds several chosen overlays behind a single OBS Browser Source:
http://127.0.0.1:8766/overlays/composite/?preset=<id>- A preset (
id,name, list of{overlaySlug, kind: "permanent" | "event", position}) is stored server-side (overlay_composite_presetstable) and read via the WS commandoverlay_composite/get_preset. Presets are created/edited viawindow.streamoos.sendCommand(privileged renderer IPC) -save_preset/delete_presetare intentionally not reachable over the network-facing overlay WS, same astwitch/stream:save_preset. - Each embedded overlay runs in its own
<iframe src="../builtin/<slug>?composite=1">- it self-positions exactly as it would in a single-source setup. The composite page never moves an iframe. ?composite=1switches that overlay'sStreamooSClientinto proxy mode: it opens no socket of its own and relays through the parent's one real connection instead (see below).- Two
kind:"event"overlays configured at the samepositionnever render at once: each callsrequestOverlaySlot(position, durationMs)(fromstreamoos-client.js) before showing a transient item and queues (FIFO, nothing dropped) if another is already showing there.kind:"permanent"overlays (radio, timer, ribbon) never queue.
Full rationale and what's explicitly out of scope for now (a preset-editing UI, a permanent-vs-event fade handoff): doc/internal/adr/ADR-010-overlay-composite-mode.md.
Shared WebSocket Client (StreamooSClient)
All overlays use app/overlays/shared/websocket-client.js - a reusable class that handles connection, automatic reconnection, and update reception.
<script src="../shared/websocket-client.js"></script>
<script>
const client = new StreamooSClient("ws://127.0.0.1:8765");
// Receive state updates
client.on("update", (msg) => {
if (msg.feature === "timer") {
const { remainingSeconds, running } = msg.data;
// update the DOM
}
});
// Send a command
client.sendCommand("timer", "start", { minutes: 5, seconds: 0 });
</script>Constructor Options
new StreamooSClient(wsUrl, options);| Option | Default | Description |
|---|---|---|
wsUrl | ws://localhost:8765 | WS server URL |
reconnectBaseDelayMs | 1000 | Initial reconnect delay |
reconnectMaxDelayMs | 15000 | Maximum reconnect delay |
reconnectJitterMs | 250 | Random jitter added to delay |
connectTimeoutMs | 5000 | Connection timeout |
autoConnect | true | Auto-connect on instantiation |
Available Events
client.on("connected", () => {
/* connected */
});
client.on("disconnected", ({ willReconnect }) => {
/* disconnected */
});
client.on("update", (msg) => {
/* msg.feature, msg.data */
});
client.on("response", (msg) => {
/* command response */
});
client.on("error", (msg) => {
/* server error */
});Methods
client.sendCommand(feature, action, data); // send a command
client.send(rawObject); // send a raw object
client.disconnect(); // manual disconnect (no reconnect)Automatic Reconnection
The client implements exponential backoff with jitter:
- 1st attempt: ~1s
- Progression: doubling + jitter up to 15s max
- Infinite reconnect until explicit
disconnect()
Overlay Themes
Each overlay supports multiple visual themes. Themes are configured via the dashboard UI or by sending a set_overlay_config command via WebSocket.
| Overlay | Themes |
|---|---|
| KHInsider | streamoos, ghibli, xp, glass, noisy, minimal-dark |
| Timer | streamoos, minimal-dark |
| SMTC | streamoos, minimal-dark |
Overlay Position
All overlays support 9 anchor positions:
| Top | Middle | Bottom |
|---|---|---|
top-left · top-center · top-right | middle-left · center · middle-right | bottom-left · bottom-center · bottom-right |
Twitch Events Ribbon - placement variants
The ribbon overlay accepts a ?position= URL query param so streamers can pin it to a corner or edge instead of the default full-canvas curve:
| Value | Layout |
|---|---|
| omit | Original 1920×1080 wavy strip |
corner-top-left · corner-top-right · corner-bottom-left · corner-bottom-right | Compact 45 %-scaled ribbon anchored in a corner |
edge-left · edge-right | Rotated 90° so the ribbon scrolls vertically down a side |
band-top · band-bottom | Compressed horizontal strip along the chosen edge |
Add the value to the URL when configuring the OBS browser source, e.g. …/twitch-events-ribbon.streamoos/twitch_events_ribbon.html?position=edge-left.
Twitch Goal Bar - URL config
twitch-goal-bar.streamoos accepts these parameters:
| Parameter | Default | Description |
|---|---|---|
metric | follows | Tracked counter, see the table below |
target | 50 | Goal, a positive integer |
label | auto label | Text shown on the left, 60 characters max |
onReached | celebrate | freeze (bar stays full), celebrate (pulse), next (roll to the next target) |
step | = target | Increment used by onReached=next |
position | bottom-center | top-left · top-center · top-right · bottom-left · bottom-center · bottom-right |
theme | neon | neon · streamoos |
accent | #9147ff | Bar colour, hex |
Session and total are not the same number. "40 follows tonight" and "1000 followers total" are different metrics; the auto label says which one, a custom label will not do that for you.
metric | Scope | Source |
|---|---|---|
follows · subscriptions · giftSubs · cheerBits · raids | Session | EventSub events of the current session |
totalFollowers · totalSubs | Total | Helix poll (channel-wide count) |
viewersNow | Instant | Helix poll |
The total* and viewersNow metrics stay at zero off-stream: the Helix poll only runs while live.
Spectrum Analyzer - URL config
spectrum-analyzer.streamoos accepts the following query params:
| Param | Default | Description |
|---|---|---|
bars | 28 | Number of frequency bars (4–64) |
color | #00ff88 | Hex bar color |
mode | pulse | pulse (beat-driven) or wave (sinusoidal only) |
sensitivity | 5 | Amplitude multiplier (1–10) |
Configuring via WebSocket
{
"type": "command",
"feature": "smtc",
"action": "set_overlay_config",
"data": {
"enabled": true,
"radioSource": "auto",
"position": "bottom-right",
"showArtwork": true,
"showProgress": true,
"theme": "glass"
}
}Custom Overlays
StreamooS supports importing custom user-created overlays via the dashboard. Custom overlays are stored in the app's userData directory and can be managed through the preload bridge:
listCustomOverlays()-- list all imported overlaysaddCustomOverlay()-- import a new HTML overlayremoveCustomOverlay(id)-- remove a custom overlayopenCustomOverlay(id)-- open in an external window
Custom overlays connect to the same ws://127.0.0.1:8765 endpoint and can use app/overlays/shared/websocket-client.js.
Building a Custom Overlay
Minimal structure:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
body {
background: transparent;
margin: 0;
}
/* OBS-friendly styles */
</style>
</head>
<body>
<div id="display">Connecting...</div>
<script src="../shared/websocket-client.js"></script>
<script>
const client = new StreamooSClient();
client.on("connected", () => {
document.getElementById("display").textContent = "Connected";
client.sendCommand("timer", "get_state");
});
client.on("update", (msg) => {
if (msg.feature !== "timer") return;
const { remainingSeconds } = msg.data;
const m = Math.floor(remainingSeconds / 60)
.toString()
.padStart(2, "0");
const s = (remainingSeconds % 60).toString().padStart(2, "0");
document.getElementById("display").textContent = `${m}:${s}`;
});
</script>
</body>
</html>Allowed WebSocket Origins
The server validates the Origin header on every connection:
| Origin | Context |
|---|---|
null | OBS Browser Source (local file) |
file:// | Electron production |
http://127.0.0.1:5173 | Vite dev server |
Any other origin is rejected with HTTP 403.
