refactor: isolate websocket globals
This commit is contained in:
+11
-52
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user