feat: wire media playback into webserver
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user