4bb64a8ca3
- ticker / QR ad overlay / break slide graphics (1920×1080) with all geometry derived from one shared module (src/shared/obs-geometry.mjs) - OBS scene-collection generator (scripts/generate-obs-scenes.mjs) and a read-only in-dashboard setup verifier, both driven by the same module - en/sr dashboard i18n selected via bundle config; configschema.json with neutral first-run defaults seeded from optional branding config - install.sh / systemd user service / update.sh for venue deployment - reference OBS scene collection + profile from a real venue as fixtures
143 lines
6.9 KiB
Markdown
143 lines
6.9 KiB
Markdown
# OBS setup
|
||
|
||
Target: one 1920×1080 venue screen driven by OBS (no stream required).
|
||
NodeCG switches scenes over obs-websocket; exeldro's **Move** plugin animates
|
||
the program shrink between scenes; a **Stinger** covers the MAIN<->BREAK
|
||
toggle.
|
||
|
||
## Prerequisites
|
||
|
||
- OBS Studio **31 or newer** (the generated scene collection uses the current
|
||
collection format).
|
||
- **Move plugin** by exeldro: <https://obsproject.com/forum/resources/move.913/>
|
||
- **WebSocket server**: Tools → WebSocket Server Settings → Enable, port
|
||
`4455`, set a password. Put the same password in `cfg/nodecg-venue-gfx.json`
|
||
(`obs.password`).
|
||
|
||
## The fast path: import the generated scene collection
|
||
|
||
From the bundle directory:
|
||
|
||
```sh
|
||
npm run obs:scenes # writes obs/generated/VenueGfx.json
|
||
# options: node scripts/generate-obs-scenes.mjs --help
|
||
```
|
||
|
||
Then in OBS: **Scene Collection → Import**, pick the file, switch to the new
|
||
collection, and finish up:
|
||
|
||
1. Open the `PROGRAM` scene and replace the **Program Placeholder** color
|
||
source with your real feed (camera, capture card, window capture, …),
|
||
1920×1080 at (0, 0). Everything else references this scene, so this is the
|
||
only place the feed lives.
|
||
2. Point the **Stinger** transition at a real video file (transition dock →
|
||
double-click Stinger). If you passed `--stinger /abs/path.webm` to the
|
||
generator this is already done.
|
||
3. Settings → Video: Base and Output **1920×1080**, **60 FPS** (the ticker
|
||
scrolls; 30 looks choppy). Encoder/output settings are yours to pick —
|
||
the graphics don't care.
|
||
4. Open the dashboard's **OBS setup check** panel and hit *Run checks* —
|
||
fix anything red, re-run until it's green (warnings are informational).
|
||
|
||
The generator reads scene/transition/source names from the bundle cfg
|
||
(`--config ../../cfg/nodecg-venue-gfx.json`), so a renamed setup stays
|
||
consistent across the graphics, the generated collection, and the verifier —
|
||
they all share one geometry module (`src/shared/obs-geometry.mjs`).
|
||
|
||
## How the scenes are built (what the generator emits)
|
||
|
||
Two **nested scenes** hold the content:
|
||
|
||
| Scene | Items (bottom → top) |
|
||
| ------------- | ----------------------------------------------- |
|
||
| `PROGRAM` | Program Placeholder (your feed), `TickerOverlay` |
|
||
| `BREAK_SLIDE` | `BreakSlide` browser source, `TickerOverlay` |
|
||
|
||
The ticker lives *inside* the content scene on purpose: when the program
|
||
shrinks to 70%, the ticker shrinks with it, and the ad graphic owns the full
|
||
L-shaped gap down to the screen edges.
|
||
|
||
Six **driven scenes** are what NodeCG switches between. Each stacks, bottom →
|
||
top: `AdOverlay` (fullscreen, identical transform everywhere) → the nested
|
||
content scene:
|
||
|
||
| Scene | Content scene | Content transform |
|
||
| ------------- | ------------- | ----------------- |
|
||
| `MAIN` | `PROGRAM` | (0, 0) ×1.0 |
|
||
| `MAIN_AD_TR` | `PROGRAM` | (576, 0) ×0.7 |
|
||
| `MAIN_AD_BL` | `PROGRAM` | (0, 324) ×0.7 |
|
||
| `BREAK` | `BREAK_SLIDE` | (0, 0) ×1.0 |
|
||
| `BREAK_AD_TR` | `BREAK_SLIDE` | (576, 0) ×0.7 |
|
||
| `BREAK_AD_BL` | `BREAK_SLIDE` | (0, 324) ×0.7 |
|
||
|
||
Why `AdOverlay` sits at the bottom of **every** scene: under the fullscreen
|
||
content it's invisible, but because it exists on both sides of every ad
|
||
transition with an identical transform, Move leaves it perfectly static — the
|
||
shrinking program physically *uncovers* the ad that was already painted
|
||
underneath. The ad graphic only paints the gamma cutout, which the
|
||
corner-parked program never overlaps.
|
||
|
||
### Transitions
|
||
|
||
- **Move** (`obs.transitions.default`, default name `Move`) — all ad
|
||
switches: the shrink and the corner-to-corner hops. Duration 700–800 ms.
|
||
With `AdOverlay` matched in every scene, only the content scene ever moves.
|
||
- **Stinger** (`obs.transitions.break`, default name `Stinger`) — only for
|
||
MAIN↔BREAK. Set the Transition Point to the frame where the video fully
|
||
covers the screen (the generator defaults to 1200 ms).
|
||
|
||
NodeCG selects the right transition over the websocket before every switch,
|
||
so whichever is "active" in the dock doesn't matter. If a named transition is
|
||
missing, the bundle logs a warning and switches with whatever is active —
|
||
nothing breaks. Do **not** set per-scene Transition Overrides on `BREAK`: the
|
||
bundle already picks the stinger only for the break toggle; an override would
|
||
also play it on every return from a break-ad burst.
|
||
|
||
obs-websocket cannot *create* transitions — that's exactly why the generator
|
||
exists. It also can't read settings of inactive transitions, which is why the
|
||
verify panel reports those as "skip: spot-check in OBS".
|
||
|
||
## Manual setup (appendix)
|
||
|
||
If you'd rather build the collection by hand, recreate the tables above:
|
||
|
||
1. Create the three **browser sources** once (Add → Browser), then reference
|
||
the *same instances* everywhere else via Add → **Add Existing** — Move
|
||
matches sources by name, duplicates would break the animation:
|
||
- `TickerOverlay` → `http://localhost:9090/bundles/nodecg-venue-gfx/graphics/ticker.html`, 1920×1080
|
||
- `AdOverlay` → `…/graphics/ad.html`, 1920×1080
|
||
- `BreakSlide` → `…/graphics/break.html`, 1920×1080
|
||
|
||
For all three: **uncheck "Shutdown source when not visible"** (keeps the
|
||
ticker scrolling and replicant connections alive across switches).
|
||
2. Build the two nested scenes, then the six driven scenes per the tables.
|
||
Set transforms precisely via right-click → Transform → Edit Transform; at
|
||
×0.7 the content is 1344×756. Keep every transform top-left aligned.
|
||
3. Create the two transitions in the Scene Transitions dock.
|
||
4. Run the **OBS setup check** panel until green — it compares the live OBS
|
||
state against the same geometry the graphics render from, normalizing
|
||
alignment and allowing ±5 px, so a hand-built setup verifies fine.
|
||
|
||
All names (scenes, transitions, sources) are configurable in
|
||
`cfg/nodecg-venue-gfx.json`; the verifier and generator follow the cfg.
|
||
|
||
## What NodeCG does at runtime
|
||
|
||
The bundle only ever selects a transition (`SetCurrentSceneTransition`) and
|
||
switches scenes (`SetCurrentProgramScene`) — all animation lives in OBS/Move,
|
||
which is the reliable path (remotely toggling Move Source filters has a known
|
||
settings-refresh bug over websocket).
|
||
|
||
- Every `normalAdIntervalMinutes`: `MAIN` → `MAIN_AD_TR`/`_BL` (alternating
|
||
corners) for `adDurationSeconds`, then back.
|
||
- Break toggled on: → `BREAK`; every `breakGapSeconds` a burst of
|
||
`breakAdsPerBurst` ads, each `breakAdDurationSeconds` long, hopping
|
||
diagonally between corners.
|
||
- Manual override buttons live on the "Choreography / OBS" dashboard panel.
|
||
- If OBS restarts, NodeCG reconnects and immediately re-applies the scene the
|
||
state machine expects.
|
||
|
||
If during break ads the *camera* should shrink into the corner instead of the
|
||
break slide, put `PROGRAM` instead of `BREAK_SLIDE` into
|
||
`BREAK_AD_TR`/`BREAK_AD_BL` — an OBS-only change, no code involved.
|