feat: add native Windows broadcaster and live score modules

- streamer/windows: vash-broadcast.exe (setup/login/start/stop/status/doctor)
  with per-instance OBS, tosu and FFmpeg, a supervised worker, OCR room
  selection and OBS scene reconciliation; built and tested on Linux only
- streamer/relay.ts shared with Vash TV; Cloudflare Access proxy for dev/staging
- worker status validation and live score helpers for the site
- STREAMING_HANDOFF.md and streamer/ALPHA_TEST.md record what is unverified

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 22:02:25 +00:00
parent 9d5a046b40
commit 54444a77f0
45 changed files with 8168 additions and 643 deletions

View File

@@ -1,14 +1,14 @@
# Streaming Vash matches
For a first test on Windows with Ubuntu WSL, follow [the WSL setup guide](WSL.md).
For a broadcast from a Windows PC with real osu!tourney gameplay, use `vash-broadcast` ([section 3](#3-full-tourney-broadcast-on-a-windows-pc)): one program that sets everything up once and then runs unattended.
Vash can put its matches on stream by itself. There are three ways to use it, from no PC at all to a full tournament broadcast.
| Setup | What viewers see | Needs |
| -------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **Vash TV** (headless container) | Vash's own data: scoreboard, mappool with picks and bans, live scores, predictions, chat, ticker | A small cloud VM or any Docker host |
| **Overlay in OBS** | Your OBS scenes with the Vash overlay on top | OBS |
| **Full tourney broadcast** | Real osu!tourney gameplay under the overlay, live scores, and the same multistream and recording | Dedicated Docker host with Wine, osu!, and tosu, or a Windows PC with OBS |
| Setup | What viewers see | Needs |
| -------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| **Vash TV** (headless container) | Vash's own data: scoreboard, mappool with picks and bans, live scores, predictions, chat, ticker | A small cloud VM or any Docker host |
| **Overlay in OBS** | Your OBS scenes with the Vash overlay on top | OBS |
| **Full tourney broadcast** | Real osu!tourney gameplay under the overlay, live scores, and the same multistream and recording | A Windows PC running `vash-broadcast`, or a Docker host with Wine |
```
Vash match runtime ─▶ director ─▶ channel websocket ─▶ tourney agent
@@ -64,7 +64,124 @@ Saving updates every open overlay immediately, including the one in OBS.
Add a **Browser** source with the overlay link, 1920×1080, on top of your scene. That is all. To put gameplay under it, capture anything below the browser source; the gameplay scene of the _Tourney client_ preset leaves that area see-through.
## 3. Full tourney broadcast (Windows)
## 3. Full tourney broadcast on a Windows PC
`vash-broadcast.exe` sets up and runs the whole broadcast on a Windows PC. Setup is once per instance; after that there is nothing to do per stream. **Go live** and **End stream** on the channel's Stream page start and stop it, and each match's room is selected without anyone touching the PC. Everything is done from an SSH session.
```
Vash ──follow (match, lobby, team size)──▶ worker ──▶ osu!tourney: tournament.cfg, restart, click the room
▲ │ │ window
│ live scores, worker status │ ▼
└──────────────── tosu ◀── reads ──────────┘ OBS (portable, this instance only)
overlay on top of the captured window
Go live ──▶ worker ──▶ local relay (ffmpeg) ◀── RTMP ── AMD AMF hardware encoder
├──▶ Twitch / YouTube / other RTMP
└──▶ Vash live video and recording (HLS to storage)
```
### Before you start
- **Windows 10 or 11, signed in at the PC.** OBS, osu! and window capture need the signed-in desktop, so the worker runs there (through a scheduled task) and not in the SSH session. It keeps running when SSH disconnects. It cannot run before someone signs in: for unattended reboots, turn on automatic sign-in (for example with Sysinternals Autologon) and turn off **Require sign-in** after sleep. Room selection clicks in osu!tourney, so the desktop must be **unlocked**; the worker keeps the PC awake and the display on while it runs, and says `desktop-locked` if Windows locks anyway.
- **osu! stable installed.** Setup copies its `osu!.exe` into a separate folder, as the [osu!tourney setup guide](https://osu.ppy.sh/wiki/en/osu!_tournament_client/osu!tourney/Setup) says; your own installation is only read.
- **A spectator osu! account with osu!supporter** (osu!tourney requires it), separate from your playing account. Set `OSU_TOURNEY_ACCOUNT` to its username in each Vash server's environment so new lobbies add it as a referee.
- **The program.** Build it with `bun run broadcast:build` (it cross-compiles from Linux) and copy `dist/vash-broadcast.exe` to the PC, e.g. `scp dist/vash-broadcast.exe stan@stan-win11:`.
- **The channel's agent token** from **Agent token** on the Broadcast page. The page shows the exact setup command.
- **Dev and staging sites behind Cloudflare Access** need a service token: Zero Trust → Access → Service Auth → create one, and add a _Service Auth_ policy for it to the site's Access application. Production needs none.
### Set up an instance
```powershell
.\vash-broadcast.exe setup --instance prod --server https://esports.vash.software --channel <channel id> --token <agent token>
.\vash-broadcast.exe setup --instance dev --server https://esports-dev.vash.software --channel <id> --token <token> --access-id <id> --access-secret <secret>
```
Setup checks the site and token first, then installs into `%LOCALAPPDATA%\Vash\Broadcast\instances\<instance>`:
| What | Version | Notes |
| ---------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| OBS Studio, portable | 32.2.2 | Its own settings, scene, websocket port and password. Your own OBS is untouched. Profile: the channel's size, frame rate and bitrate, AMD AMF hardware encoding, streaming to the local relay. |
| tosu | 4.26.2 | Own port, no auto-update, no browser on start. |
| FFmpeg (gyan.dev essentials) | 9.0.2 | The local relay: restreams and records what OBS sends, unchanged. |
| osu!tourney folder | copied `osu!.exe` | Installs itself on first run during the sign-in. |
Every download is pinned and its SHA-256 checked before use (`streamer/windows/install.ts`); nothing is downloaded when the worker starts. Setup also copies itself to `%LOCALAPPDATA%\Vash\Broadcast\bin`, and registers the scheduled task _Vash Broadcast (prod)_ that runs the worker in your desktop when you sign in (`--no-autostart` to only start it by hand). Running setup again keeps the settings, login and scene, and only changes what differs; stop the instance first. Other options: `--osu-exe`, `--tourney-height` (default 720; the window is about 200 px taller), `--encoder x264` to choose software encoding on purpose, `--token-stdin`.
### Sign in the spectator account, once
```powershell
.\vash-broadcast.exe login --instance prod
```
This opens osu! normally on the PC's screen and prints an SSH tunnel command and a link. Open the tunnel from your Mac and the link in a browser: it shows the PC's screen and passes your clicks and typing through. A fresh installation first downloads osu!'s files. Sign in, tick **Remember username** and **Remember password**, and press **Finish**. osu! closes, and the worker checks the login was saved. The viewer listens on 127.0.0.1 only, needs the printed token, and exists only during the sign-in. Other instances can reuse it: `login --instance dev --copy-from prod`.
### Start it, then use the site
```powershell
.\vash-broadcast.exe start --instance prod
```
The worker starts OBS, tosu and osu!tourney, and follows the channel:
- **Matches.** When the director picks a match, osu!tourney restarts if the team size needs it, and the worker selects the match's room (see below). The captured gameplay is shown under the overlay only once tosu confirms the right room; otherwise the overlay's own scenes cover it.
- **Go live** makes the relay listen, then OBS streams into it; the relay sends to every enabled destination and records for the site. **End stream** stops OBS, finishes the recording and ends the session.
- **Failures.** OBS, tosu, osu!tourney and the relay restart on their own with growing delays; so does the worker itself. Channel video changes restart OBS while off air.
- **Status.** The worker reports its state to the site (Broadcast list and Stream page) and to `status`:
| State | Meaning |
| -------------------------- | ----------------------------------------------------------------------------- |
| `starting`, `reconnecting` | Starting or restarting a program; the detail says which. |
| `idle` | Ready; the channel shows no match with an osu! lobby. |
| `waiting-for-lobby` | The room is not in osu!tourney's list yet. |
| `selecting` | Clicked the room; waiting for tosu to confirm it. |
| `spectating` | tosu confirms the room. (If tosu is down, the detail says it is unconfirmed.) |
| `login-required` | Run `login` once. |
| `desktop-locked` | Windows is locked; the room cannot be clicked. |
| `blocked` | Another instance is running osu!tourney on this PC. |
| `error` | Needs attention; the detail says why. |
### How the room is selected
osu!tourney has no command or IPC for choosing a room. It lists rooms named `VASH: (Red) vs (Blue)` in its control panel as `<match id> : <room name>`. The worker brings the osu!tourney window forward, reads the list with Windows' built-in OCR (no extra install), clicks the line with the lobby's match id, and then checks tosu: osu!tourney's current team names must be the requested room's. Without a readable match id it clicks a room by name only when exactly one room has both team names; it never clicks "the first room". It retries while the room is not listed, and selects again if tosu shows another room. The last capture of the panel and what OCR read are in the instance's `snapshots` folder.
### Several instances (dev, staging, prod)
Each instance has its own folder, OBS, osu!tourney folder and login, tosu, relay, ports, logs and scheduled task, and stopping one only ever stops programs inside its own folder. One limit is real: tosu reads every osu! process on the PC and uses the first osu!tourney it finds, so **only one instance runs osu!tourney at a time**. Another started instance waits in `blocked` until the first stops. Switch with `stop --instance dev` and `start --instance prod`.
### Checking on it
Live score collection works while the worker is **off air**, too: it only needs tosu to read the
match in osu!tourney and the channel to follow that match. Go live is not required. The match page
shows each player's score, accuracy and combo during play, and marks scores as delayed when no
fresh update arrives for five seconds. A simulated opponent has no real gameplay client for tosu
to read; its scores come from Vash's simulator instead.
If the match page is waiting for scores, `status --instance prod` shows `tosu`'s player count and
last frame time, followed by `scores`' last send time, accepted player count and last acceptance
time. These distinguish a missing tosu feed from a feed Vash cannot associate with the current
map or roster. Only the channel's followed match receives that worker's scores.
After replacing the executable with a newer build, stop the instance, run `setup --instance prod`
again to update its installed worker copy using the saved settings, then start it. Use your own
instance name in place of `prod`.
```powershell
.\vash-broadcast.exe status --instance prod # state, match, room, OBS, relay, destinations, encoder
.\vash-broadcast.exe logs --instance prod -f
.\vash-broadcast.exe snapshot --instance prod # PNGs of OBS's output and the osu!tourney window
.\vash-broadcast.exe doctor --instance prod # setup, task, login, GPU; desktop, OCR and capture checks
.\vash-broadcast.exe stop --instance prod # goes off air cleanly first
.\vash-broadcast.exe list
```
`status` shows which encoder OBS actually runs, read from OBS's log after the stream starts. When the AMD encoder is unavailable, OBS switches to x264 by itself; the worker then warns rather than claiming hardware encoding.
### What has been tested
The logic is covered by `bun run test:streamer`. This includes room matching, OCR line handling and tosu confirmation against a fake desktop, launch and restart rules, OBS scene reconciliation and websocket timeouts against a fake OBS, instance isolation, the tourney lock, supervision backoff, pinned downloads, the relay lifecycle, and the worker's start, report, Go live and stop against a fake Vash. **None of it has run on Windows yet.** Still to confirm on the PC: the PowerShell desktop helper (window discovery, focus, clicks, capture, Windows OCR of the room list), osu!tourney's window layout and title handling, OBS window capture with its audio, AMD AMF output, the scheduled task start from SSH, the sign-in viewer, and a multi-hour soak with real matches. Run `doctor`, then a test match, and compare `snapshot` output.
### Manual OBS setup
Before `vash-broadcast`, the Windows setup was assembled by hand. It still works when you want to run OBS yourself; nothing below is automatic.
Live scores for real lobbies only come from the osu! tourney client. Bancho's IRC only reports results once a map ends. So the streaming PC runs:
@@ -123,7 +240,7 @@ For playback in Chrome and Firefox, the bucket needs a CORS rule (Cloudflare das
The `streamer/tv` image renders a channel in Chromium on a virtual screen, encodes it once, and copies the stream to every destination and to the recording. It follows the channel's **Go live** switch, so the container can idle between matches. To use your own PC for now, build and run it there:
```sh
docker build -t vash-tv streamer/tv
docker build -f streamer/tv/Dockerfile -t vash-tv streamer
docker run -d --restart unless-stopped --shm-size=1g --name vash-tv \
-e VASH_SERVER=https://esports.vash.software \
-e VASH_CHANNEL=<channel id> \