feat: implement WebCodecs Opus decode for browser listen

- Add broadcastOpusToWeb to PcmBroadcaster interface for raw Opus packets
- Server broadcasts Opus frames with mode byte (1) + user hash + packet data
- Browser detects packet mode: mode=1 for Opus, mode=0 for legacy PCM
- Implement WebCodecs AudioDecoder for Opus decoding in browser
- Keep existing PCM playback as fallback for compatibility
- Show error if WebCodecs unsupported
- Fixes listen feature under Bun where native Opus decode unavailable
This commit is contained in:
MythEclipse
2026-05-13 22:13:03 +07:00
parent 251a176b2b
commit 25dbd8413b
11 changed files with 1308 additions and 958 deletions
+50
View File
@@ -10,6 +10,7 @@ import { AppError } from "./errors";
import { createChildLogger, logger } from "./logger";
import { getMetrics, uptimeGauge } from "./metrics";
import { discordPlayer } from "./player";
import { renderDashboardPage } from "./web/dashboardPage";
import type { VoiceController } from "./voiceController";
import { getDatabase } from "./muxer-queue";
import { getMessagesByChannel, getAttachmentsByChannel } from "./moderation/messageStore";
@@ -70,6 +71,39 @@ export function startWebserver(
app.use(pinoHttp({ logger }));
app.use(express.json());
app.get("/", async (req, res, next) => {
try {
const guilds = voiceController.listGuilds();
const selectedGuildId =
typeof req.query.guild === "string" ? req.query.guild : guilds[0]?.id || "";
const selectedChannelId =
typeof req.query.channel === "string" ? req.query.channel : "";
const [voiceChannels, watchChannels] = selectedGuildId
? await Promise.all([
voiceController.listVoiceChannels(selectedGuildId),
voiceController.listWatchableChannels(selectedGuildId),
])
: [[], []];
const messages = selectedChannelId
? getMessagesByChannel(getDatabase(), selectedChannelId, 80, 0)
: [];
res.type("html").send(
renderDashboardPage({
guilds,
voiceChannels,
watchChannels,
selectedGuildId,
selectedChannelId,
messages,
status: voiceController.getStatus(),
}),
);
} catch (error) {
next(error);
}
});
app.use(express.static(path.join(__dirname, "../public")));
// Health check endpoint
@@ -204,6 +238,22 @@ export function startWebserver(
});
};
// Inbound: Discord Opus → tagged chunks → browser (WebCodecs decode)
(global as any).broadcastOpusToWeb = (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(5);
header.writeUInt8(1, 0); // mode: 1 = Opus
header.writeInt32LE(hash, 1);
const packet = Buffer.concat([header, chunk]);
wsClients.forEach((client) => {
if (client.readyState === 1) client.send(packet);
});
};
(global as any).updateActiveUser = (
userId: string,
data: { username: string; avatar: string; speaking: boolean },