Files
esports/streamer/vash-api.ts
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

83 lines
2.0 KiB
TypeScript

/** The channel's stream API (/api/broadcast/<id>/…), authorized by its agent token. */
export interface Destination {
id: string;
platform: string;
label: string;
url: string;
}
export interface StreamConfig {
live: boolean;
name?: string;
format?: string;
width: number;
height: number;
fps: number;
bitrateKbps: number;
overlayUrl: string;
record: boolean;
destinations: Destination[];
}
export class VashApiError extends Error {
constructor(
message: string,
readonly status: number
) {
super(message);
}
}
export class VashApi {
constructor(
readonly server: string,
readonly channel: string,
private token: string,
/** Extra headers, e.g. Cloudflare Access service-token headers. */
private headers: Record<string, string> = {}
) {
this.server = server.replace(/\/$/, '');
}
async call<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${this.server}/api/broadcast/${this.channel}${path}`, {
...init,
redirect: 'manual',
headers: {
...this.headers,
authorization: `Bearer ${this.token}`,
'content-type': 'application/json',
...init.headers
},
signal: AbortSignal.timeout(15_000)
});
if (response.status >= 300 && response.status < 400)
throw new VashApiError(
`${path}: redirected to ${response.headers.get('location') ?? 'a login page'}; the site may need a Cloudflare Access service token`,
response.status
);
if (!response.ok)
throw new VashApiError(
`${path}: ${response.status} ${await response.text().catch(() => '')}`.slice(0, 300),
response.status
);
return response.json() as Promise<T>;
}
stream() {
return this.call<StreamConfig>('/stream');
}
report<T = unknown>(body: Record<string, unknown>) {
return this.call<T>('/stream', { method: 'POST', body: JSON.stringify(body) });
}
uploadUrls(sessionId: string, files: string[]) {
return this.call<{ urls: Record<string, string> }>('/uploads', {
method: 'POST',
body: JSON.stringify({ sessionId, files })
});
}
}