refactor: isolate websocket globals

This commit is contained in:
MythEclipse
2026-05-19 14:26:06 +07:00
parent 3639028f6f
commit c3f7291543
2 changed files with 92 additions and 52 deletions
+11 -52
View File
@@ -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;
+81
View File
@@ -0,0 +1,81 @@
import type { createChildLogger } from "../logger";
import type { BroadcasterClient, ModerationBroadcaster } from "../moderation/types";
type Logger = ReturnType<typeof createChildLogger>;
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();
};
}