From 06b6db703c68b3e392986a5a49d5dc04fe9dda4f Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Fri, 15 May 2026 17:52:16 +0700 Subject: [PATCH] feat: wire media playback into webserver Co-Authored-By: Claude Opus 4.7 --- src/moderation/broadcaster.ts | 6 +++++- src/moderation/types.ts | 26 ++++++++++++++++++++++++-- src/webserver.ts | 10 ++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/moderation/broadcaster.ts b/src/moderation/broadcaster.ts index 4190670..f49d800 100644 --- a/src/moderation/broadcaster.ts +++ b/src/moderation/broadcaster.ts @@ -3,6 +3,7 @@ import { createChildLogger } from "../logger"; import type { AnalysisQueueStatus, AttachmentRecord, + MediaState, MessageRecord, ModerationWsEvent, } from "./types"; @@ -72,7 +73,10 @@ export function createBroadcaster() { analysisQueueStatus(data: AnalysisQueueStatus) { sendJson(clients, { type: "analysis_queue_status", data }); }, + mediaState(state: MediaState) { + sendJson(clients, { type: "media_state", state }); + }, }; } -export type ModerationBroadcaster = ReturnType; +export type ModerationBroadcaster = ReturnType; \ No newline at end of file diff --git a/src/moderation/types.ts b/src/moderation/types.ts index 2fafa72..baa627b 100644 --- a/src/moderation/types.ts +++ b/src/moderation/types.ts @@ -92,6 +92,27 @@ export interface AnalysisResult { analysis: string; } +export type MediaMode = "music" | "screen"; +export type MediaSourceKind = "url" | "local"; +export type MediaQueueItemStatus = "queued" | "playing" | "failed"; + +export interface MediaQueueItem { + id: string; + mode: MediaMode; + source: string; + title: string; + kind: MediaSourceKind; + requestedBy: string; + addedAt: number; + status: MediaQueueItemStatus; +} + +export interface MediaState { + playing: boolean; + current: MediaQueueItem | null; + queue: MediaQueueItem[]; +} + export type ModerationWsEvent = | { type: "ui_state"; state: unknown } | { type: "user_state"; users: unknown[] } @@ -100,10 +121,11 @@ export type ModerationWsEvent = | { type: "message_deleted"; data: { id: string; deleted_at: number } } | { type: "message_analyzed"; data: MessageRecord } | { type: "attachment_created"; data: AttachmentRecord } - | { type: "analysis_queue_status"; data: AnalysisQueueStatus }; + | { type: "analysis_queue_status"; data: AnalysisQueueStatus } + | { type: "media_state"; state: MediaState }; export interface AnalysisQueueStatus { queuedConversations: number; activeRequests: number; lastError: string | null; -} +} \ No newline at end of file diff --git a/src/webserver.ts b/src/webserver.ts index 68e7aa7..d1e3790 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -14,10 +14,12 @@ import type { ModerationBroadcaster } from "./moderation/types"; import { getPersistedValue, setPersistedValue } from "./muxer-queue"; import { discordPlayer } from "./player"; import { createAnalysisRoutes } from "./routes/analysisRoutes"; +import { createMediaRoutes } from "./routes/mediaRoutes"; import { createMessageRoutes } from "./routes/messageRoutes"; import { createSyncRoutes } from "./routes/syncRoutes"; import { createUIStateRoutes } from "./routes/uiStateRoutes"; import { createVoiceRoutes } from "./routes/voiceRoutes"; +import { MediaController } from "./media/mediaController"; import type { VoiceController } from "./voiceController"; const wsLogger = createChildLogger("webserver"); @@ -160,6 +162,12 @@ export async function startWebserver( const broadcaster = createBroadcaster(); (globalThis as VoiceGlobals).moderationBroadcaster = broadcaster; + const mediaController = new MediaController({ + isVoiceConnected: () => voiceController.getStatus().connected, + isBrowserStreaming: () => sharedUIState.isStreaming, + onStateChange: (state) => broadcaster.mediaState(state), + }); + // Security headers. CSP disabled because the current static UI uses inline scripts/styles. app.use( helmet({ @@ -234,6 +242,7 @@ export async function startWebserver( app.use("/api", createMessageRoutes()); app.use("/api", createAnalysisRoutes()); app.use("/api", createSyncRoutes(_client)); + app.use("/api", createMediaRoutes(mediaController)); // Inbound: Discord PCM → tagged chunks → browser (globalThis as VoiceGlobals).broadcastPcmToWeb = ( @@ -369,6 +378,7 @@ export async function startWebserver( }), ); ws.send(JSON.stringify({ type: "ui_state", state: getSharedUIState() })); + ws.send(JSON.stringify({ type: "media_state", state: mediaController.getState() })); ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => { if (!Buffer.isBuffer(data)) return;