# Streaming Vash matches 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 | 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. ### The overlay link `https:///broadcast//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](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 --token .\vash-broadcast.exe setup --instance dev --server https://esports-dev.vash.software --channel --token --access-id --access-secret ``` Setup checks the site and token first, then installs into `%LOCALAPPDATA%\Vash\Broadcast\instances\`: | 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 ` : `. 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: 1. **osu! tourney client.** Put a `tournament.cfg` next to `osu!.exe` (see the [osu! wiki](https://osu.ppy.sh/wiki/en/osu!tourney)) and start it with a spare account. 2. **tosu** ([tosuapp/tosu](https://github.com/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. ```powershell # 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 --token --obs-password --setup-obs # Every stream: .\dist\vash-streamer.exe --server https://esports.vash.software --channel --token --obs-password --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): ```json [ { "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: ```sh 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= \ -e VASH_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: ```sh 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= \ -e VASH_TOKEN= -e 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://: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://: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: ```sh 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= -e VASH_TOKEN= /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: ```sh 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](https://github.com/tosuapp/tosu/releases) 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: ```sh 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='' 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: ```sh 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='' \ -e VASH_SERVER=https://esports.vash.software \ -e VASH_CHANNEL='' -e VASH_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](https://osu.ppy.sh/wiki/en/osu!_tournament_client/osu!tourney/Multiplayer_usage) and the [official setup guide](https://osu.ppy.sh/wiki/en/osu!_tournament_client/osu!tourney/Setup). The IPC file is only needed for the separate osu!(lazer) tournament overlay; this container draws the Vash overlay itself.