You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 14:11:59 +01:00
Merge pull request #160 from AmateurAudioDude/update/v1.3.9-remove-audio-proxy
Remove need for audio proxy (http-proxy)
This commit is contained in:
@@ -4,7 +4,6 @@ const endpoints = require('./endpoints');
|
|||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const httpProxy = require('http-proxy');
|
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
@@ -18,6 +17,7 @@ const path = require('path');
|
|||||||
const net = require('net');
|
const net = require('net');
|
||||||
const client = new net.Socket();
|
const client = new net.Socket();
|
||||||
const { SerialPort } = require('serialport');
|
const { SerialPort } = require('serialport');
|
||||||
|
const audioServer = require('./stream/3las.server');
|
||||||
const tunnel = require('./tunnel');
|
const tunnel = require('./tunnel');
|
||||||
|
|
||||||
// File imports
|
// File imports
|
||||||
@@ -94,13 +94,6 @@ console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
|||||||
require('./stream/index');
|
require('./stream/index');
|
||||||
require('./plugins');
|
require('./plugins');
|
||||||
|
|
||||||
// Create a WebSocket proxy instance
|
|
||||||
const proxy = httpProxy.createProxyServer({
|
|
||||||
target: 'ws://localhost:' + (Number(serverConfig.webserver.webserverPort) + 10), // WebSocket httpServer's address
|
|
||||||
ws: true, // Enable WebSocket proxying
|
|
||||||
changeOrigin: true // Change the origin of the host header to the target URL
|
|
||||||
});
|
|
||||||
|
|
||||||
let currentUsers = 0;
|
let currentUsers = 0;
|
||||||
let serialport;
|
let serialport;
|
||||||
|
|
||||||
@@ -390,12 +383,12 @@ wss.on('connection', (ws, request) => {
|
|||||||
const userCommandHistory = {};
|
const userCommandHistory = {};
|
||||||
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
|
||||||
|
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientIp.includes(',')) {
|
if (clientIp && clientIp.includes(',')) {
|
||||||
clientIp = clientIp.split(',')[0].trim();
|
clientIp = clientIp.split(',')[0].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,15 +707,15 @@ httpServer.on('upgrade', (request, socket, head) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (request.url === '/audio') {
|
} else if (request.url === '/audio') {
|
||||||
isPortOpen('localhost', (Number(serverConfig.webserver.webserverPort) + 10)).then((open) => {
|
if (typeof audioServer?.handleAudioUpgrade === 'function') {
|
||||||
if (open) {
|
audioServer.handleAudioUpgrade(request, socket, head, (ws) => {
|
||||||
proxy.ws(request, socket, head);
|
audioServer.Server?.Server?.emit?.('connection', ws, request);
|
||||||
} else {
|
});
|
||||||
logWarn(`Audio stream port ${(Number(serverConfig.webserver.webserverPort) + 10)} not yet open — skipping proxy connection.`);
|
} else {
|
||||||
socket.end(); // close socket so client isn't left hanging
|
logWarn('[Audio WebSocket] Audio server not ready — dropping client connection.');
|
||||||
}
|
socket.destroy();
|
||||||
});
|
}
|
||||||
} else if (request.url === '/chat') {
|
} else if (request.url === '/chat') {
|
||||||
sessionMiddleware(request, {}, () => {
|
sessionMiddleware(request, {}, () => {
|
||||||
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
chatWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
chatWss.emit('connection', ws, request);
|
chatWss.emit('connection', ws, request);
|
||||||
@@ -733,21 +726,21 @@ httpServer.on('upgrade', (request, socket, head) => {
|
|||||||
rdsWss.handleUpgrade(request, socket, head, (ws) => {
|
rdsWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
rdsWss.emit('connection', ws, request);
|
rdsWss.emit('connection', ws, request);
|
||||||
|
|
||||||
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
|
||||||
const userCommandHistory = {};
|
const userCommandHistory = {};
|
||||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||||
ws.close(1008, 'Banned IP');
|
ws.close(1008, 'Banned IP');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anti-spam tracking for each client
|
// Anti-spam tracking for each client
|
||||||
const userCommands = {};
|
const userCommands = {};
|
||||||
let lastWarn = { time: 0 };
|
let lastWarn = { time: 0 };
|
||||||
|
|
||||||
ws.on('message', function incoming(message) {
|
ws.on('message', function incoming(message) {
|
||||||
// Anti-spam
|
// Anti-spam
|
||||||
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
|
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
var fs = require('fs');
|
|
||||||
const checkFFmpeg = require('./checkFFmpeg');
|
|
||||||
const {serverConfig} = require('../server_config');
|
|
||||||
|
|
||||||
let ffmpegStaticPath;
|
|
||||||
|
|
||||||
function runStream() {
|
|
||||||
/*
|
/*
|
||||||
Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming)
|
Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming)
|
||||||
https://github.com/JoJoBond/3LAS
|
https://github.com/JoJoBond/3LAS
|
||||||
*/
|
*/
|
||||||
|
var fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const checkFFmpeg = require('./checkFFmpeg');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
||||||
|
const { serverConfig } = require('../server_config');
|
||||||
|
|
||||||
|
let ffmpegStaticPath = 'ffmpeg'; // fallback value
|
||||||
|
|
||||||
|
let ServerInstance;
|
||||||
|
let handleAudioUpgradeFn;
|
||||||
|
|
||||||
|
let readyResolve;
|
||||||
|
const waitUntilReady = new Promise((resolve) => {
|
||||||
|
readyResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
checkFFmpeg().then((resolvedPath) => {
|
||||||
|
ffmpegStaticPath = resolvedPath;
|
||||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||||
if (k2 === undefined) k2 = k;
|
if (k2 === undefined) k2 = k;
|
||||||
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
||||||
@@ -42,7 +54,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||||||
const fs_1 = require("fs");
|
const fs_1 = require("fs");
|
||||||
const child_process_1 = require("child_process");
|
const child_process_1 = require("child_process");
|
||||||
const ws = __importStar(require("ws"));
|
const ws = __importStar(require("ws"));
|
||||||
const Settings = JSON.parse((0, fs_1.readFileSync)('server/stream/settings.json', 'utf-8'));
|
const Settings = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'settings.json'), 'utf-8'));
|
||||||
const FFmpeg_command = ffmpegStaticPath;
|
const FFmpeg_command = ffmpegStaticPath;
|
||||||
class StreamClient {
|
class StreamClient {
|
||||||
constructor(server, socket) {
|
constructor(server, socket) {
|
||||||
@@ -117,7 +129,7 @@ class StreamClient {
|
|||||||
}
|
}
|
||||||
class StreamServer {
|
class StreamServer {
|
||||||
constructor(port, channels, sampleRate) {
|
constructor(port, channels, sampleRate) {
|
||||||
this.Port = port;
|
this.Port = port || null;
|
||||||
this.Channels = channels;
|
this.Channels = channels;
|
||||||
this.SampleRate = sampleRate;
|
this.SampleRate = sampleRate;
|
||||||
this.Clients = new Set();
|
this.Clients = new Set();
|
||||||
@@ -139,12 +151,21 @@ class StreamServer {
|
|||||||
}
|
}
|
||||||
Run() {
|
Run() {
|
||||||
this.Server = new ws.Server({
|
this.Server = new ws.Server({
|
||||||
"host": ["127.0.0.1", "::1"],
|
noServer: true,
|
||||||
"port": this.Port,
|
clientTracking: true,
|
||||||
"clientTracking": true,
|
perMessageDeflate: false,
|
||||||
"perMessageDeflate": false
|
|
||||||
});
|
});
|
||||||
|
// Allow manual upgrade handling from index.js
|
||||||
|
this.handleUpgrade = (req, socket, head) => {
|
||||||
|
this.Server.handleUpgrade(req, socket, head, (ws) => {
|
||||||
|
this.Server.emit('connection', ws, req);
|
||||||
|
});
|
||||||
|
};
|
||||||
this.Server.on('connection', this.OnServerConnection.bind(this));
|
this.Server.on('connection', this.OnServerConnection.bind(this));
|
||||||
|
if (!this.StdIn) {
|
||||||
|
logError('[Stream] No audio input stream defined (this.StdIn is null)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.StdIn.on('data', this.OnStdInData.bind(this));
|
this.StdIn.on('data', this.OnStdInData.bind(this));
|
||||||
this.StdIn.resume();
|
this.StdIn.resume();
|
||||||
}
|
}
|
||||||
@@ -195,6 +216,7 @@ class StreamServer {
|
|||||||
"wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0),
|
"wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0),
|
||||||
"mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0),
|
"mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0),
|
||||||
};
|
};
|
||||||
|
let total = 0;
|
||||||
for (let format in fallback) {
|
for (let format in fallback) {
|
||||||
total += fallback[format];
|
total += fallback[format];
|
||||||
}
|
}
|
||||||
@@ -204,10 +226,8 @@ class StreamServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
static Create(options) {
|
static Create(options) {
|
||||||
if (!options["-port"])
|
// Allow Port to be omitted
|
||||||
throw new Error("Port undefined. Please use -port to define the port.");
|
const port = options["-port"] || null;
|
||||||
if (typeof options["-port"] !== "number" || options["-port"] !== Math.floor(options["-port"]) || options["-port"] < 1 || options["-port"] > 65535)
|
|
||||||
throw new Error("Invalid port. Must be natural number between 1 and 65535.");
|
|
||||||
if (!options["-channels"])
|
if (!options["-channels"])
|
||||||
throw new Error("Channels undefined. Please use -channels to define the number of channels.");
|
throw new Error("Channels undefined. Please use -channels to define the number of channels.");
|
||||||
if (typeof options["-channels"] !== "number" || options["-channels"] !== Math.floor(options["-channels"]) ||
|
if (typeof options["-channels"] !== "number" || options["-channels"] !== Math.floor(options["-channels"]) ||
|
||||||
@@ -217,7 +237,7 @@ class StreamServer {
|
|||||||
throw new Error("Sample rate undefined. Please use -samplerate to define the sample rate.");
|
throw new Error("Sample rate undefined. Please use -samplerate to define the sample rate.");
|
||||||
if (typeof options["-samplerate"] !== "number" || options["-samplerate"] !== Math.floor(options["-samplerate"]) || options["-samplerate"] < 1)
|
if (typeof options["-samplerate"] !== "number" || options["-samplerate"] !== Math.floor(options["-samplerate"]) || options["-samplerate"] < 1)
|
||||||
throw new Error("Invalid sample rate. Must be natural number greater than 0.");
|
throw new Error("Invalid sample rate. Must be natural number greater than 0.");
|
||||||
return new StreamServer(options["-port"], options["-channels"], options["-samplerate"]);
|
return new StreamServer(port, options["-channels"], options["-samplerate"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class AFallbackProvider {
|
class AFallbackProvider {
|
||||||
@@ -328,12 +348,31 @@ for (let i = 2; i < (process.argv.length - 1); i += 2) {
|
|||||||
throw new Error("Redefined argument: '" + process.argv[i] + "'. Please use '" + process.argv[i] + "' only ONCE");
|
throw new Error("Redefined argument: '" + process.argv[i] + "'. Please use '" + process.argv[i] + "' only ONCE");
|
||||||
Options[process.argv[i]] = OptionParser[process.argv[i]](process.argv[i + 1]);
|
Options[process.argv[i]] = OptionParser[process.argv[i]](process.argv[i + 1]);
|
||||||
}
|
}
|
||||||
const Server = StreamServer.Create(Options);
|
const Server = new StreamServer(null, 2, 48000);
|
||||||
Server.Run();
|
|
||||||
//# sourceMappingURL=3las.server.js.map
|
|
||||||
}
|
|
||||||
|
|
||||||
checkFFmpeg().then((ffmpegResult) => {
|
ServerInstance = Server;
|
||||||
ffmpegStaticPath = ffmpegResult;
|
|
||||||
runStream();
|
handleAudioUpgradeFn = function (request, socket, head, cb) {
|
||||||
});
|
if (Server.Server && Server.Server.handleUpgrade) {
|
||||||
|
Server.Server.handleUpgrade(request, socket, head, cb);
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
readyResolve();
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
logError('[Stream] Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get Server() {
|
||||||
|
return ServerInstance;
|
||||||
|
},
|
||||||
|
get handleAudioUpgrade() {
|
||||||
|
return handleAudioUpgradeFn;
|
||||||
|
},
|
||||||
|
waitUntilReady
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=3las.server.js.map
|
||||||
|
|||||||
@@ -1,134 +1,394 @@
|
|||||||
const { spawn, execSync } = require('child_process');
|
const { spawn, execSync } = require('child_process');
|
||||||
const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config');
|
const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config');
|
||||||
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console');
|
||||||
const checkFFmpeg = require('./checkFFmpeg');
|
const checkFFmpeg = require('./checkFFmpeg');
|
||||||
|
const audioServer = require('./3las.server');
|
||||||
let ffmpeg, ffmpegCommand, ffmpegParams;
|
|
||||||
|
const consoleLogTitle = '[Audio Stream]';
|
||||||
function checkAudioUtilities() {
|
|
||||||
if (process.platform === 'darwin') {
|
let startupSuccess;
|
||||||
try {
|
|
||||||
execSync('which rec');
|
function connectMessage(message) {
|
||||||
//console.log('[Audio Utility Check] SoX ("rec") found.');
|
if (!startupSuccess) {
|
||||||
} catch (error) {
|
logInfo(message);
|
||||||
logError('[Audio Utility Check] Error: SoX ("rec") not found. Please install SoX (e.g., using `brew install sox`).');
|
startupSuccess = true;
|
||||||
process.exit(1); // Exit the process with an error code
|
}
|
||||||
}
|
}
|
||||||
} else if (process.platform === 'linux') {
|
|
||||||
try {
|
function checkAudioUtilities() {
|
||||||
execSync('which arecord');
|
if (process.platform === 'darwin') {
|
||||||
//console.log('[Audio Utility Check] ALSA ("arecord") found.');
|
try {
|
||||||
} catch (error) {
|
execSync('which rec');
|
||||||
logError('[Audio Utility Check] Error: ALSA ("arecord") not found. Please ensure ALSA utilities are installed (e.g., using `sudo apt-get install alsa-utils` or `sudo yum install alsa-utils`).');
|
} catch (error) {
|
||||||
process.exit(1); // Exit the process with an error code
|
logError(`${consoleLogTitle} Error: SoX ("rec") not found. Please install SoX.`);
|
||||||
}
|
process.exit(1);
|
||||||
} else {
|
}
|
||||||
//console.log(`[Audio Utility Check] Platform "${process.platform}" does not require explicit checks for rec or arecord.`);
|
} else if (process.platform === 'linux') {
|
||||||
}
|
try {
|
||||||
}
|
execSync('which arecord');
|
||||||
|
} catch (error) {
|
||||||
function buildCommand() {
|
logError(`${consoleLogTitle} Error: ALSA ("arecord") not found. Please install ALSA utils.`);
|
||||||
// Common audio options for FFmpeg
|
process.exit(1);
|
||||||
const baseOptions = {
|
}
|
||||||
flags: '-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32',
|
}
|
||||||
codec: `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`,
|
}
|
||||||
output: `${serverConfig.audio.audioBoost == true && serverConfig.audio.ffmpeg == true ? '-af "volume=3.5"' : ''} -f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960`
|
|
||||||
};
|
function buildCommand(ffmpegPath) {
|
||||||
|
const inputDevice = serverConfig.audio.audioDevice || 'Stereo Mix';
|
||||||
if (process.platform === 'win32') {
|
const audioChannels = serverConfig.audio.audioChannels || 2;
|
||||||
// Windows: ffmpeg using dshow
|
const webPort = Number(serverConfig.webserver.webserverPort);
|
||||||
logInfo('[Audio Stream] Platform: Windows (win32). Using "dshow" input.');
|
|
||||||
ffmpegCommand = `"${ffmpeg.replace(/\\/g, '\\\\')}"`;
|
// Common audio options for FFmpeg
|
||||||
return `${ffmpegCommand} ${baseOptions.flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ` +
|
const baseOptions = {
|
||||||
`${baseOptions.codec} ${baseOptions.output} pipe:1 | node server/stream/3las.server.js -port ` +
|
flags: ['-fflags', '+nobuffer+flush_packets', '-flags', 'low_delay', '-rtbufsize', '6192', '-probesize', '32'],
|
||||||
`${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
|
codec: ['-acodec', 'pcm_s16le', '-ar', '48000', '-ac', `${audioChannels}`],
|
||||||
} else if (process.platform === 'darwin') {
|
output: ['-f', 's16le', '-fflags', '+nobuffer+flush_packets', '-packetsize', '384', '-flush_packets', '1', '-bufsize', '960', '-reconnect', '1', '-reconnect_streamed', '1', '-reconnect_delay_max', '10', 'pipe:1']
|
||||||
// macOS: using SoX's rec with coreaudio
|
};
|
||||||
if (!serverConfig.audio.ffmpeg) {
|
|
||||||
logInfo('[Audio Stream] Platform: macOS (darwin) using "coreaudio" with the default audio device.');
|
// Windows
|
||||||
const recCommand = `rec -t coreaudio -b 32 -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -b 16 -r 48000 -c ${serverConfig.audio.audioChannels} -`;
|
if (process.platform === 'win32') {
|
||||||
return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` +
|
logInfo(`${consoleLogTitle} Platform: Windows (win32). Using "dshow" input.`);
|
||||||
` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
|
return {
|
||||||
} else {
|
command: ffmpegPath,
|
||||||
ffmpegCommand = ffmpeg;
|
args: [
|
||||||
ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`;
|
...baseOptions.flags,
|
||||||
ffmpegParams += ` ${baseOptions.output} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 10 pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
|
'-f', 'dshow',
|
||||||
return `${ffmpegCommand} ${ffmpegParams}`;
|
'-audio_buffer_size', '200',
|
||||||
}
|
'-i', `audio=${inputDevice}`,
|
||||||
} else {
|
...baseOptions.codec,
|
||||||
// Linux: use alsa with arecord
|
...baseOptions.output
|
||||||
// If softwareMode is enabled, prefix the device with 'plug'
|
]
|
||||||
if (!serverConfig.audio.ffmpeg) {
|
};
|
||||||
const audioDevicePrefix = (serverConfig.audio.softwareMode && serverConfig.audio.softwareMode === true) ? 'plug' : '';
|
} else if (process.platform === 'darwin') {
|
||||||
logInfo('[Audio Stream] Platform: Linux. Using "alsa" input.');
|
// macOS
|
||||||
const recCommand = `while true; do arecord -D "${audioDevicePrefix}${serverConfig.audio.audioDevice}" -f S16_LE -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -; done`;
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` +
|
logInfo(`${consoleLogTitle} Platform: macOS (darwin) using "coreaudio" with the default audio device.`);
|
||||||
` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
|
return {
|
||||||
} else {
|
// command not used if recArgs are used
|
||||||
ffmpegCommand = ffmpeg;
|
command: `rec -t coreaudio -b 32 -r 48000 -c ${audioChannels} -t raw -b 16 -r 48000 -c ${audioChannels}`,
|
||||||
ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`;
|
args: [],
|
||||||
ffmpegParams += ` ${baseOptions.output} -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 10 pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`;
|
recArgs: [
|
||||||
return `${ffmpegCommand} ${ffmpegParams}`;
|
'-t', 'coreaudio',
|
||||||
}
|
'-b', '32',
|
||||||
}
|
'-r', '48000',
|
||||||
}
|
'-c', `${audioChannels}`,
|
||||||
|
'-t', 'raw',
|
||||||
function enableAudioStream() {
|
'-b', '16',
|
||||||
// Ensure the webserver port is a number.
|
'-r', '48000',
|
||||||
serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort);
|
'-c', `${audioChannels}`
|
||||||
let startupSuccess = false;
|
]
|
||||||
const command = buildCommand();
|
};
|
||||||
|
} else {
|
||||||
// Only log audio device details if the platform is not macOS.
|
const device = serverConfig.audio.audioDevice;
|
||||||
if (process.platform !== 'darwin') {
|
return {
|
||||||
logInfo(`Trying to start audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
command: ffmpegPath,
|
||||||
}
|
args: [
|
||||||
else {
|
...baseOptions.flags,
|
||||||
// For macOS, log the default audio device.
|
'-f', 'avfoundation',
|
||||||
logInfo(`Trying to start audio stream on default input device.`);
|
'-i', `${device || ':0'}`,
|
||||||
}
|
...baseOptions.codec,
|
||||||
|
...baseOptions.output
|
||||||
logInfo(`Using internal audio network port: ${serverConfig.webserver.webserverPort + 10}`);
|
]
|
||||||
logInfo('Using', ffmpeg === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static');
|
};
|
||||||
logDebug(`[Audio Stream] Full command:\n${command}`);
|
}
|
||||||
|
} else {
|
||||||
// Start the stream only if a valid audio device is configured.
|
// Linux
|
||||||
if (serverConfig.audio.audioDevice && serverConfig.audio.audioDevice.length > 2) {
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
const childProcess = spawn(command, { shell: true });
|
const prefix = serverConfig.audio.softwareMode ? 'plug' : '';
|
||||||
|
const device = `${prefix}${serverConfig.audio.audioDevice}`;
|
||||||
childProcess.stdout.on('data', (data) => {
|
logInfo(`${consoleLogTitle} Platform: Linux. Using "alsa" input.`);
|
||||||
logFfmpeg(`[stream:stdout] ${data}`);
|
return {
|
||||||
});
|
// command not used if arecordArgs are used
|
||||||
|
command: `while true; do arecord -D "${device}" -f S16_LE -r 48000 -c ${audioChannels} -t raw; done`,
|
||||||
childProcess.stderr.on('data', (data) => {
|
args: [],
|
||||||
logFfmpeg(`[stream:stderr] ${data}`);
|
arecordArgs: [
|
||||||
|
'-D', device,
|
||||||
if (data.includes('I/O error')) {
|
'-f', 'S16_LE',
|
||||||
logError(`[Audio Stream] Audio device "${serverConfig.audio.audioDevice}" failed to start.`);
|
'-r', '48000',
|
||||||
logError('Please start the server with: node . --ffmpegdebug for more info.');
|
'-c', audioChannels,
|
||||||
}
|
'-t', 'raw'
|
||||||
if (data.includes('size=') && !startupSuccess) {
|
],
|
||||||
logInfo('[Audio Stream] Audio stream started up successfully.');
|
ffmpegArgs: []
|
||||||
startupSuccess = true;
|
};
|
||||||
}
|
} else {
|
||||||
});
|
const device = serverConfig.audio.audioDevice;
|
||||||
|
return {
|
||||||
childProcess.on('close', (code) => {
|
command: ffmpegPath,
|
||||||
logFfmpeg(`[Audio Stream] Child process exited with code: ${code}`);
|
args: [
|
||||||
});
|
...baseOptions.flags,
|
||||||
|
'-f', 'alsa',
|
||||||
childProcess.on('error', (err) => {
|
'-i', `${device}`,
|
||||||
logFfmpeg(`[Audio Stream] Error starting child process: ${err}`);
|
...baseOptions.codec,
|
||||||
});
|
...baseOptions.output
|
||||||
} else {
|
],
|
||||||
logWarn('[Audio Stream] No valid audio device configured. Skipping audio stream initialization.');
|
arecordArgs: [],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if(configExists()) {
|
}
|
||||||
checkFFmpeg().then((ffmpegResult) => {
|
|
||||||
ffmpeg = ffmpegResult;
|
checkFFmpeg().then((ffmpegPath) => {
|
||||||
if (!serverConfig.audio.ffmpeg) checkAudioUtilities();
|
if (!serverConfig.audio.ffmpeg) checkAudioUtilities();
|
||||||
enableAudioStream();
|
let audioErrorLogged = false;
|
||||||
});
|
|
||||||
}
|
logInfo(`${consoleLogTitle} Using`, ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static');
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
||||||
|
} else {
|
||||||
|
logInfo(`${consoleLogTitle} Starting audio stream on default input device.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows (FFmpeg DirectShow Capture)
|
||||||
|
let ffmpeg;
|
||||||
|
let restartTimer = null;
|
||||||
|
let lastTimestamp = null;
|
||||||
|
let lastCheckTime = Date.now();
|
||||||
|
let audioErrorLogged = false;
|
||||||
|
let staleCount = 0;
|
||||||
|
|
||||||
|
function launchFFmpeg() {
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
let ffmpegArgs = commandDef.args;
|
||||||
|
|
||||||
|
// Apply audio boost if enabled
|
||||||
|
if (serverConfig.audio.audioBoost) {
|
||||||
|
ffmpegArgs.splice(ffmpegArgs.indexOf('pipe:1'), 0, '-af', 'volume=3.5');
|
||||||
|
}
|
||||||
|
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${ffmpegArgs.join(' ')}`);
|
||||||
|
ffmpeg = spawn(ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) \u2192 FFmpeg (process) \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
const msg = data.toString();
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${msg}`);
|
||||||
|
|
||||||
|
if (msg.includes('I/O error') && !audioErrorLogged) {
|
||||||
|
audioErrorLogged = true;
|
||||||
|
logError(`${consoleLogTitle} Audio device "${serverConfig.audio.audioDevice}" failed to start.`);
|
||||||
|
logError('Please start the server with: node . --ffmpegdebug for more info.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect frozen timestamp
|
||||||
|
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) {
|
||||||
|
const now = Date.now();
|
||||||
|
staleCount++;
|
||||||
|
if (staleCount >= 10 && now - lastCheckTime > 10000 && !restartTimer) {
|
||||||
|
restartTimer = setTimeout(() => {
|
||||||
|
restartTimer = null;
|
||||||
|
staleCount = 0;
|
||||||
|
try {
|
||||||
|
ffmpeg.kill('SIGKILL');
|
||||||
|
} catch (e) {
|
||||||
|
logWarn(`${consoleLogTitle} Failed to kill FFmpeg process: ${e.message}`);
|
||||||
|
}
|
||||||
|
launchFFmpeg(); // Restart FFmpeg
|
||||||
|
}, 0);
|
||||||
|
setTimeout(() => logWarn(`${consoleLogTitle} FFmpeg appears frozen. Restarting...`), 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastTimestamp = totalSec;
|
||||||
|
lastCheckTime = Date.now();
|
||||||
|
staleCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ffmpeg.on('exit', (code, signal) => {
|
||||||
|
if (signal) {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with signal ${signal}`);
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg was killed with signal ${signal}`);
|
||||||
|
} else {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on device fail
|
||||||
|
if (audioErrorLogged) {
|
||||||
|
logWarn(`${consoleLogTitle} Retrying in 10 seconds...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
audioErrorLogged = false;
|
||||||
|
launchFFmpeg();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
launchFFmpeg(); // Initial launch
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
// macOS (rec --> 3las.server.js --> FFmpeg)
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
|
||||||
|
// Apply audio boost if enabled and FFmpeg is used
|
||||||
|
if (serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg) {
|
||||||
|
commandDef.args.splice(commandDef.recArgs.indexOf('pipe:1'), 0, '-af', 'volume=3.5');
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRec() {
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
// Spawn rec
|
||||||
|
logDebug(`${consoleLogTitle} Launching rec with args: ${commandDef.recArgs.join(' ')}`);
|
||||||
|
|
||||||
|
//const rec = spawn(commandDef.command, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
const rec = spawn('rec', commandDef.recArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = rec.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected rec \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
rec.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
rec.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
rec.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[rec stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
rec.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[rec exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
setTimeout(startRec, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startRec();
|
||||||
|
|
||||||
|
if (serverConfig.audio.ffmpeg) {
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${commandDef.args.join(' ')}`);
|
||||||
|
const ffmpeg = spawn(ffmpegPath, commandDef.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
// Pipe FFmpeg output to 3las.server.js
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg stderr handling
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg exit handling
|
||||||
|
ffmpeg.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux (arecord --> 3las.server.js --> FFmpeg)
|
||||||
|
const commandDef = buildCommand(ffmpegPath);
|
||||||
|
|
||||||
|
// Apply audio boost if enabled and FFmpeg is used
|
||||||
|
if (serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg) {
|
||||||
|
commandDef.args.splice(commandDef.args.indexOf('pipe:1'), 0, '-af', 'volume=3.5');
|
||||||
|
}
|
||||||
|
|
||||||
|
function startArecord() {
|
||||||
|
if (!serverConfig.audio.ffmpeg) {
|
||||||
|
// Spawn the arecord loop
|
||||||
|
logDebug(`${consoleLogTitle} Launching arecord with args: ${commandDef.arecordArgs.join(' ')}`);
|
||||||
|
|
||||||
|
//const arecord = spawn(commandDef.command, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
const arecord = spawn('arecord', commandDef.arecordArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = arecord.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected arecord \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
arecord.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
arecord.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
arecord.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[arecord stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
arecord.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[arecord exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
setTimeout(startArecord, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startArecord();
|
||||||
|
|
||||||
|
if (serverConfig.audio.ffmpeg) {
|
||||||
|
logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${commandDef.args.join(' ')}`);
|
||||||
|
const ffmpeg = spawn(ffmpegPath, commandDef.args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
// Pipe FFmpeg output to 3las.server.js
|
||||||
|
audioServer.waitUntilReady.then(() => {
|
||||||
|
audioServer.Server.StdIn = ffmpeg.stdout;
|
||||||
|
audioServer.Server.Run();
|
||||||
|
connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('exit', () => {
|
||||||
|
ffmpeg.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg stderr handling
|
||||||
|
ffmpeg.stderr.on('data', (data) => {
|
||||||
|
logFfmpeg(`[FFmpeg stderr]: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// FFmpeg exit handling
|
||||||
|
ffmpeg.on('exit', (code) => {
|
||||||
|
logFfmpeg(`[FFmpeg exited] with code ${code}`);
|
||||||
|
if (code !== 0) {
|
||||||
|
logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
logError(`${consoleLogTitle} Error: ${err.message}`);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user