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

@@ -12,7 +12,7 @@ interface Word {
h: number;
}
const plain = (value: string) =>
export const plain = (value: string) =>
value
.normalize('NFKD')
.toLowerCase()
@@ -49,15 +49,19 @@ export function findRoom(tsv: string, roomName: string): { x: number; y: number
return null;
}
function tournamentConfig(existing: string, teamSize: number, height: number) {
/** tournament.cfg with our keys replaced and anything else osu! wrote kept. */
export function tournamentConfig(
existing: string,
options: { teamSize: number; height: number; acronym?: string }
) {
const lines = existing
.split(/\r?\n/)
.filter((line) => !/^\s*(TeamSize|acronym|Height)\s*=/i.test(line));
return [
...lines.filter(Boolean),
`TeamSize = ${teamSize}`,
'acronym = VASH',
`Height = ${height}`,
`TeamSize = ${options.teamSize}`,
`acronym = ${options.acronym ?? 'VASH'}`,
`Height = ${options.height}`,
''
].join('\n');
}
@@ -93,7 +97,7 @@ export class Tourney {
}
const configPath = join(this.directory, 'tournament.cfg');
const existing = await readFile(configPath, 'utf8').catch(() => '');
await writeFile(configPath, tournamentConfig(existing, teamSize, this.height));
await writeFile(configPath, tournamentConfig(existing, { teamSize, height: this.height }));
this.process = Bun.spawn(['wine', this.exe], {
cwd: this.directory,
env: { ...process.env, DISPLAY: this.display },