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
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Bootstrap a complete NodeCG + nodecg-venue-gfx install on a venue machine.
#
# Creates a standalone NodeCG installation (the nodecg npm package extracted
# as the install root — the layout NodeCG itself calls "standalone"; running
# it as a node_modules dependency is still flagged experimental upstream),
# clones this bundle into bundles/, builds it, writes the config files, and
# generates an importable OBS scene collection.
#
# Usage:
# ./install.sh [--dir ~/venue-gfx] [--port 9090] [--obs-password SECRET]
# [--repo <git-url>] [--local <path-to-checkout>] [--yes]
#
# --local uses an existing checkout (copied, not cloned) instead of --repo;
# when run from inside a checkout, --local defaults to that checkout.
# --yes accepts all defaults without prompting.
set -euo pipefail
BUNDLE=nodecg-venue-gfx
DEFAULT_REPO="https://git.dvdrw.dev/kckljajicevo/nodecg-venue-gfx.git"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
dir=""
port=""
obs_password=""
repo=""
local_src=""
assume_yes=0
while [[ $# -gt 0 ]]; do
case "$1" in
--dir) dir="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
--obs-password) obs_password="$2"; shift 2 ;;
--repo) repo="$2"; shift 2 ;;
--local) local_src="$2"; shift 2 ;;
--yes|-y) assume_yes=1; shift ;;
--help|-h) sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 1 ;;
esac
done
# ── prerequisites ─────────────────────────────────────────────────────────
command -v node >/dev/null || { echo "node not found — install Node.js 20+" >&2; exit 1; }
command -v npm >/dev/null || { echo "npm not found" >&2; exit 1; }
node_major=$(node -p 'process.versions.node.split(".")[0]')
if (( node_major < 20 )); then
echo "Node.js $(node -v) is too old — need 20+" >&2
exit 1
fi
# When run from inside a checkout of the bundle, default to copying it.
if [[ -z $local_src && -z $repo && -f "$SCRIPT_DIR/../package.json" ]]; then
if node -p 'require(process.argv[1]).name' "$SCRIPT_DIR/../package.json" 2>/dev/null | grep -qx "$BUNDLE"; then
local_src="$(cd "$SCRIPT_DIR/.." && pwd)"
fi
fi
ask() { # ask <prompt> <default-var-name> <default-value>
local prompt="$1" var="$2" def="$3" answer
if [[ -n ${!var} ]]; then return; fi
if (( assume_yes )); then printf -v "$var" '%s' "$def"; return; fi
read -r -p "$prompt [$def]: " answer
printf -v "$var" '%s' "${answer:-$def}"
}
ask "Install directory" dir "$HOME/venue-gfx"
ask "NodeCG port" port "9090"
ask "OBS websocket password (empty = auth disabled)" obs_password ""
if [[ -z $local_src ]]; then
ask "Bundle git URL" repo "$DEFAULT_REPO"
fi
dir="${dir/#\~/$HOME}"
echo
echo "Installing into $dir (NodeCG on port $port)"
# ── NodeCG itself (standalone layout: install root = the nodecg package) ──
mkdir -p "$dir"
cd "$dir"
if [[ -f package.json ]] && node -p 'require("./package.json").name' | grep -qx nodecg; then
echo "NodeCG already present — skipping"
else
echo "Downloading NodeCG…"
tarball=$(npm pack nodecg@^2 --silent | tail -n1)
tar xzf "$tarball" --strip-components=1
rm -f "$tarball"
echo "Installing NodeCG dependencies…"
npm install --omit=dev --no-audit --no-fund
fi
mkdir -p bundles cfg obs
# ── the bundle ────────────────────────────────────────────────────────────
if [[ ! -d bundles/$BUNDLE ]]; then
if [[ -n $local_src ]]; then
echo "Copying bundle from $local_src"
mkdir -p "bundles/$BUNDLE"
(cd "$local_src" && git archive HEAD 2>/dev/null || tar cf - --exclude node_modules --exclude .git .) \
| tar xf - -C "bundles/$BUNDLE"
else
echo "Cloning $repo"
git clone "$repo" "bundles/$BUNDLE"
fi
fi
echo "Building the bundle…"
(cd "bundles/$BUNDLE" && npm ci --no-audit --no-fund && npm run build)
# ── config ────────────────────────────────────────────────────────────────
if [[ ! -f cfg/nodecg.json ]]; then
printf '{\n\t"port": %s\n}\n' "$port" > cfg/nodecg.json
echo "Wrote cfg/nodecg.json"
fi
if [[ ! -f cfg/$BUNDLE.json ]]; then
node - "$obs_password" > "cfg/$BUNDLE.json" <<'EOF'
const password = process.argv[2] ?? "";
process.stdout.write(JSON.stringify({
locale: "en",
obs: { enabled: true, url: "ws://127.0.0.1:4455", password },
}, null, "\t") + "\n");
EOF
echo "Wrote cfg/$BUNDLE.json"
fi
# ── OBS scene collection ──────────────────────────────────────────────────
node "bundles/$BUNDLE/scripts/generate-obs-scenes.mjs" \
--config "cfg/$BUNDLE.json" \
--nodecg-url "http://localhost:$port" \
--name VenueGfx \
--out obs/VenueGfx.json
# ── done ──────────────────────────────────────────────────────────────────
cat <<EOF
──────────────────────────────────────────────────────────────────────
Install complete. Start NodeCG with:
cd $dir && node index.js
Dashboard: http://localhost:$port
OBS setup (see bundles/$BUNDLE/docs/OBS-SETUP.md for details):
1. Install exeldro's Move plugin: https://obsproject.com/forum/resources/move.913/
2. Tools → WebSocket Server Settings: enable, port 4455$( [[ -n $obs_password ]] && echo ", set the password you chose" || echo ", disable authentication" )
3. Scene Collection → Import → $dir/obs/VenueGfx.json, then switch to it
4. Replace the "Program Placeholder" source with your real capture
5. Point the Stinger transition at a real video file
6. Settings → Video: 1920×1080 base + output, 60 FPS
7. Dashboard → "OBS setup check" panel → Run checks, fix anything red
To run NodeCG as a service that survives reboots:
bundles/$BUNDLE/scripts/install-service.sh $dir
──────────────────────────────────────────────────────────────────────
EOF