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
This commit is contained in:
2026-07-15 17:43:05 +02:00
commit 4bb64a8ca3
71 changed files with 18186 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
# 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 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:
- `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.
+13
View File
@@ -0,0 +1,13 @@
{
"locale": "en",
"branding": {
"tickerBlurb": "My Venue ♪",
"breakHeading": "Stay tuned — there's more!",
"sampleQrUrl": "https://example.com/my-venue"
},
"obs": {
"enabled": true,
"url": "ws://127.0.0.1:4455",
"password": "changeme"
}
}