Files
dvdrw 4bb64a8ca3 init: initial commit
- 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
2026-07-15 19:30:32 +02:00

6.9 KiB
Raw Permalink Blame History

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:

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 700800 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:

    • TickerOverlayhttp://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: MAINMAIN_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.