diff --git a/src/webserver.ts b/src/webserver.ts index 0637293..3fd29f7 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -20,7 +20,6 @@ import { MediaController } from "./media/mediaController"; import { createScreenShareController } from "./media/screenShareController"; import { getMetrics, uptimeGauge } from "./metrics"; import { createBroadcaster } from "./moderation/broadcaster"; -import type { ModerationBroadcaster } from "./moderation/types"; import { createSharedUIStateStore } from "./state/uiState"; import { Streamer } from "./streaming"; import type { VoiceController } from "./voiceController"; @@ -36,6 +35,12 @@ import { createRecordingsRoutes } from "./routes/recordingsRoutes"; import { createSyncRoutes } from "./routes/syncRoutes"; import { createUIStateRoutes } from "./routes/uiStateRoutes"; import { createVoiceRoutes } from "./routes/voiceRoutes"; +import { + exposeActiveUserGlobal, + exposeModerationGlobals, + exposePcmBroadcastGlobal, + exposeVideoBroadcastGlobal, +} from "./ws/broadcastGlobals"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -47,16 +52,6 @@ const activeUsers = new Map< { username: string; avatar: string; speaking: boolean } >(); -type VoiceGlobals = typeof globalThis & { - moderationBroadcaster?: ModerationBroadcaster; - broadcastPcmToWeb?: (chunk: Buffer, userId: string) => void; - broadcastVideoToWeb?: (chunk: Buffer) => void; - updateActiveUser?: ( - userId: string, - data: { username: string; avatar: string; speaking: boolean }, - ) => void; -}; - export async function startWebserver( port: number = 3000, _client: Client, @@ -74,8 +69,7 @@ export async function startWebserver( // Create broadcaster instance const broadcaster = createBroadcaster(); - (globalThis as VoiceGlobals).moderationBroadcaster = broadcaster; - (globalThis as any).ADMIN_PASSWORD = config.ADMIN_PASSWORD; + exposeModerationGlobals(broadcaster, config.ADMIN_PASSWORD); const streamer = new Streamer(_client); const screenController = createScreenShareController({ @@ -202,45 +196,6 @@ export async function startWebserver( }), ); - // Inbound: Discord PCM → tagged chunks → browser - (globalThis as VoiceGlobals).broadcastPcmToWeb = ( - chunk: Buffer, - userId: string, - ) => { - let hash = 0; - for (let i = 0; i < userId.length; i++) { - hash = (hash << 5) - hash + userId.charCodeAt(i); - hash |= 0; - } - const header = Buffer.alloc(4); - header.writeInt32LE(hash, 0); - const packet = Buffer.concat([header, chunk]); - for (const client of broadcaster.getClients()) { - if (client.readyState === 1) client.send(packet); - } - }; - - // Outbound: server video stream (matroska chunks) -> browser clients - (globalThis as VoiceGlobals).broadcastVideoToWeb = (chunk: Buffer) => { - for (const client of broadcaster.getClients()) { - if (client.readyState === 1) { - try { - client.send(chunk); - } catch (err) { - wsLogger.warn({ err }, "Failed to send video chunk"); - } - } - } - }; - - (globalThis as VoiceGlobals).updateActiveUser = ( - userId: string, - data: { username: string; avatar: string; speaking: boolean }, - ) => { - activeUsers.set(userId, data); - broadcastUserState(); - }; - function broadcastUserState() { const users = Array.from(activeUsers.entries()).map(([id, data]) => ({ id, @@ -249,6 +204,10 @@ export async function startWebserver( broadcaster.userState(users); } + exposePcmBroadcastGlobal(broadcaster); + exposeVideoBroadcastGlobal(() => broadcaster.getClients(), wsLogger); + exposeActiveUserGlobal(activeUsers, broadcastUserState); + // --- Outbound: browser PCM (24kHz mono) → Opus → Discord --- const RATE = 48000; const CHANNELS = 2; diff --git a/src/ws/broadcastGlobals.ts b/src/ws/broadcastGlobals.ts new file mode 100644 index 0000000..6a6c554 --- /dev/null +++ b/src/ws/broadcastGlobals.ts @@ -0,0 +1,81 @@ +import type { createChildLogger } from "../logger"; +import type { BroadcasterClient, ModerationBroadcaster } from "../moderation/types"; + +type Logger = ReturnType; + +type ActiveUsers = Map< + string, + { username: string; avatar: string; speaking: boolean } +>; + +type VoiceGlobals = typeof globalThis & { + ADMIN_PASSWORD?: string; + moderationBroadcaster?: ModerationBroadcaster; + broadcastPcmToWeb?: (chunk: Buffer, userId: string) => void; + broadcastVideoToWeb?: (chunk: Buffer) => void; + updateActiveUser?: ( + userId: string, + data: { username: string; avatar: string; speaking: boolean }, + ) => void; +}; + +export function exposeModerationGlobals( + broadcaster: ModerationBroadcaster, + adminPassword: string, +): void { + (globalThis as VoiceGlobals).moderationBroadcaster = broadcaster; + (globalThis as VoiceGlobals).ADMIN_PASSWORD = adminPassword; +} + +export function exposePcmBroadcastGlobal( + broadcaster: ModerationBroadcaster, +): void { + (globalThis as VoiceGlobals).broadcastPcmToWeb = ( + chunk: Buffer, + userId: string, + ) => { + let hash = 0; + for (let i = 0; i < userId.length; i++) { + hash = (hash << 5) - hash + userId.charCodeAt(i); + hash |= 0; + } + + const header = Buffer.alloc(4); + header.writeInt32LE(hash, 0); + const packet = Buffer.concat([header, chunk]); + + for (const client of broadcaster.getClients()) { + if (client.readyState === 1) client.send(packet); + } + }; +} + +export function exposeVideoBroadcastGlobal( + clients: () => BroadcasterClient[], + logger: Logger, +): void { + (globalThis as VoiceGlobals).broadcastVideoToWeb = (chunk: Buffer) => { + for (const client of clients()) { + if (client.readyState === 1) { + try { + client.send(chunk); + } catch (err) { + logger.warn({ err }, "Failed to send video chunk"); + } + } + } + }; +} + +export function exposeActiveUserGlobal( + activeUsers: ActiveUsers, + broadcastUserState: () => void, +): void { + (globalThis as VoiceGlobals).updateActiveUser = ( + userId: string, + data: { username: string; avatar: string; speaking: boolean }, + ) => { + activeUsers.set(userId, data); + broadcastUserState(); + }; +}