From 220c3b93d2650438a8fcc05a226aaf517963c0e0 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 13 May 2026 18:56:44 +0700 Subject: [PATCH] feat: implement Opus decoder runtime checks and add tests for decoder functionality --- .env.example | 2 -- src/recorder/decoder.ts | 41 ++++++++++++++++++++++++++++++++++++++--- src/webserver.ts | 8 ++++++-- tests/decoder.test.ts | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 tests/decoder.test.ts diff --git a/.env.example b/.env.example index 39659db..f28a383 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,5 @@ # Discord Bot Configuration DISCORD_TOKEN=your_bot_token_here -VOICE_CHANNEL_ID=your_voice_channel_id_here -GUILD_ID=your_guild_id_here # Recording Configuration RECORDINGS_DIR=./recordings diff --git a/src/recorder/decoder.ts b/src/recorder/decoder.ts index 029bfe6..b4e4814 100644 --- a/src/recorder/decoder.ts +++ b/src/recorder/decoder.ts @@ -1,6 +1,34 @@ +import { createRequire } from "node:module"; import prism from "prism-media"; 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 { cooldownMs: number; rotateMs: number; @@ -23,8 +51,14 @@ export class OpusDecoder { this.onData = options.onData; this.createDecoderFn = 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, channels: config.AUDIO_CHANNELS as 1 | 2, rate: config.AUDIO_SAMPLE_RATE as @@ -33,7 +67,8 @@ export class OpusDecoder { | 16000 | 24000 | 48000, - })); + }); + }); } rotateIfNeeded(): void { diff --git a/src/webserver.ts b/src/webserver.ts index 98f12c9..3dd9472 100644 --- a/src/webserver.ts +++ b/src/webserver.ts @@ -57,8 +57,12 @@ export function startWebserver( const wss = new WebSocketServer({ server, path: wsPath }); wsLogger.info({ port, wsPath }, "WebSocket server listening"); - // Security headers - app.use(helmet()); + // Security headers. CSP disabled because the current static UI uses inline scripts/styles. + app.use( + helmet({ + contentSecurityPolicy: false, + }), + ); // HTTP request logging app.use(pinoHttp({ logger })); diff --git a/tests/decoder.test.ts b/tests/decoder.test.ts new file mode 100644 index 0000000..6ae9611 --- /dev/null +++ b/tests/decoder.test.ts @@ -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); + }); +});