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 { createScreenShareController } from "./media/screenShareController";
|
||||||
import { getMetrics, uptimeGauge } from "./metrics";
|
import { getMetrics, uptimeGauge } from "./metrics";
|
||||||
import { createBroadcaster } from "./moderation/broadcaster";
|
import { createBroadcaster } from "./moderation/broadcaster";
|
||||||
import type { ModerationBroadcaster } from "./moderation/types";
|
|
||||||
import { createSharedUIStateStore } from "./state/uiState";
|
import { createSharedUIStateStore } from "./state/uiState";
|
||||||
import { Streamer } from "./streaming";
|
import { Streamer } from "./streaming";
|
||||||
import type { VoiceController } from "./voiceController";
|
import type { VoiceController } from "./voiceController";
|
||||||
@@ -36,6 +35,12 @@ import { createRecordingsRoutes } from "./routes/recordingsRoutes";
|
|||||||
import { createSyncRoutes } from "./routes/syncRoutes";
|
import { createSyncRoutes } from "./routes/syncRoutes";
|
||||||
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
import { createUIStateRoutes } from "./routes/uiStateRoutes";
|
||||||
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
import { createVoiceRoutes } from "./routes/voiceRoutes";
|
||||||
|
import {
|
||||||
|
exposeActiveUserGlobal,
|
||||||
|
exposeModerationGlobals,
|
||||||
|
exposePcmBroadcastGlobal,
|
||||||
|
exposeVideoBroadcastGlobal,
|
||||||
|
} from "./ws/broadcastGlobals";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -47,16 +52,6 @@ const activeUsers = new Map<
|
|||||||
{ username: string; avatar: string; speaking: boolean }
|
{ 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(
|
export async function startWebserver(
|
||||||
port: number = 3000,
|
port: number = 3000,
|
||||||
_client: Client,
|
_client: Client,
|
||||||
@@ -74,8 +69,7 @@ export async function startWebserver(
|
|||||||
|
|
||||||
// Create broadcaster instance
|
// Create broadcaster instance
|
||||||
const broadcaster = createBroadcaster();
|
const broadcaster = createBroadcaster();
|
||||||
(globalThis as VoiceGlobals).moderationBroadcaster = broadcaster;
|
exposeModerationGlobals(broadcaster, config.ADMIN_PASSWORD);
|
||||||
(globalThis as any).ADMIN_PASSWORD = config.ADMIN_PASSWORD;
|
|
||||||
|
|
||||||
const streamer = new Streamer(_client);
|
const streamer = new Streamer(_client);
|
||||||
const screenController = createScreenShareController({
|
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() {
|
function broadcastUserState() {
|
||||||
const users = Array.from(activeUsers.entries()).map(([id, data]) => ({
|
const users = Array.from(activeUsers.entries()).map(([id, data]) => ({
|
||||||
id,
|
id,
|
||||||
@@ -249,6 +204,10 @@ export async function startWebserver(
|
|||||||
broadcaster.userState(users);
|
broadcaster.userState(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exposePcmBroadcastGlobal(broadcaster);
|
||||||
|
exposeVideoBroadcastGlobal(() => broadcaster.getClients(), wsLogger);
|
||||||
|
exposeActiveUserGlobal(activeUsers, broadcastUserState);
|
||||||
|
|
||||||
// --- Outbound: browser PCM (24kHz mono) → Opus → Discord ---
|
// --- Outbound: browser PCM (24kHz mono) → Opus → Discord ---
|
||||||
const RATE = 48000;
|
const RATE = 48000;
|
||||||
const CHANNELS = 2;
|
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