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); });