feat: wire media playback into webserver

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-05-15 17:52:16 +07:00
parent 94e497b7a6
commit 06b6db703c
3 changed files with 39 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ import { createChildLogger } from "../logger";
import type { import type {
AnalysisQueueStatus, AnalysisQueueStatus,
AttachmentRecord, AttachmentRecord,
MediaState,
MessageRecord, MessageRecord,
ModerationWsEvent, ModerationWsEvent,
} from "./types"; } from "./types";
@@ -72,6 +73,9 @@ export function createBroadcaster() {
analysisQueueStatus(data: AnalysisQueueStatus) { analysisQueueStatus(data: AnalysisQueueStatus) {
sendJson(clients, { type: "analysis_queue_status", data }); sendJson(clients, { type: "analysis_queue_status", data });
}, },
mediaState(state: MediaState) {
sendJson(clients, { type: "media_state", state });
},
}; };
} }

View File

@@ -92,6 +92,27 @@ export interface AnalysisResult {
analysis: string; 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 = export type ModerationWsEvent =
| { type: "ui_state"; state: unknown } | { type: "ui_state"; state: unknown }
| { type: "user_state"; users: unknown[] } | { type: "user_state"; users: unknown[] }
@@ -100,7 +121,8 @@ export type ModerationWsEvent =
| { type: "message_deleted"; data: { id: string; deleted_at: number } } | { type: "message_deleted"; data: { id: string; deleted_at: number } }
| { type: "message_analyzed"; data: MessageRecord } | { type: "message_analyzed"; data: MessageRecord }
| { type: "attachment_created"; data: AttachmentRecord } | { 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 { export interface AnalysisQueueStatus {
queuedConversations: number; queuedConversations: number;

View File

@@ -14,10 +14,12 @@ import type { ModerationBroadcaster } from "./moderation/types";
import { getPersistedValue, setPersistedValue } from "./muxer-queue"; import { getPersistedValue, setPersistedValue } from "./muxer-queue";
import { discordPlayer } from "./player"; import { discordPlayer } from "./player";
import { createAnalysisRoutes } from "./routes/analysisRoutes"; import { createAnalysisRoutes } from "./routes/analysisRoutes";
import { createMediaRoutes } from "./routes/mediaRoutes";
import { createMessageRoutes } from "./routes/messageRoutes"; import { createMessageRoutes } from "./routes/messageRoutes";
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 { MediaController } from "./media/mediaController";
import type { VoiceController } from "./voiceController"; import type { VoiceController } from "./voiceController";
const wsLogger = createChildLogger("webserver"); const wsLogger = createChildLogger("webserver");
@@ -160,6 +162,12 @@ export async function startWebserver(
const broadcaster = createBroadcaster(); const broadcaster = createBroadcaster();
(globalThis as VoiceGlobals).moderationBroadcaster = broadcaster; (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. // Security headers. CSP disabled because the current static UI uses inline scripts/styles.
app.use( app.use(
helmet({ helmet({
@@ -234,6 +242,7 @@ export async function startWebserver(
app.use("/api", createMessageRoutes()); app.use("/api", createMessageRoutes());
app.use("/api", createAnalysisRoutes()); app.use("/api", createAnalysisRoutes());
app.use("/api", createSyncRoutes(_client)); app.use("/api", createSyncRoutes(_client));
app.use("/api", createMediaRoutes(mediaController));
// Inbound: Discord PCM → tagged chunks → browser // Inbound: Discord PCM → tagged chunks → browser
(globalThis as VoiceGlobals).broadcastPcmToWeb = ( (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: "ui_state", state: getSharedUIState() }));
ws.send(JSON.stringify({ type: "media_state", state: mediaController.getState() }));
ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => { ws.on("message", (data: Buffer | ArrayBuffer | Buffer[]) => {
if (!Buffer.isBuffer(data)) return; if (!Buffer.isBuffer(data)) return;