diff --git a/public/index.html b/public/index.html
index 8fff19f..11b154f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -163,9 +163,12 @@
listenStatus.innerText = 'Disconnected';
isListening = false;
} else {
- discordAudio.src = '/listen';
+ discordAudio.src = '/listen?t=' + Date.now();
discordAudio.style.display = 'block';
- discordAudio.play();
+ discordAudio.play().catch(err => {
+ console.error('Playback error:', err);
+ listenStatus.innerText = 'Playback failed: ' + err.message;
+ });
listenBtn.innerText = 'Stop Listening';
listenBtn.style.backgroundColor = '#f04747';
listenStatus.innerText = 'Listening live...';
diff --git a/src/index.ts b/src/index.ts
index e769749..adc6dfe 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,4 @@
+import "./mock-crc";
import { Client } from "discord.js-selfbot-v13";
import { startRecording } from "./recorder";
import { config } from "./config";
diff --git a/src/mock-crc.ts b/src/mock-crc.ts
new file mode 100644
index 0000000..75afa51
--- /dev/null
+++ b/src/mock-crc.ts
@@ -0,0 +1,31 @@
+// Mock node-crc to provide pure JS implementation and bypass native build issues
+const CRC_TABLE = new Uint32Array(256);
+for (let i = 0; i < 256; i++) {
+ let r = i << 24;
+ for (let j = 0; j < 8; j++) {
+ r = (r & 0x80000000) !== 0 ? ((r << 1) ^ 0x04c11db7) : (r << 1);
+ }
+ CRC_TABLE[i] = (r >>> 0);
+}
+
+const Module = require('module');
+const originalRequire = Module.prototype.require;
+Module.prototype.require = function (id: string) {
+ if (id === 'node-crc') {
+ return {
+ crc: function (width: number, reflectIn: boolean, poly: number, init: number, refOut: boolean, xorOut: number, unk1: number, unk2: number, buffer: Buffer) {
+ let crc = 0;
+ for (let i = 0; i < buffer.length; i++) {
+ crc = ((crc << 8) >>> 0) ^ CRC_TABLE[((crc >>> 24) ^ buffer[i]) & 0xff];
+ crc >>>= 0;
+ }
+ const result = Buffer.alloc(4);
+ result.writeUInt32BE(crc, 0);
+ return result;
+ }
+ };
+ }
+ return originalRequire.apply(this, arguments);
+};
+
+console.log("[mock] node-crc has been mocked globally.");
diff --git a/src/player.ts b/src/player.ts
index 4c16269..a28856b 100644
--- a/src/player.ts
+++ b/src/player.ts
@@ -32,9 +32,12 @@ export class DiscordPlayer {
}
public playStream(stream: Readable) {
+ console.log("[player] Starting new audio stream...");
// Use WebmDemuxer to extract Opus packets from browser stream
const demuxer = new prism.opus.WebmDemuxer();
+ demuxer.on('error', err => console.error("[player] Demuxer error:", err));
+
const resource = createAudioResource(stream.pipe(demuxer), {
inputType: StreamType.Opus,
});
diff --git a/src/recorder.ts b/src/recorder.ts
index 1fea65b..0dd3f04 100644
--- a/src/recorder.ts
+++ b/src/recorder.ts
@@ -94,35 +94,6 @@ export async function startRecording(client: Client, channel: VoiceChannel): Pro
try {
const packetFilter = new PacketFilter(10);
- // Mock node-crc to provide pure JS implementation and bypass native build issues
- const CRC_TABLE = new Uint32Array(256);
- for (let i = 0; i < 256; i++) {
- let r = i << 24;
- for (let j = 0; j < 8; j++) {
- r = (r & 0x80000000) !== 0 ? ((r << 1) ^ 0x04c11db7) : (r << 1);
- }
- CRC_TABLE[i] = (r >>> 0);
- }
-
- const Module = require('module');
- const originalRequire = Module.prototype.require;
- Module.prototype.require = function (id: string) {
- if (id === 'node-crc') {
- return {
- crc: function (width: number, reflectIn: boolean, poly: number, init: number, refOut: boolean, xorOut: number, unk1: number, unk2: number, buffer: Buffer) {
- let crc = 0;
- for (let i = 0; i < buffer.length; i++) {
- crc = ((crc << 8) >>> 0) ^ CRC_TABLE[((crc >>> 24) ^ buffer[i]) & 0xff];
- crc >>>= 0;
- }
- const result = Buffer.alloc(4);
- result.writeUInt32BE(crc, 0);
- return result;
- }
- };
- }
- return originalRequire.apply(this, arguments);
- };
const oggStream = new prism.opus.OggLogicalBitstream({
opusHead: new prism.opus.OpusHead({
@@ -139,10 +110,10 @@ export async function startRecording(client: Client, channel: VoiceChannel): Pro
// Pipe: audioStream -> packetFilter -> oggStream -> out
audioStream.pipe(packetFilter).pipe(oggStream).pipe(out);
- // Also forward to web listeners
- oggStream.on('data', (chunk) => {
- if ((global as any).broadcastToWeb) {
- (global as any).broadcastToWeb(chunk);
+ // Forward raw Opus packets to the web shared Ogg stream
+ packetFilter.on('data', (chunk) => {
+ if ((global as any).broadcastOpusToWeb) {
+ (global as any).broadcastOpusToWeb(chunk);
}
});
diff --git a/src/webserver.ts b/src/webserver.ts
index c9fdf5b..1d80289 100644
--- a/src/webserver.ts
+++ b/src/webserver.ts
@@ -4,6 +4,7 @@ import http from "http";
import path from "path";
import { PassThrough } from "stream";
import { discordPlayer } from "./player";
+import prism from "prism-media";
export function startWebserver(port: number = 3000) {
const app = express();
@@ -12,14 +13,41 @@ export function startWebserver(port: number = 3000) {
const listeners = new Set();
let headerChunks: Buffer[] = [];
+
+ // Create a single, continuous Ogg stream for all web listeners
+ const oggStream = new prism.opus.OggLogicalBitstream({
+ opusHead: new prism.opus.OpusHead({
+ channelCount: 2,
+ sampleRate: 48000,
+ }),
+ pageSizeControl: {
+ maxPackets: 10,
+ },
+ });
+
+ // Forward Ogg pages to all connected web listeners
+ oggStream.on("data", (chunk) => {
+ // Cache the first 2 chunks (headers)
+ if (headerChunks.length < 2) {
+ headerChunks.push(chunk);
+ }
+ listeners.forEach(res => res.write(chunk));
+ });
+
+ // Prime the stream with a silent packet to generate headers immediately
+ // Silent Opus packet (1 frame, 20ms)
+ const silentPacket = Buffer.from([0xf8, 0xff, 0xfe]);
+ oggStream.write(silentPacket);
app.use(express.static(path.join(__dirname, "../public")));
// Endpoint for receiving (listening) audio from Discord
app.get("/listen", (req, res) => {
res.setHeader("Content-Type", "audio/ogg");
+ res.setHeader("Transfer-Encoding", "chunked");
+ res.setHeader("Connection", "keep-alive");
- // Send cached headers so the browser can decode the stream
+ // Send cached headers immediately so the browser recognizes the stream
headerChunks.forEach(chunk => res.write(chunk));
listeners.add(res);
@@ -31,13 +59,9 @@ export function startWebserver(port: number = 3000) {
});
});
- // Function to broadcast audio chunks to all listeners
- (global as any).broadcastToWeb = (chunk: Buffer) => {
- // Store the first two chunks as headers (OpusHead and OpusTags)
- if (headerChunks.length < 2) {
- headerChunks.push(chunk);
- }
- listeners.forEach(res => res.write(chunk));
+ // Function to broadcast raw Opus packets from Discord to the shared Ogg stream
+ (global as any).broadcastOpusToWeb = (chunk: Buffer) => {
+ oggStream.write(chunk);
};
wss.on("connection", (ws) => {
@@ -47,7 +71,7 @@ export function startWebserver(port: number = 3000) {
discordPlayer.playStream(audioStream);
ws.on("message", (data: Buffer) => {
- // Write incoming audio chunks to the stream
+ // console.log(`[webserver] Received chunk: ${data.length} bytes`);
audioStream.write(data);
});