Files
esports/streamer/README.md
Stan Runge 54444a77f0 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>
2026-10-18 22:02:25 +00:00

30 KiB
Raw Permalink Blame History

Streaming Vash matches

For a broadcast from a Windows PC with real osu!tourney gameplay, use vash-broadcast (section 3): 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 A Windows PC running vash-broadcast, or a Docker host with Wine
 Vash match runtime ─▶ director ─▶ channel websocket ─▶ tourney agent
         ▲                                           │ room selection
         │ live scores                               ▼
         └────────── tosu ◀────────────── osu!tourney under Wine
                                                   │ gameplay screen
 Vash overlay page ──green key──▶ ffmpeg compositor ◀┘
                                  │ one encoded stream
                                  ├──▶ Twitch / YouTube / other RTMP
                                  └──▶ Vash live video and recording

1. Create a channel

Referees and admins open Broadcast in the sidebar and create a channel. A channel shows one of these:

  • Whatever is most worth watching. The director picks among all live matches: tournament matches first, then close series near match point, tiebreakers, and busy matches. It never cuts away during a map and stays on a result for 30 seconds.
  • A tournament's matches, by the same rules.
  • One match.

Pick a starting layout; each shows a live preview:

Layout Use it when
Over osu! gameplay Capture osu!tourney in the autonomous container or OBS. The gameplay scene leaves a see-through window for it.
Vash only The automatic stream (Vash TV), or any stream without an osu! capture. Everything comes from Vash's data.
Scoreboard only You have your own OBS scenes and only want the score on top.
Phone (portrait) TikTok, Instagram or YouTube Shorts. Makes a 9:16 channel.

The agent token shown after creating the channel is for the stream container and vash-streamer. It is shown once; Agent token makes a new one.

Layout editor

Edit layout opens the editor. Each scene can be arranged on its own: Starting soon, Lobby, Pick & ban, Gameplay and Results. The overlay switches scenes as the match moves on.

  • Drag a component to move it and drag its corner to resize. Movement snaps to a 10 px grid; hold Alt to move freely.
  • Arrow keys nudge the selected component (Shift for 10 px). Delete removes it and Ctrl+D duplicates it.
  • Components: scoreboard, live score, now playing, mappool, turn and timer, map history, lobby chat, crowd predictions, players, winner, gameplay window, live ticker of other matches, schedule, text, image, and clock.
  • Backgrounds: each scene can be see-through, the theme background, or an image.
  • Demo match: when nothing is live, the editor shows a made-up match so every component has something to draw.

Saving updates every open overlay immediately, including the one in OBS.

https://<site>/broadcast/<channel>/overlay is a 1920×1080 page with a transparent background. It needs no login and shows only public match data.

  • ?bg=solid paints the see-through scenes too, for streams with nothing underneath.
  • ?scene=gameplay pins one scene, if you prefer one browser source per OBS scene.

2. Overlay 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 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 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

.\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

.\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

.\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.

.\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:

  1. osu! tourney client. Put a tournament.cfg next to osu!.exe (see the osu! wiki) and start it with a spare account.
  2. tosu (tosuapp/tosu). It reads the tourney client's memory. Its defaults work.
  3. OBS 28 or newer with Tools → WebSocket Server Settings enabled.
  4. vash-streamer.exe, built with bun run streamer:build (output in dist/). It is one file with no install.
# Once: create a "Vash" scene with the tourney client capture and the overlay on top.
.\dist\vash-streamer.exe --server https://esports.vash.software --channel <id> --token <token> --obs-password <pw> --setup-obs

# Every stream:
.\dist\vash-streamer.exe --server https://esports.vash.software --channel <id> --token <token> --obs-password <pw> --obs-scenes '{"idle":"Starting Soon","gameplay":"Vash"}'

While it runs:

  • Live scores. Live scores from tosu reach Vash four times a second, matched to players by osu! id. They drive the overlay's live score bar and the match page on the site.
  • OBS scenes. With --obs-scenes, OBS switches scenes as the match does. The scene names are idle, lobby, pickban, gameplay and results.
  • On Windows, select the named room in osu!tourney's bottom control panel. The container setup below performs this selection automatically under Wine/X11.

The lazer tournament client can run alongside for its own screens, since it reads the stable client's IPC. Vash does not drive it; the Vash overlay covers the same ground with live data.

4. Go live on Twitch, YouTube and more

Open a channel's Stream page:

  1. Add destinations. Twitch, YouTube, Facebook, X, Instagram, TikTok, or any RTMP server; several of one kind is fine (two YouTube channels, say). Each platform shows where to find its stream key. Keys are stored encrypted and only sent to this channel's stream container.
  2. Pick the shape and quality. Landscape (16:9) for Twitch, YouTube, Facebook and X; portrait (9:16) for TikTok and Instagram. To stream both at once, make a second, portrait channel: both follow the same matches.
  3. Start the stream container on your PC or Docker host, then press Go live. Go live changes the channel's state; it does not launch Docker. A connected container goes on air, reports each destination's state on the page, and reconnects any destination that drops. End stream tells it to stop streaming.

Recordings. Every broadcast is also cut into 2-second HLS segments and uploaded to storage (R2). That is the live stream on the site (the Live page and the channel's Watch page), and afterwards the recording, with chapters that jump to each match. Match pages link to the broadcast that showed them. Delete old recordings from the Stream page.

For playback in Chrome and Firefox, the bucket needs a CORS rule (Cloudflare dashboard → R2 → bucket → Settings → CORS policy):

[
	{
		"AllowedOrigins": [
			"https://esports.vash.software",
			"https://esports-dev.vash.software",
			"https://esports-staging.vash.software"
		],
		"AllowedMethods": ["GET", "HEAD"],
		"AllowedHeaders": ["*"],
		"MaxAgeSeconds": 3600
	}
]

VODs on the platforms. YouTube keeps every stream as a video by itself. On Twitch, turn on Store past broadcasts (Settings → Stream); Twitch keeps them 7 days, or 60 for Partners, Turbo and Prime, so the copy on Vash's storage is the one that lasts. Facebook and X keep the post; Instagram asks to share it when the stream ends.

5. The stream container (Vash TV)

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:

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> \
  -e VASH_TOKEN=<agent token> \
  vash-tv

For gameplay from a Windows PC, the simplest setup is osu!tourney, tosu, OBS, and the Vash streamer agent from sections 2–3. Stream directly from OBS to your platform. In that setup, the site's Go live switch and on-air status do not control OBS. If you also want the site's watch page, recording, and destinations, run Vash TV in relay mode on the same PC and send OBS to its local RTMP input:

docker run -d --restart unless-stopped --name vash-relay -p 127.0.0.1:1935:1935 \
  -e VASH_SERVER=https://esports.vash.software -e VASH_CHANNEL=<channel id> \
  -e VASH_TOKEN=<agent token> -e INGEST_KEY=<private ingest key> vash-tv

In OBS, set a Custom stream server to rtmp://127.0.0.1:1935/live and use the same private ingest key. Start the container, press Go live on the Stream page, then start streaming in OBS. Stop OBS and press End stream when finished.

Variable
VASH_SERVER, VASH_CHANNEL, VASH_TOKEN The site, the channel, and its agent token. Everything else comes from the channel's settings.
INGEST_KEY Relay mode: instead of rendering, listen for OBS on rtmp://<host>:1935/live with this key (INGEST_PORT to change the port). What OBS sends is recorded and sent to every destination unchanged. Use it for the full tourney broadcast so it is recorded and multistreamed too: in OBS, Settings → Stream → Custom, server rtmp://<host>:1935/live, key INGEST_KEY.
OVERLAY_ORIGIN Load the overlay from another address, e.g. http://127.0.0.1:3102 when the container runs next to a dev site behind a login.
MUSIC_URL An audio file or stream to loop under the broadcast (silence otherwise).
OVERLAY_URL + RTMP_URL Without Vash managing it: one overlay to one RTMP destination (WIDTH, HEIGHT, FPS, VIDEO_BITRATE apply).

Measured on the Vash VM, 720p30 uses about half a CPU core and 510 MB of memory; 1080p and busy scenes use more. Give it 2 vCPUs and 2 GB, and keep it off the Vash server. Upload bandwidth is the bitrate once per destination, plus once for the recording.

Google Cloud (e2-medium, about $25 a month). Container-Optimized OS comes with Docker:

gcloud compute instances create vash-tv --machine-type=e2-medium --zone=europe-west4-a \
  --image-family=cos-stable --image-project=cos-cloud \
  --metadata=startup-script='docker run -d --restart unless-stopped --shm-size=1g --name vash-tv \
    -e VASH_SERVER=https://esports.vash.software -e VASH_CHANNEL=<id> -e VASH_TOKEN=<token> <registry>/vash-tv'

AWS: an EC2 t3.small with Docker and the docker run above (about $15 a month), or an ECS Fargate task with 2 vCPU and 4 GB (about $70 a month running full time). For relay mode, open TCP 1935 to your streaming PC only.

Stop the instance when nothing is scheduled; the channel's recordings stay in storage.

6. Autonomous osu!tourney container

The streamer/tourney image adds a separate virtual screen for the stable osu!tourney client, starts it under Wine, reads its scores with tosu, and keys the Vash overlay over the gameplay in ffmpeg. The same encoder sends to all enabled destinations and records to the site. The director selects the room automatically from osu!tourney's control panel. It retries while the room is still opening.

osu!tourney needs a dedicated osu! stable installation and an osu! account with an active supporter tag. Use an account you are allowed to operate, and sign in once with Remember Username and Remember Password. Set OSU_TOURNEY_ACCOUNT to that username in the Vash server environment so each new lobby grants it referee access. The container does not store an osu! password in Vash. All new Vash lobbies use the VASH: (Red) vs (Blue) naming required by this single osu!tourney installation. The agent restarts osu!tourney when a match needs a different TeamSize; it never edits tournament.cfg while the client is running.

Build the image from the repository root:

docker build -f streamer/tourney/Dockerfile -t vash-tourney streamer

Prepare a persistent directory owned by uid 1000. Place a fresh, separate osu! stable installation at /path/to/vash-tourney-data/osu/osu!.exe and the executable Linux build of tosu at /path/to/vash-tourney-data/tosu/tosu. The tosu project documents Wine support; this setup still needs a real lobby test on your host. A GPU host is recommended until performance is measured for your team size and resolution.

First sign in to osu! in normal mode. For a fresh Wine prefix, OSU_DOTNET=1 installs .NET Framework 4.8 with winetricks before launching osu!; the installer may take several minutes and download Microsoft files. If the mounted prefix already has a working .NET installation, omit that variable. Use a strong temporary VNC password and keep the web port bound to localhost, then reach it through SSH forwarding if the Docker host is remote:

docker run --rm --shm-size=1g --cap-add SYS_PTRACE \
  -v /path/to/vash-tourney-data:/data -p 127.0.0.1:6080:6080 \
  -e OSU_SETUP=1 -e OSU_DOTNET=1 -e VNC_PASSWORD='<private password>' vash-tourney
# Open http://127.0.0.1:6080/vnc.html, sign in to osu!, then stop the container.

Create a broadcast channel with the Over osu! gameplay layout, add Twitch, YouTube, or other destinations, and copy its one-time agent token. Then run:

docker run -d --restart unless-stopped --shm-size=1g --cap-add SYS_PTRACE \
  --name vash-tourney -v /path/to/vash-tourney-data:/data \
  -p 127.0.0.1:6080:6080 \
  -e VNC_PASSWORD='<private password>' \
  -e VASH_SERVER=https://esports.vash.software \
  -e VASH_CHANNEL='<channel id>' -e VASH_TOKEN='<agent token>' \
  vash-tourney

The Go live switch controls ffmpeg and destinations; the tourney client stays ready to follow matches. Watch docker logs -f vash-tourney for room selection and tosu connection. The VNC screen shows the osu!tourney control panel if it needs attention. Live scores appear when tosu reads the spectated clients. The container captures the top 1280×720 area of osu!tourney and the bottom control panel only for room selection. Set GAMEPLAY_WIDTH and GAMEPLAY_HEIGHT to change that capture size, and use the matching channel quality. The compositor also supports portrait outputs, with the 16:9 gameplay letterboxed under the portrait overlay.

This uses the osu!tourney room naming and slot rules described in the official multiplayer guide and the official setup guide. The IPC file is only needed for the separate osu!(lazer) tournament overlay; this container draws the Vash overlay itself.