refactor: modularize CRC mocking and implement a persistent Ogg stream for web audio playback
This commit is contained in:
@@ -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...';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "./mock-crc";
|
||||
import { Client } from "discord.js-selfbot-v13";
|
||||
import { startRecording } from "./recorder";
|
||||
import { config } from "./config";
|
||||
|
||||
31
src/mock-crc.ts
Normal file
31
src/mock-crc.ts
Normal file
@@ -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.");
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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<express.Response>();
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user