# 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: - **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.