- 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
6.9 KiB
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 incfg/nodecg-venue-gfx.json(obs.password).
The fast path: import the generated scene collection
From the bundle directory:
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:
- Open the
PROGRAMscene 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. - Point the Stinger transition at a real video file (transition dock →
double-click Stinger). If you passed
--stinger /abs/path.webmto the generator this is already done. - 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.
- 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 nameMove) — all ad switches: the shrink and the corner-to-corner hops. Duration 700–800 ms. WithAdOverlaymatched in every scene, only the content scene ever moves. - Stinger (
obs.transitions.break, default nameStinger) — 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:
-
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×1080AdOverlay→…/graphics/ad.html, 1920×1080BreakSlide→…/graphics/break.html, 1920×1080
For all three: uncheck "Shutdown source when not visible" (keeps the ticker scrolling and replicant connections alive across switches).
-
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.
-
Create the two transitions in the Scene Transitions dock.
-
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) foradDurationSeconds, then back. - Break toggled on: →
BREAK; everybreakGapSecondsa burst ofbreakAdsPerBurstads, eachbreakAdDurationSecondslong, 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.