/** * 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(); constructor( private url: string, private password: string ) {} get connected() { return this.socket?.readyState === WebSocket.OPEN; } connect(): Promise { 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(requestType: string, requestData: Record = {}): Promise { 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((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(); } }