feat: implement Opus decoder runtime checks and add tests for decoder functionality

This commit is contained in:
MythEclipse
2026-05-13 18:56:44 +07:00
parent 4dadcf3871
commit 220c3b93d2
4 changed files with 77 additions and 7 deletions

View File

@@ -1,7 +1,5 @@
# Discord Bot Configuration # Discord Bot Configuration
DISCORD_TOKEN=your_bot_token_here DISCORD_TOKEN=your_bot_token_here
VOICE_CHANNEL_ID=your_voice_channel_id_here
GUILD_ID=your_guild_id_here
# Recording Configuration # Recording Configuration
RECORDINGS_DIR=./recordings RECORDINGS_DIR=./recordings

View File

@@ -1,6 +1,34 @@
import { createRequire } from "node:module";
import prism from "prism-media"; import prism from "prism-media";
import { config } from "../config"; import { config } from "../config";
const require = createRequire(import.meta.url);
interface OpusDecoderRuntime {
isBun: boolean;
canLoadNativeOpus: boolean;
}
export function shouldEnableDefaultOpusDecoder(
runtime: OpusDecoderRuntime,
): boolean {
return !runtime.isBun || runtime.canLoadNativeOpus;
}
function canLoadNativeOpus(): boolean {
try {
require("@discordjs/opus");
return true;
} catch {
return false;
}
}
const defaultDecoderEnabled = shouldEnableDefaultOpusDecoder({
isBun: Boolean(process.versions.bun),
canLoadNativeOpus: canLoadNativeOpus(),
});
export interface OpusDecoderOptions { export interface OpusDecoderOptions {
cooldownMs: number; cooldownMs: number;
rotateMs: number; rotateMs: number;
@@ -23,8 +51,14 @@ export class OpusDecoder {
this.onData = options.onData; this.onData = options.onData;
this.createDecoderFn = this.createDecoderFn =
options.createDecoder ?? options.createDecoder ??
(() => (() => {
new prism.opus.Decoder({ if (!defaultDecoderEnabled) {
throw new Error(
"Native @discordjs/opus is unavailable under Bun; web PCM decode disabled to avoid opusscript aborts",
);
}
return new prism.opus.Decoder({
frameSize: config.OPUS_FRAME_SIZE, frameSize: config.OPUS_FRAME_SIZE,
channels: config.AUDIO_CHANNELS as 1 | 2, channels: config.AUDIO_CHANNELS as 1 | 2,
rate: config.AUDIO_SAMPLE_RATE as rate: config.AUDIO_SAMPLE_RATE as
@@ -33,7 +67,8 @@ export class OpusDecoder {
| 16000 | 16000
| 24000 | 24000
| 48000, | 48000,
})); });
});
} }
rotateIfNeeded(): void { rotateIfNeeded(): void {

View File

@@ -57,8 +57,12 @@ export function startWebserver(
const wss = new WebSocketServer({ server, path: wsPath }); const wss = new WebSocketServer({ server, path: wsPath });
wsLogger.info({ port, wsPath }, "WebSocket server listening"); wsLogger.info({ port, wsPath }, "WebSocket server listening");
// Security headers // Security headers. CSP disabled because the current static UI uses inline scripts/styles.
app.use(helmet()); app.use(
helmet({
contentSecurityPolicy: false,
}),
);
// HTTP request logging // HTTP request logging
app.use(pinoHttp({ logger })); app.use(pinoHttp({ logger }));

33
tests/decoder.test.ts Normal file
View File

@@ -0,0 +1,33 @@
import process from "node:process";
import { beforeEach, describe, expect, it, vi } from "vitest";
beforeEach(() => {
process.env = {
...process.env,
DISCORD_TOKEN: "token",
NODE_ENV: "test",
};
vi.resetModules();
});
describe("shouldEnableDefaultOpusDecoder", () => {
it("disables default decoder on Bun when native opus is unavailable", async () => {
const { shouldEnableDefaultOpusDecoder } = await import(
"../src/recorder/decoder"
);
expect(
shouldEnableDefaultOpusDecoder({ isBun: true, canLoadNativeOpus: false }),
).toBe(false);
});
it("enables default decoder when native opus is available", async () => {
const { shouldEnableDefaultOpusDecoder } = await import(
"../src/recorder/decoder"
);
expect(
shouldEnableDefaultOpusDecoder({ isBun: true, canLoadNativeOpus: true }),
).toBe(true);
});
});