- 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>
83 lines
2.0 KiB
TypeScript
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 })
|
|
});
|
|
}
|
|
}
|