# nodecg-venue-gfx Broadcast graphics for a venue screen, as a [NodeCG](https://nodecg.dev) 2 bundle: an always-on scrolling ticker with operator-added TTL messages, corner-alternating ads that shrink the live program into a corner, a break announcement slide, and automated + manual OBS scene choreography over obs-websocket. Built for festivals and concerts; generic enough for any single-screen show. - All on-screen text is edited live from the dashboard - Support for multiple locales - OBS setup is generated. One script emits an importable scene collection, and a dashboard panel verifies a live OBS against the exact geometry the graphics render from. - If OBS dies mid-show, the scheduler keeps running and re-applies the right scene the moment OBS comes back. If NodeCG dies, restart it and it comes back in live mode with all content intact. ## Quick start **Fresh venue machine** (installs NodeCG + this bundle + config + OBS scene collection in one go): ```sh curl -fsSLO https://git.dvdrw.dev/kckljajicevo/nodecg-venue-gfx/raw/branch/master/scripts/install.sh bash install.sh ``` **Into an existing NodeCG install:** ```sh cd /bundles git clone https://git.dvdrw.dev/kckljajicevo/nodecg-venue-gfx.git cd nodecg-venue-gfx npm ci && npm run build ``` Then: 1. Write `cfg/nodecg-venue-gfx.json` (see [docs/examples/cfg.example.json](docs/examples/cfg.example.json)). 2. Set up OBS: `npm run obs:scenes` -> import -> done. Details in [docs/OBS-SETUP.md](docs/OBS-SETUP.md). 3. Start NodeCG (`node index.js` from the install root) and open the dashboard at . To run as a service that survives reboots: `scripts/install-service.sh` (systemd user unit). To update later: `scripts/update.sh`. ## Configuration `cfg/nodecg-venue-gfx.json` in the NodeCG install, validated against [configschema.json](configschema.json). Everything is optional: | Key | Default | Purpose | | --- | --- | --- | | `locale` | `"en"` | Dashboard language (`"en"` or `"sr"`) | | `branding.tickerBlurb` | `"Welcome ♪"` | First-run seed for the recurring ticker blurb | | `branding.breakHeading` | `"Stay tuned — there's more!"` | First-run seed for the break-slide heading | | `branding.sampleQrUrl` | `"https://example.com"` | First-run seed for the sample ad's QR URL | | `obs.enabled` | `false` | Connect to OBS and drive scenes | | `obs.url` | `"ws://127.0.0.1:4455"` | obs-websocket URL | | `obs.password` | `""` | obs-websocket password | | `obs.scenes.*` | `MAIN`, `MAIN_AD_TR`, … | Names of the six driven scenes | | `obs.transitions.*` | `Move`, `Stinger` | Names of the two transitions | | `obs.sources.*` | `TickerOverlay`, … | Names of the shared sources/nested scenes (for the generator + verifier) | `branding.*` values only seed the very first run — after that, everything is edited from the dashboard and persists in NodeCG's database. Set `obs.enabled: false` to develop without OBS; everything else keeps working. ## Dashboard panels - **Ticker messages** — ticker settings (loop blurb, separator, default TTL, speed) and message management; each active message shows a depleting fill for its remaining time, with renew/remove/inline-edit. - **Ads** — CRUD for ads (strip text, column text, column image from the Assets dialog, QR URL, enabled), with a live QR preview per row. Each row has a "Trigger now" button to force that specific ad onto air immediately (enabled or not), bypassing the round-robin — disabled while an ad slot is already showing. - **Choreography / OBS** — connection status, mode/phase badges, countdown to the next automatic transition, break-slide content, and manual controls: show ad now (round-robin), skip ad, break toggle. - **Timing** — the choreography timing settings: ad interval, ad duration, break gap, break ad duration, ads per burst. - **OBS setup check** — read-only verification of the live OBS against the expected scenes/sources/transforms/transitions, with concrete fixes. Images (ad backgrounds, column images, break background, act logos) are uploaded through NodeCG's standard **Assets** dialog. ## Graphics (1920×1080) | URL | Default OBS source | Purpose | | --- | --- | --- | | `graphics/ticker.html` | `TickerOverlay` | Bottom 96 px scrolling ticker | | `graphics/ad.html` | `AdOverlay` | QR + headline in the gamma-shaped cutout | | `graphics/break.html` | `BreakSlide` | Break announcement slide (ambient motion) | The ticker uses a segment-queue marquee: loop content is snapshotted per revolution, so adding/removing messages mid-scroll never causes visual jumps. ## Theming - **Colors**: edit the `THEME LAB` block in [`src/graphics/index.css`](src/graphics/index.css) — every brand color the graphics use is a token there. - **Fonts**: the `FONT LAB` block right above it (Margarine is bundled as the default; drop a `.ttf`/`.woff2` into `src/graphics/fonts/` to swap). - **Geometry** (canvas size, the 70% shrink, ad cutout, OBS transforms): [`src/shared/obs-geometry.mjs`](src/shared/obs-geometry.mjs) — one module shared by the graphics, the scene-collection generator, and the verify panel, so they can never disagree. v1 is fixed at 1920×1080. ## Adding a language 1. Copy `src/i18n/en.ts` to `src/i18n/.ts` and translate (the `Strings` type keeps you honest). 2. Register it in `src/i18n/index.ts` (`locales`). 3. Add the code to the `locale` enum in `configschema.json`. ## Development ```sh npm run dev # parcel watch + nodemon restarting NodeCG npm run build # one-shot production build npm run obs:scenes # regenerate the OBS scene collection ``` Dev mode assumes the bundle lives inside a NodeCG install (`bundles/nodecg-venue-gfx`); `nodemon` runs `node ../../index.js`. Rules of thumb: - **Graphics/dashboard** changes: rebuild (or let the watcher do it), then refresh the page / OBS browser source. No server restart. - **Extension** (`src/extension/`), **types**, or **package.json** changes: rebuild **and restart NodeCG** — plain `node index.js` never rebuilds anything. - Graphics are fixed 1920×1080 canvases — judge them in OBS or a fullscreen 1080p tab; smaller windows show a crop. ## Architecture notes - The scheduler is a state machine over `(mode: normal|break, phase: live|ad)` with exactly one pending timer; OBS calls are fire-and-forget, so an unreachable OBS never stalls the choreography. - Corner alternation and ad round-robin persist across restarts (`schedulerState` replicant). On startup the bundle always returns to (normal, live) + the main scene. - Replicant defaults are only applied when nothing is persisted, so operator edits survive restarts. ## License [AGPL-3.0](LICENSE)