Files
2026-07-10 22:02:51 +02:00

154 lines
4.8 KiB
JavaScript

const { PassThrough } = require('stream');
const audio_pipe = new PassThrough();
module.exports = audio_pipe; // Important
const { serverConfig, configExists } = require('../server_config');
if (!configExists() || !serverConfig.audio.audioDevice) return;
const { spawn } = require('child_process');
const { logDebug, logError, logInfo, logWarn } = require('../console');
const checkFFmpeg = function() {
return new Promise((resolve, reject) => {
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
stdio: ['ignore', 'ignore', 'ignore'],
});
checkFFmpegProcess.on('error', () => {
resolve(require('ffmpeg-static'));
});
checkFFmpegProcess.on('exit', (code) => {
if (code === 0) resolve('ffmpeg');
else resolve(require('ffmpeg-static'));
});
});
}
const consoleLogTitle = '[Audio Stream]';
let startupSuccess;
function connectMessage(message) {
if (!startupSuccess) {
logInfo(message);
startupSuccess = true;
}
}
checkFFmpeg().then((ffmpegPath) => {
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
const sampleRate = Number(serverConfig.audio.sampleRate || 32000);
const channels = Number(serverConfig.audio.audioChannels || 2);
let ffmpeg = null;
let restartTimer = null;
let lastTimestamp = null;
let staleCount = 0;
let lastCheckTime = Date.now();
function buildArgs() {
const device = serverConfig.audio.audioDevice;
let inputArgs;
if (process.platform === 'win32') inputArgs = ["-f", "dshow", "-i", `audio=${device}`];
else if (process.platform === 'darwin') inputArgs = ["-f", "avfoundation", "-i", device || ":0"];
else inputArgs = ["-f", "alsa", "-i", device];
return [
"-fflags", "+nobuffer+flush_packets",
"-flags", "low_delay",
"-rtbufsize", "2048",
"-probesize", "128",
...inputArgs,
"-thread_queue_size", "1024",
"-ar", String(sampleRate),
"-ac", String(channels),
"-c:a", "libmp3lame",
"-b:a", serverConfig.audio.audioBitrate,
"-ac", String(channels),
"-reservoir", "0",
"-f", "mp3",
"-write_xing", "0",
"-id3v2_version", "0",
"-fflags", "+nobuffer",
"-flush_packets", "1",
"pipe:1"
];
}
function launchFFmpeg() {
const args = buildArgs();
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${args.join(' ')}`);
ffmpeg = spawn(ffmpegPath, args, {stdio: ['ignore', 'pipe', 'pipe']});
ffmpeg.stdout.pipe(audio_pipe, { end: false });
ffmpeg.stderr.on('data', (data) => {
const msg = data.toString();
// Detect frozen timestamps
const match = msg.match(/time=(\d\d):(\d\d):(\d\d\.\d+)/);
if (match) {
const [_, hh, mm, ss] = match;
const totalSec = parseInt(hh) * 3600 + parseInt(mm) * 60 + parseFloat(ss);
if (lastTimestamp !== null && totalSec === lastTimestamp) {
staleCount++;
const now = Date.now();
if (staleCount >= 10 && now - lastCheckTime > 10000 && !restartTimer) {
logWarn(`${consoleLogTitle} FFmpeg appears frozen. Restarting...`);
restartTimer = setTimeout(() => {
restartTimer = null;
staleCount = 0;
try {
ffmpeg.kill('SIGKILL');
} catch (e) {
logWarn(`${consoleLogTitle} Failed to kill FFmpeg: ${e.message}`);
}
launchFFmpeg();
}, 0);
}
} else {
lastTimestamp = totalSec;
lastCheckTime = Date.now();
staleCount = 0;
}
}
});
ffmpeg.on('exit', (code, signal) => {
if (signal) logWarn(`${consoleLogTitle} FFmpeg killed with signal ${signal}`);
else if (code !== 0) logWarn(`${consoleLogTitle} FFmpeg exited with code ${code}`);
logWarn(`${consoleLogTitle} Restarting FFmpeg in 5 seconds...`);
setTimeout(launchFFmpeg, 5000);
});
}
process.on('SIGINT', () => {
if (ffmpeg) ffmpeg.kill('SIGINT');
process.exit();
});
process.on('exit', () => {
if (ffmpeg) ffmpeg.kill('SIGINT');
});
launchFFmpeg();
}).catch((err) => {
logError(`${consoleLogTitle} Error: ${err.message}`);
});