feat: redesign match play and add broadcast tooling
This commit is contained in:
87
streamer/obs.ts
Normal file
87
streamer/obs.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Just enough of obs-websocket 5 (built into OBS 28+) to switch scenes and
|
||||
* set up sources: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md
|
||||
*/
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
|
||||
const sha256 = (text: string) => createHash('sha256').update(text).digest('base64');
|
||||
|
||||
/** The challenge-response from the protocol's Authentication section. */
|
||||
export function obsAuthentication(password: string, salt: string, challenge: string) {
|
||||
return sha256(sha256(password + salt) + challenge);
|
||||
}
|
||||
|
||||
export class Obs {
|
||||
private socket: WebSocket | null = null;
|
||||
private pending = new Map<string, { resolve(data: unknown): void; reject(err: Error): void }>();
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private password: string
|
||||
) {}
|
||||
|
||||
get connected() {
|
||||
return this.socket?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = new WebSocket(this.url);
|
||||
this.socket = socket;
|
||||
socket.onerror = () =>
|
||||
reject(
|
||||
new Error(
|
||||
`Could not reach OBS at ${this.url}. Is Tools → WebSocket Server Settings enabled?`
|
||||
)
|
||||
);
|
||||
socket.onclose = () => {
|
||||
for (const waiter of this.pending.values()) waiter.reject(new Error('OBS disconnected'));
|
||||
this.pending.clear();
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
const { op, d } = JSON.parse(String(event.data));
|
||||
if (op === 0) {
|
||||
const auth = d.authentication as { salt: string; challenge: string } | undefined;
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
op: 1,
|
||||
d: {
|
||||
rpcVersion: 1,
|
||||
eventSubscriptions: 0,
|
||||
...(auth
|
||||
? { authentication: obsAuthentication(this.password, auth.salt, auth.challenge) }
|
||||
: {})
|
||||
}
|
||||
})
|
||||
);
|
||||
} else if (op === 2) resolve();
|
||||
else if (op === 7) {
|
||||
const waiter = this.pending.get(d.requestId);
|
||||
this.pending.delete(d.requestId);
|
||||
if (d.requestStatus?.result) waiter?.resolve(d.responseData);
|
||||
else
|
||||
waiter?.reject(
|
||||
new Error(
|
||||
`OBS ${d.requestType}: ${d.requestStatus?.comment ?? d.requestStatus?.code}`
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
request<T = unknown>(requestType: string, requestData: Record<string, unknown> = {}): Promise<T> {
|
||||
const socket = this.socket;
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN)
|
||||
return Promise.reject(new Error('OBS is not connected'));
|
||||
const requestId = randomUUID();
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.pending.set(requestId, { resolve: resolve as (data: unknown) => void, reject });
|
||||
socket.send(JSON.stringify({ op: 6, d: { requestType, requestId, requestData } }));
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.socket?.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user