# BlasterVision Admin Guide

This is the step-by-step playbook for getting a game from "just played" to fully
working in BlasterVision: video(s), photos, GameChanger play-by-play, and a
calibrated score bug / play list. It also covers live streaming and relaying a
live feed to GameChanger.

All admin actions require the admin code, entered once in the app (stored in
`localStorage` as `vs_admin_token`). Admin-only controls (⚙ Streams, + Add,
Set Angles, Set first/last pitch, Rec, Relay) only appear after that code is
accepted.

---

## 1. Add the game to BlasterVision

1. Open **📼 Past Games**.
2. In the "Add game" row at the top, fill in:
   - **Date** — the game date.
   - **Title / opponent** — e.g. `vs. Cardinals` (this becomes part of the
     game's slug, so try to match how GameChanger names the opponent — see
     "fixOpponent" note below if it ever needs correcting).
   - **Video URL** — leave blank for now if the video isn't uploaded yet. You
     can also leave this blank entirely if you're going to use "Set Angles"
     (step 2) to point at an R2 folder instead.
   - **Photo folder URL** (optional) — only needed if photos are hosted
     somewhere other than the default `photos/<slug>/` R2 path used by
     `upload.html`.
3. Click **+ Add**. The game appears in the Past Games list immediately and is
   saved to the Worker (`/state`), so all viewers see it.

> Tip: the game's internal "slug" is `date + title`, lowercased and
> punctuation-stripped (e.g. `2026-06-13-vs-cardinals`). This slug is what
> photo uploads (`upload.html`) and the GC play-by-play matcher use to find
> the right game, so keep the title consistent once it's set.

---

## 2. Upload the video(s) — single camera or multiple angles

Recordings made with **⏺ Rec** (see §6) are saved automatically to R2 and the
game's `url`/`angles` are set for you — for those games you can skip to step 3.

For videos uploaded another way (e.g. copied to R2 manually, or recorded by a
videographer and dropped into an R2 folder):

1. In **📼 Past Games**, find the game row and click **Set Angles** (the
   camera icon).
2. Enter the **R2 folder name** containing the video(s), e.g. `game_20260613`.
   Two layouts are supported:
   - **Single camera**: an `.m3u8` (+ its `.ts` segments) directly inside the
     folder → BlasterVision sets it as the one video for the game.
   - **Multiple angles**: subfolders per camera (one `.m3u8` each), optionally
     with a `sync.json` describing each camera's label and `offset_seconds`
     (used to align angles to each other and to the score-bug timeline).
3. BlasterVision reads the folder via the Worker's `/r2list` endpoint, finds
   the playlists, and saves either `entry.url` (single) or `entry.angles` +
   `entry.anglesFolder` (multi) to the game.
4. A toast confirms what was found (e.g. "✓ 2 angles set (Field = main)").
   Leave the folder field **blank** and confirm to clear/unset video for a
   game.

---

## 3. Pull in the GameChanger play-by-play

This step generates the `/plays` data (timeline, scores, named batters) used
for the **📋 Plays** panel and the score-bug.

From `C:\Projects\VideoStream`:

```powershell
# 1. Pull fresh events from GameChanger (opens a real browser — log in once,
#    GC will remember the device after that). Run from anywhere; it reads/writes
#    C:\Projects\GC\data.
node pull-gc-events.js pull

# 2. Normalize + upload all games' play-by-play to BlasterVision (R2 plays/<key>.json
#    and the plays/index.json summary, including computed + official scores).
node pull-gc-events.js upload <ADMIN_TOKEN>

# Or do both in one step:
node pull-gc-events.js all <ADMIN_TOKEN>
```

- `pull` opens a Playwright browser window. The first time, log into
  GameChanger when prompted — credentials are never seen/stored by the script,
  only the browser session/profile is kept (`C:\Projects\GC\.gc-browser-profile`)
  so future runs usually skip the login.
- `pull` walks every game in the latest `C:\Projects\GC\data\scrape_Blasters_*.json`
  scrape file and saves each game's raw events to
  `C:\Projects\GC\data\events\<gameId>.json`.
- `upload` reads those saved event files, normalizes them (builds the
  play-by-play timeline, final score, and the "official" GC score from the
  scrape data), and POSTs each game to the Worker. This is what makes a game
  show up in **📋 Plays** and gets it a W/L/T score badge in Past Games.
- If a game's reconstructed score doesn't match GameChanger's official final
  score, its score badge in Past Games gets an **orange circle** with a
  tooltip showing the official score — that's a signal to double-check the
  play-by-play for that game (an "undo"/correction GC made that wasn't
  captured, a missed run, etc.).

Run `pull` + `upload` after every game (or in a batch for several games)
once GameChanger has finalized the box score.

---

## 4. Calibrate first pitch (sync the score bug / play links to the video)

This links GameChanger's play timestamps to a position in the video so the
score bug shows the right inning/score/count as the video plays, and so
**📋 Plays → ▶** jumps to the right moment.

**Multi-angle games with `sync.json`** (recordings made via §6, or any
folder where `sync.json` has a `start_time_utc`) are calibrated automatically
— no manual step needed.

**Otherwise (manual one-time calibration), per game:**

1. Open **📼 Past Games → 📋 Plays** for the game. If uncalibrated, the Plays
   button shows a warning indicator and the panel shows:
   *"One-time setup: play this game's video, pause exactly at the FIRST
   PITCH, then tap **Set first pitch**."*
2. Click **Set first pitch**. This closes the Plays panel, loads the game
   video, and shows a floating helper bar.
3. Scrub/play the video to the exact moment of the **first pitch** of the
   game, pause, then click the helper's **Set first pitch** button.
   - If it can't find a timestamp yet, it'll tell you to pause exactly there
     and try again.
4. The Plays panel reopens with **"Calibration set — play links are live"**.

**If the recording started late** (first pitch isn't in the video):

- Use **Set last pitch** instead — pause the video at the **last recorded
  play** of the game and calibrate from that anchor (it's the same flow, just
  anchored to the end of the game instead of the start).

**Fine-tuning** — once calibrated, the Plays panel shows a small toolbar:
- **"+1s earlier" / "-1s later"** — nudge the whole timeline by 1 second per
  click if the score bug looks slightly off.
- It also displays "first pitch at *m:ss* in video" for reference.
- **Re-calibrate** — clears the calibration so you can redo step 1–3.

---

## 5. Anything else to check

- **Photos**: photographers use `upload.html` with the shared "photographer
  upload code" — they pick the game from a dropdown (built from `/state`'s
  playlist) and drop photos in. Nothing else needed on the admin side unless
  a custom photo folder URL is required (step 1's "Photo folder URL" field).
- **Score badge / mismatch check**: after running `pull-gc-events.js upload`,
  glance at Past Games for any **orange-circled** score badges — these mean
  the reconstructed score differs from GameChanger's official final score.
  Investigate via the `/plays` data for that game (often an "undo"/correction
  in GC that wasn't captured, or an incomplete event capture).
- **Live Views** vs **Past Games**: a "Live View" (added in 📹 Live Views) is
  a live stream URL shown to viewers while a game is in progress. Once the
  game ends and the recording is stopped (§6), it automatically becomes a
  Past Games entry — you generally don't need to add it twice.
- **Schedule**: the 📅 Schedule panel pulls from GameChanger's schedule scrape
  automatically — no manual entry needed there.
- **Deploys**: front-end changes need `npx netlify deploy --prod`
  (site `blasters-stream`, prod `https://watch.hisbaseball.us`). Worker
  changes (`worker.js`) need `npx wrangler deploy` from
  `C:\Projects\VideoStream`. Bump the version first with `npm run bump`.

---

## 6. Recording, streaming, and relaying live video to GameChanger

### A. Set up a Live View (once per camera/location)

1. Open **📹 Live Views**.
2. Under "Add view", enter a **name** (e.g. `Detyens Field View 1`) and the
   **HLS URL** (`https://…/index.m3u8`) of the camera's live stream.
3. Click **+ Add**. The view appears for all viewers while it's live.

This HLS URL is typically produced by a camera/encoder (e.g. a streaming box
or app) pushing to your streaming provider, which serves it back as
`.m3u8`/HLS.

**Going live from a phone instead of a dedicated camera**: open
`broadcast.html`, enter the admin code, allow camera/mic, then **Start
Camera → ● Go Live**. This publishes via WHIP/WebRTC directly into
BlasterVision's live pipeline (requires `WHIP_URL` to be configured on the
Worker). Use **🔄 Switch camera** to flip front/back without dropping the
stream, and **■ Stop** to end it.

### B. Record a live view to Past Games

1. In **📹 Live Views** (or the ⚙ Streams panel), each stream/view with an
   HLS source shows a **● Rec** button (admin only).
2. Click **● Rec**:
   - Enter a **title** for the recording (defaults to the view name + today's
     date) — this becomes the Past Games entry title.
   - Enter an **auto-stop time in minutes** (optional — leave blank for a 4
     hour cap).
3. The button turns into **● Recording** (red dot), and a row appears in the
   "active recordings" bar at the top of the panel showing the title and
   auto-stop time, with an **■ Stop** button.
4. When the game ends, click **■ Stop** (or let it auto-stop). The recording
   is saved to R2 and a confirmation toast shows **"✓ Saved to Past Games"**.
   The new entry appears in 📼 Past Games within ~20 seconds (the app polls
   state periodically).
5. Continue with **§3 (pull GC play-by-play)** and **§4 (calibration —
   usually automatic for these, via `sync.json`)**.

### C. Relay a live stream to GameChanger (RTMPS)

BlasterVision can re-stream (relay) any HLS source — an enabled stream, a
Live View, or a custom `.m3u8` URL — out to an RTMPS destination such as
GameChanger's broadcast ingest, without re-encoding video.

**One-time setup** — the relay runs on a small always-on host (it can't run
on Netlify/Workers because it needs `ffmpeg`). See `relay/README.md` for full
setup options:
- **Fly.io** (recommended, a few $/month): `flyctl launch`, set
  `RELAY_TOKEN`, `flyctl deploy`, ensure `min_machines_running = 1`.
- **Small VPS**: install `ffmpeg` + Node, run `relay-server.js` under
  systemd, put it behind HTTPS.
- Once reachable at an HTTPS URL, the relay URL + `RELAY_TOKEN` get wired into
  the Worker so the app's relay controls can call it.

**Per-game usage** (admin, in the ⚙ Streams panel's relay section):

1. **Source** — pick from the dropdown: any enabled stream or Live View with
   an `.m3u8` URL, or choose "Custom URL…" and paste one.
2. **Destination** — paste the **RTMPS URL** GameChanger gives you for the
   game's broadcast (`rtmps://…`).
3. Click **Start relay**. On success you'll see "✓ Relay started" and the
   relay appears in the active relays list with a **Stop** button.
4. When the game ends, click **Stop** on that relay row (or it can be left to
   run — but stop it to free up the relay host's capacity for the next game).

Behind the scenes this calls the Worker's `/relay/start` and `/relay/stop`,
which forward to the relay host; the relay pulls the HLS source and pushes it
to GameChanger via `ffmpeg -c:v copy` (video passthrough, audio transcoded to
AAC as RTMP requires).

---

## Quick reference — typical post-game checklist

1. ☐ Recording stopped → appears in 📼 Past Games (or add manually + Set
   Angles if uploaded separately).
2. ☐ `node pull-gc-events.js all <ADMIN_TOKEN>` once GC's box score is final.
3. ☐ Check Past Games for an orange-circled score badge → investigate if
   present.
4. ☐ Confirm 📋 Plays opens and the score bug timing looks right (calibrate
   manually if the game wasn't recorded via the multi-angle `sync.json` flow).
5. ☐ Photos uploaded via `upload.html` (photographer code).
