From 3f79f7a0c73eb0a574b63dc57fa12448e70b702e Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 03:57:31 +1000 Subject: [PATCH 01/20] add auto bw, ceq, ims, stereo options to setup --- server/endpoints.js | 1 + server/index.js | 43 ++++++++++++++++++++++++++++++++++++++--- server/server_config.js | 10 +++++++++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/server/endpoints.js b/server/endpoints.js index 712fc62..1736723 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -146,6 +146,7 @@ router.get('/wizard', (req, res) => { enabledPlugins: updatedConfig.plugins, onlineUsers: dataHandler.dataToSend.users, connectedUsers: storage.connectedUsers, + device: serverConfig.device, banlist: updatedConfig.webserver.banlist // Updated banlist from the latest config }); }); diff --git a/server/index.js b/server/index.js index a1db2c0..66d2efc 100644 --- a/server/index.js +++ b/server/index.js @@ -188,7 +188,20 @@ if (serverConfig.xdrd.wirelessConnection === false) { serialport.write('F-1\n'); serialport.write('W0\n'); serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n'); - serialport.write('G00\n'); + // cEQ and iMS combinations + if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") { + serialport.write("G00\n"); // Both Disabled + } else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") { + serialport.write(`G10\n`); + } else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") { + serialport.write(`G01\n`); + } else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") { + serialport.write("G11\n"); // Both Enabled + } + // Handle stereo mode + if (serverConfig.stereoStartup === "1") { + serialport.write("B1\n"); // Mono + } serverConfig.audio.startupVolume ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') : serialport.write('Y100\n'); @@ -485,8 +498,32 @@ wss.on('connection', (ws, request) => { if (currentUsers === 0) { storage.connectedUsers = []; - output.write('W0\n'); - output.write('B0\n'); + + if (serverConfig.bwAutoNoUsers === "1") { + output.write("W0\n"); // Auto BW 'Enabled' + } + + // cEQ and iMS combinations + if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") { + output.write("G00\n"); // Both Disabled + } else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") { + output.write(`G0${dataHandler.dataToSend.ims}\n`); + } else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") { + output.write(`G${dataHandler.dataToSend.eq}0\n`); + } else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") { + output.write(`G1${dataHandler.dataToSend.ims}\n`); + } else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") { + output.write(`G${dataHandler.dataToSend.eq}1\n`); + } else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") { + output.write("G11\n"); // Both Enabled + } + + // Handle stereo mode + if (serverConfig.stereoNoUsers === "1") { + output.write("B0\n"); + } else if (serverConfig.stereoNoUsers === "2") { + output.write("B1\n"); + } } if (tunerLockTracker.has(ws)) { diff --git a/server/server_config.js b/server/server_config.js index ff5ad2e..cc36c81 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -105,7 +105,15 @@ let serverConfig = { autoShutdown: false, enableDefaultFreq: false, defaultFreq: "87.5", - bwSwitch: false + bwSwitch: false, + bwAutoStartup: "0", + bwAutoNoUsers: "0", + ceqStartup: "0", + ceqNoUsers: "0", + imsStartup: "0", + imsNoUsers: "0", + stereoStartup: "0", + stereoNoUsers: "0" }; // Function to add missing fields without overwriting existing values From 2177c7e46c15c14d9cb1fa2617493d3316246e5d Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 03:59:56 +1000 Subject: [PATCH 02/20] add auto bw, ceq, ims, stereo options to setup --- web/setup.ejs | 106 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/web/setup.ejs b/web/setup.ejs index 59ae2f0..5834a61 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -217,18 +217,18 @@

Experimental

-

If you use an USB audio card on Linux, enabling this option might fix your audio issues.

+

If you use a USB audio card on Linux, enabling this option might fix your audio issues.

<%- include('_components', {component: 'checkbox', cssClass: '', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %>
-
+

FFmpeg

Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.

<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
-
-

Sample rate Offset

+
+

Sample rate offset

Using a negative value could eliminate audio buffering issues during long periods of listening.
However, a value that’s too low might increase the buffer over time.

@@ -325,7 +325,7 @@

You can switch between American (RBDS) / Global (RDS) mode here.

<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>
-
+

Transmitter Search Algorithm

Different modes may help with more accurate transmitter identification depending on your region.

<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1', @@ -426,6 +426,102 @@
+
+
+

On startup

+

Settings take effect on server launch

+
+ <% if (device === 'tef') { %> + <%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
+ + <%- include('_components', { component: 'dropdown', id: 'imsStartup-dropdown', inputId: 'imsStartup', label: 'iMS', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
+ <% } else if (device === 'xdr') { %> + <%- include('_components', { component: 'dropdown', id: 'rfStartup-dropdown', inputId: 'ceqStartup', label: 'RF+', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
+ + <%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
+ <% } %> + <%- include('_components', { component: 'dropdown', id: 'stereoStartup-dropdown', inputId: 'stereoStartup', label: 'Stereo Mode', cssClass: '', placeholder: 'Stereo (Default)', + options: [ + { value: '0', label: 'Stereo (Default)' }, + { value: '1', label: 'Mono' }, + ] + }) %>
+
+
+
+
+
+

On no active users

+

Settings take effect immediately after saving

+
+ <%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
+ <% if (device === 'tef') { %> + <%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ+', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
+ + <%- include('_components', { component: 'dropdown', id: 'imsNoUsers-dropdown', inputId: 'imsNoUsers', label: 'iMS', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
+ <% } else if (device === 'xdr') { %> + <%- include('_components', { component: 'dropdown', id: 'rfNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'RF+', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
+ <%- include('_components', { component: 'dropdown', id: 'ifNoUsers-dropdown', inputId: 'imsNoUsers', label: 'IF+', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
+ <% } %> + <%- include('_components', { component: 'dropdown', id: 'stereoNoUsers-dropdown', inputId: 'stereoNoUsers', label: 'Stereo Mode', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Stereo' }, + { value: '2', label: 'Mono' }, + ] + }) %>
+
+
+
From 95280d88fa84ffbaaa11295763bdb65705dec62e Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 04:16:51 +1000 Subject: [PATCH 03/20] fix typo for add auto bw, ceq, ims, stereo --- web/setup.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/setup.ejs b/web/setup.ejs index 5834a61..3f05524 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -481,7 +481,7 @@ ] }) %>
<% if (device === 'tef') { %> - <%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ+', cssClass: '', placeholder: 'Unchanged', + <%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ', cssClass: '', placeholder: 'Unchanged', options: [ { value: '0', label: 'Unchanged' }, { value: '1', label: 'Disabled' }, From 8700b663ca72d81fa7cde6d8e9d1fbe603c75922 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 04:52:06 +1000 Subject: [PATCH 04/20] add antenna to setup options --- server/index.js | 13 ++++++++++++- server/server_config.js | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/server/index.js b/server/index.js index 66d2efc..27fc145 100644 --- a/server/index.js +++ b/server/index.js @@ -171,7 +171,7 @@ if (serverConfig.xdrd.wirelessConnection === false) { setTimeout(() => { serialport.write('Q0\n'); serialport.write('M0\n'); - serialport.write('Z0\n'); + serialport.write(`Z${serverConfig.antennaStartup}\n`); // Antenna on startup if (serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true) { serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n'); @@ -524,6 +524,17 @@ wss.on('connection', (ws, request) => { } else if (serverConfig.stereoNoUsers === "2") { output.write("B1\n"); } + + // Handle Antenna selection + if (serverConfig.antennaNoUnsers === "1") { + output.write("Z0\n"); + } else if (serverConfig.antennaNoUnsers === "2") { + output.write("Z1\n"); + } else if (serverConfig.antennaNoUnsers === "3") { + output.write("Z2\n"); + } else if (serverConfig.antennaNoUnsers === "4") { + output.write("Z3\n"); + } } if (tunerLockTracker.has(ws)) { diff --git a/server/server_config.js b/server/server_config.js index cc36c81..5f754dc 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -113,7 +113,9 @@ let serverConfig = { imsStartup: "0", imsNoUsers: "0", stereoStartup: "0", - stereoNoUsers: "0" + stereoNoUsers: "0", + antennaStartup: "0", + antennaNoUnsers: "0" }; // Function to add missing fields without overwriting existing values From 6e4138b283b2c42ee8706e9cfdd2e5ecae7201c3 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 04:52:20 +1000 Subject: [PATCH 05/20] add antenna to setup options --- web/setup.ejs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/web/setup.ejs b/web/setup.ejs index 3f05524..8610070 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -466,11 +466,19 @@ { value: '1', label: 'Mono' }, ] }) %>
+ <%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)', + options: [ + { value: '0', label: 'Antenna 0 (Default)' }, + { value: '1', label: 'Antenna 1' }, + { value: '2', label: 'Antenna 2' }, + { value: '3', label: 'Antenna 3' }, + ] + }) %>
-
+

On no active users

Settings take effect immediately after saving

@@ -519,6 +527,15 @@ { value: '2', label: 'Mono' }, ] }) %>
+ <%- include('_components', { component: 'dropdown', id: 'antennaNoUnsers-dropdown', inputId: 'antennaNoUnsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Antenna 0' }, + { value: '2', label: 'Antenna 1' }, + { value: '3', label: 'Antenna 2' }, + { value: '4', label: 'Antenna 3' }, + ] + }) %>
From 61638175faa51de161cf3a871e2fcf372f15da00 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sat, 3 May 2025 04:59:28 +1000 Subject: [PATCH 06/20] reposition antenna setup option --- web/setup.ejs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/setup.ejs b/web/setup.ejs index 8610070..bfc20e0 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -427,7 +427,7 @@
-
+

On startup

Settings take effect on server launch

@@ -452,7 +452,6 @@ { value: '1', label: 'Enabled' }, ] }) %>
- <%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled', options: [ { value: '0', label: 'Disabled' }, @@ -466,6 +465,8 @@ { value: '1', label: 'Mono' }, ] }) %>
+
+
<%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)', options: [ { value: '0', label: 'Antenna 0 (Default)' }, @@ -527,6 +528,8 @@ { value: '2', label: 'Mono' }, ] }) %>
+
+
<%- include('_components', { component: 'dropdown', id: 'antennaNoUnsers-dropdown', inputId: 'antennaNoUnsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', options: [ { value: '0', label: 'Unchanged' }, From 4ece21d5137c5d5c051480a99c8a9c4b1ac41f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Farka=C5=A1?= Date: Sun, 1 Jun 2025 17:48:00 +0200 Subject: [PATCH 07/20] hotfix test for freq handling --- web/js/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/api.js b/web/js/api.js index ac64042..5e64c1a 100644 --- a/web/js/api.js +++ b/web/js/api.js @@ -41,7 +41,7 @@ function tuneDown() { function tuneTo(freq) { previousFreq = getCurrentFreq(); - socket.send("T" + ((parseFloat(freq)) * 1000).toFixed(3)); + socket.send("T" + ((parseFloat(freq)) * 1000).toFixed(0)); } function resetRDS() { From 2ade42d1fb6fc13dc90b47bf0f3c9eb39b92040a Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:23:19 +1000 Subject: [PATCH 08/20] Remove audio proxy (http-proxy) --- server/index.js | 57 ++--- server/stream/3las.server.js | 91 +++++-- server/stream/index.js | 474 +++++++++++++++++++++++++---------- 3 files changed, 430 insertions(+), 192 deletions(-) diff --git a/server/index.js b/server/index.js index d9ea23e..77d49b4 100644 --- a/server/index.js +++ b/server/index.js @@ -4,7 +4,6 @@ const endpoints = require('./endpoints'); const session = require('express-session'); const bodyParser = require('body-parser'); const http = require('http'); -const httpProxy = require('http-proxy'); const readline = require('readline'); const app = express(); const httpServer = http.createServer(app); @@ -18,6 +17,7 @@ const path = require('path'); const net = require('net'); const client = new net.Socket(); const { SerialPort } = require('serialport'); +const audioServer = require('./stream/3las.server'); const tunnel = require('./tunnel'); // File imports @@ -94,13 +94,6 @@ console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m'); require('./stream/index'); 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 serialport; @@ -390,12 +383,12 @@ wss.on('connection', (ws, request) => { const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); - if (serverConfig.webserver.banlist?.includes(clientIp)) { + if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); return; } - if (clientIp.includes(',')) { + if (clientIp && clientIp.includes(',')) { clientIp = clientIp.split(',')[0].trim(); } @@ -714,15 +707,15 @@ httpServer.on('upgrade', (request, socket, head) => { }); }); } else if (request.url === '/audio') { - isPortOpen('localhost', (Number(serverConfig.webserver.webserverPort) + 10)).then((open) => { - if (open) { - proxy.ws(request, socket, head); - } else { - logWarn(`Audio stream port ${(Number(serverConfig.webserver.webserverPort) + 10)} not yet open — skipping proxy connection.`); - socket.end(); // close socket so client isn't left hanging - } - }); -} else if (request.url === '/chat') { + if (typeof audioServer?.handleAudioUpgrade === 'function') { + audioServer.handleAudioUpgrade(request, socket, head, (ws) => { + audioServer.Server?.Server?.emit?.('connection', ws, request); + }); + } else { + logWarn('[Audio WebSocket] Audio server not ready — dropping client connection.'); + socket.destroy(); + } + } else if (request.url === '/chat') { sessionMiddleware(request, {}, () => { chatWss.handleUpgrade(request, socket, head, (ws) => { chatWss.emit('connection', ws, request); @@ -733,21 +726,21 @@ httpServer.on('upgrade', (request, socket, head) => { rdsWss.handleUpgrade(request, socket, head, (ws) => { rdsWss.emit('connection', ws, request); - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - const userCommandHistory = {}; - if (serverConfig.webserver.banlist?.includes(clientIp)) { - ws.close(1008, 'Banned IP'); - return; - } + const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + const userCommandHistory = {}; + if (serverConfig.webserver.banlist?.includes(clientIp)) { + ws.close(1008, 'Banned IP'); + return; + } - // Anti-spam tracking for each client - const userCommands = {}; - let lastWarn = { time: 0 }; + // Anti-spam tracking for each client + const userCommands = {}; + let lastWarn = { time: 0 }; - ws.on('message', function incoming(message) { - // Anti-spam - const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds'); - }); + ws.on('message', function incoming(message) { + // Anti-spam + const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '5', 'rds'); + }); }); }); diff --git a/server/stream/3las.server.js b/server/stream/3las.server.js index b2efd05..9893203 100644 --- a/server/stream/3las.server.js +++ b/server/stream/3las.server.js @@ -1,15 +1,27 @@ "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) 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) { if (k2 === undefined) k2 = 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 child_process_1 = require("child_process"); 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; class StreamClient { constructor(server, socket) { @@ -117,7 +129,7 @@ class StreamClient { } class StreamServer { constructor(port, channels, sampleRate) { - this.Port = port; + this.Port = port || null; this.Channels = channels; this.SampleRate = sampleRate; this.Clients = new Set(); @@ -139,12 +151,21 @@ class StreamServer { } Run() { this.Server = new ws.Server({ - "host": ["127.0.0.1", "::1"], - "port": this.Port, - "clientTracking": true, - "perMessageDeflate": false + noServer: true, + clientTracking: true, + 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)); + 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.resume(); } @@ -195,6 +216,7 @@ class StreamServer { "wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0), "mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0), }; + let total = 0; for (let format in fallback) { total += fallback[format]; } @@ -204,10 +226,8 @@ class StreamServer { }; } static Create(options) { - if (!options["-port"]) - throw new Error("Port undefined. Please use -port to define the port."); - 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."); + // Allow Port to be omitted + const port = options["-port"] || null; if (!options["-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"]) || @@ -217,7 +237,7 @@ class StreamServer { 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) 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 { @@ -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"); Options[process.argv[i]] = OptionParser[process.argv[i]](process.argv[i + 1]); } -const Server = StreamServer.Create(Options); -Server.Run(); -//# sourceMappingURL=3las.server.js.map -} + const Server = new StreamServer(null, 2, 48000); -checkFFmpeg().then((ffmpegResult) => { - ffmpegStaticPath = ffmpegResult; - runStream(); -}); \ No newline at end of file + ServerInstance = Server; + + 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 diff --git a/server/stream/index.js b/server/stream/index.js index bd64cce..353ec52 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -1,134 +1,340 @@ -const { spawn, execSync } = require('child_process'); -const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config'); -const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); -const checkFFmpeg = require('./checkFFmpeg'); - -let ffmpeg, ffmpegCommand, ffmpegParams; - -function checkAudioUtilities() { - if (process.platform === 'darwin') { - try { - execSync('which rec'); - //console.log('[Audio Utility Check] SoX ("rec") found.'); - } catch (error) { - logError('[Audio Utility Check] Error: SoX ("rec") not found. Please install SoX (e.g., using `brew install sox`).'); - process.exit(1); // Exit the process with an error code - } - } else if (process.platform === 'linux') { - try { - execSync('which arecord'); - //console.log('[Audio Utility Check] ALSA ("arecord") found.'); - } catch (error) { - 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`).'); - process.exit(1); // Exit the process with an error code - } - } else { - //console.log(`[Audio Utility Check] Platform "${process.platform}" does not require explicit checks for rec or arecord.`); - } -} - -function buildCommand() { - // Common audio options for FFmpeg - 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` - }; - - if (process.platform === 'win32') { - // Windows: ffmpeg using dshow - logInfo('[Audio Stream] Platform: Windows (win32). Using "dshow" input.'); - ffmpegCommand = `"${ffmpeg.replace(/\\/g, '\\\\')}"`; - return `${ffmpegCommand} ${baseOptions.flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ` + - `${baseOptions.codec} ${baseOptions.output} pipe:1 | node server/stream/3las.server.js -port ` + - `${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; - } else if (process.platform === 'darwin') { - // macOS: using SoX's rec with coreaudio - if (!serverConfig.audio.ffmpeg) { - logInfo('[Audio Stream] Platform: macOS (darwin) using "coreaudio" with the default audio device.'); - const recCommand = `rec -t coreaudio -b 32 -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -b 16 -r 48000 -c ${serverConfig.audio.audioChannels} -`; - return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` + - ` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; - } else { - ffmpegCommand = ffmpeg; - ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`; - 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}`; - return `${ffmpegCommand} ${ffmpegParams}`; - } - } else { - // Linux: use alsa with arecord - // If softwareMode is enabled, prefix the device with 'plug' - if (!serverConfig.audio.ffmpeg) { - const audioDevicePrefix = (serverConfig.audio.softwareMode && serverConfig.audio.softwareMode === true) ? 'plug' : ''; - logInfo('[Audio Stream] Platform: Linux. Using "alsa" input.'); - const recCommand = `while true; do arecord -D "${audioDevicePrefix}${serverConfig.audio.audioDevice}" -f S16_LE -r 48000 -c ${serverConfig.audio.audioChannels} -t raw -; done`; - return `${recCommand} | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10}` + - ` -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; - } else { - ffmpegCommand = ffmpeg; - ffmpegParams = `${baseOptions.flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${baseOptions.codec}`; - 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}`; - return `${ffmpegCommand} ${ffmpegParams}`; - } - } -} - -function enableAudioStream() { - // Ensure the webserver port is a number. - serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); - let startupSuccess = false; - const command = buildCommand(); - - // Only log audio device details if the platform is not macOS. - if (process.platform !== 'darwin') { - logInfo(`Trying to start audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`); - } - else { - // For macOS, log the default audio device. - logInfo(`Trying to start audio stream on default input device.`); - } - - 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}`); - - // Start the stream only if a valid audio device is configured. - if (serverConfig.audio.audioDevice && serverConfig.audio.audioDevice.length > 2) { - const childProcess = spawn(command, { shell: true }); - - childProcess.stdout.on('data', (data) => { - logFfmpeg(`[stream:stdout] ${data}`); - }); - - childProcess.stderr.on('data', (data) => { - logFfmpeg(`[stream:stderr] ${data}`); - - if (data.includes('I/O error')) { - logError(`[Audio Stream] Audio device "${serverConfig.audio.audioDevice}" failed to start.`); - logError('Please start the server with: node . --ffmpegdebug for more info.'); - } - if (data.includes('size=') && !startupSuccess) { - logInfo('[Audio Stream] Audio stream started up successfully.'); - startupSuccess = true; - } - }); - - childProcess.on('close', (code) => { - logFfmpeg(`[Audio Stream] Child process exited with code: ${code}`); - }); - - childProcess.on('error', (err) => { - logFfmpeg(`[Audio Stream] Error starting child process: ${err}`); - }); - } else { - logWarn('[Audio Stream] No valid audio device configured. Skipping audio stream initialization.'); - } -} - -if(configExists()) { - checkFFmpeg().then((ffmpegResult) => { - ffmpeg = ffmpegResult; - if (!serverConfig.audio.ffmpeg) checkAudioUtilities(); - enableAudioStream(); - }); -} \ No newline at end of file +const { spawn, execSync } = require('child_process'); +const { configName, serverConfig, configUpdate, configSave, configExists } = require('../server_config'); +const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); +const checkFFmpeg = require('./checkFFmpeg'); +const audioServer = require('./3las.server'); + +const consoleLogTitle = '[Audio Stream]'; + +let startupSuccess; + +function connectMessage(message) { + if (!startupSuccess) { + logInfo(message); + startupSuccess = true; + } +} + +function checkAudioUtilities() { + if (process.platform === 'darwin') { + try { + execSync('which rec'); + } catch (error) { + logError(`${consoleLogTitle} Error: SoX ("rec") not found. Please install SoX.`); + process.exit(1); + } + } else if (process.platform === 'linux') { + try { + execSync('which arecord'); + } catch (error) { + logError(`${consoleLogTitle} Error: ALSA ("arecord") not found. Please install ALSA utils.`); + process.exit(1); + } + } +} + +function buildCommand(ffmpegPath) { + const inputDevice = serverConfig.audio.audioDevice || 'Stereo Mix'; + const audioChannels = serverConfig.audio.audioChannels || 2; + const webPort = Number(serverConfig.webserver.webserverPort); + + // Common audio options for FFmpeg + const baseOptions = { + flags: ['-fflags', '+nobuffer+flush_packets', '-flags', 'low_delay', '-rtbufsize', '6192', '-probesize', '32'], + codec: ['-acodec', 'pcm_s16le', '-ar', '48000', '-ac', `${audioChannels}`], + 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'] + }; + + // Windows + if (process.platform === 'win32') { + logInfo(`${consoleLogTitle} Platform: Windows (win32). Using "dshow" input.`); + return { + command: ffmpegPath, + args: [ + ...baseOptions.flags, + '-f', 'dshow', + '-audio_buffer_size', '200', + '-i', `audio=${inputDevice}`, + ...baseOptions.codec, + ...baseOptions.output + ] + }; + } else if (process.platform === 'darwin') { + // macOS + if (!serverConfig.audio.ffmpeg) { + logInfo(`${consoleLogTitle} Platform: macOS (darwin) using "coreaudio" with the default audio device.`); + return { + // command not used if recArgs are used + command: `rec -t coreaudio -b 32 -r 48000 -c ${audioChannels} -t raw -b 16 -r 48000 -c ${audioChannels}`, + args: [], + recArgs: [ + '-t', 'coreaudio', + '-b', '32', + '-r', '48000', + '-c', `${audioChannels}`, + '-t', 'raw', + '-b', '16', + '-r', '48000', + '-c', `${audioChannels}` + ] + }; + } else { + const device = serverConfig.audio.audioDevice; + return { + command: ffmpegPath, + args: [ + ...baseOptions.flags, + '-f', 'avfoundation', + '-i', `${device || ':0'}`, + ...baseOptions.codec, + ...baseOptions.output + ] + }; + } + } else { + // Linux + if (!serverConfig.audio.ffmpeg) { + const prefix = serverConfig.audio.softwareMode ? 'plug' : ''; + const device = `${prefix}${serverConfig.audio.audioDevice}`; + logInfo(`${consoleLogTitle} Platform: Linux. Using "alsa" input.`); + 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`, + args: [], + arecordArgs: [ + '-D', device, + '-f', 'S16_LE', + '-r', '48000', + '-c', audioChannels, + '-t', 'raw' + ], + ffmpegArgs: [] + }; + } else { + const device = serverConfig.audio.audioDevice; + return { + command: ffmpegPath, + args: [ + ...baseOptions.flags, + '-f', 'alsa', + '-i', `${device}`, + ...baseOptions.codec, + ...baseOptions.output + ], + arecordArgs: [], + }; + } + } +} + +checkFFmpeg().then((ffmpegPath) => { + if (!serverConfig.audio.ffmpeg) checkAudioUtilities(); + 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) + 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(' ')}`); + const ffmpeg = spawn(ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); + + audioServer.waitUntilReady.then(() => { + audioServer.Server.StdIn = ffmpeg.stdout; + audioServer.Server.Run(); + connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) → FFmpeg (process) → Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); + }); + + ffmpeg.stderr.on('data', (data) => { + logFfmpeg(`[FFmpeg stderr]: ${data}`); + + if (data.includes('I/O error') && !audioErrorLogged) { + audioErrorLogged = true; + logError(`${consoleLogTitle} Audio device "${serverConfig.audio.audioDevice}" failed to start.`); + } + }); + + ffmpeg.on('exit', (code) => { + logFfmpeg(`[FFmpeg exited] with code ${code}`); + if (code !== 0) { + logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`); + } + }); + } 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 → FFmpeg → 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 → 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 → FFmpeg → 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 → 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}`); +}); From f29570261e0548ce4d45f7c8c0699445495fecae Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:32:04 +1000 Subject: [PATCH 09/20] fix minor typos, potential mem leaks --- server/datahandler.js | 2 +- server/endpoints.js | 12 ++++- server/index.js | 101 +--------------------------------------- server/server_config.js | 2 +- server/tx_search.js | 2 +- web/setup.ejs | 1 + 6 files changed, 17 insertions(+), 103 deletions(-) diff --git a/server/datahandler.js b/server/datahandler.js index d0e2291..2fac24f 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -272,7 +272,7 @@ function rdsReceived() { rdsTimeoutTimer = null; } if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) { - rdsTimeoutTimer = setInterval(rdsReset, serverConfig.webserver.rdsTimeout * 1000); + rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000); } } diff --git a/server/endpoints.js b/server/endpoints.js index cddcd50..87eb480 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -21,7 +21,7 @@ router.get('/', (req, res) => { let requestIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress; const normalizedIp = requestIp?.replace(/^::ffff:/, ''); - const ipList = normalizedIp.split(',').map(ip => ip.trim()); // in case there are multiple IPs (proxy), we need to check all of them + const ipList = (normalizedIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them const isBanned = ipList.some(ip => serverConfig.webserver.banlist.some(banEntry => banEntry[0] === ip)); @@ -140,12 +140,14 @@ router.get('/wizard', (req, res) => { audioDevices: result.videoDevices, serialPorts: serialPorts, memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB', + memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB', processUptime: formattedProcessUptime, consoleOutput: logs, plugins: allPluginConfigs, enabledPlugins: updatedConfig.plugins, onlineUsers: dataHandler.dataToSend.users, connectedUsers: storage.connectedUsers, + device: serverConfig.device, banlist: updatedConfig.webserver.banlist // Updated banlist from the latest config }); }); @@ -369,6 +371,14 @@ const logHistory = {}; function canLog(id) { const now = Date.now(); const sixtyMinutes = 60 * 60 * 1000; // 60 minutes in milliseconds + + // Remove expired entries + for (const [entryId, timestamp] of Object.entries(logHistory)) { + if ((now - timestamp) >= sixtyMinutes) { + delete logHistory[entryId]; + } + } + if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) { return false; // Deny logging if less than 60 minutes have passed } diff --git a/server/index.js b/server/index.js index d9ea23e..ca8e5b8 100644 --- a/server/index.js +++ b/server/index.js @@ -326,59 +326,6 @@ app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '../web')); app.use('/', endpoints); -function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) { - const command = message.toString(); - const now = Date.now(); - if (endpointName === 'text') logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); - // Initialize user command history if not present - if (!userCommandHistory[clientIp]) { - userCommandHistory[clientIp] = []; - } - - // Record the current timestamp for the user - userCommandHistory[clientIp].push(now); - - // Remove timestamps older than 20 ms from the history - userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20); - - // Check if there are 8 or more commands in the last 20 ms - if (userCommandHistory[clientIp].length >= 8) { - logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`); - - // Add to banlist if not already banned - if (!serverConfig.webserver.banlist.includes(clientIp)) { - serverConfig.webserver.banlist.push(clientIp); - logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`); - console.log(serverConfig.webserver.banlist); - configSave(); - } - - ws.close(1008, 'Bot-like behavior detected'); - return command; // Return command value before closing connection - } - // Update the last message time for general spam detection - lastMessageTime = now; - // Initialize command history for rate-limiting checks - if (!userCommands[command]) { - userCommands[command] = []; - } - // Record the current timestamp for this command - userCommands[command].push(now); - // Remove timestamps older than 1 second - userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000); - // If command count exceeds limit, close connection - if (userCommands[command].length > lengthCommands) { - if (now - lastWarn.time > 1000) { // Check if 1 second has passed - logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`); - lastWarn.time = now; // Update the last warning time - } - ws.close(1008, 'Spamming detected'); - return command; // Return command value before closing connection - } - return command; // Return command value for normal execution -} - - /** * WEBSOCKET BLOCK */ @@ -390,12 +337,12 @@ wss.on('connection', (ws, request) => { const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); - if (serverConfig.webserver.banlist?.includes(clientIp)) { + if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); return; } - if (clientIp.includes(',')) { + if (clientIp && clientIp.includes(',')) { clientIp = clientIp.split(',')[0].trim(); } @@ -641,50 +588,6 @@ pluginsWss.on('connection', (ws, request) => { }); }); -// Additional web socket for using plugins -pluginsWss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; - const userCommandHistory = {}; - if (serverConfig.webserver.banlist?.includes(clientIp)) { - ws.close(1008, 'Banned IP'); - return; - } - // Anti-spam tracking for each client - const userCommands = {}; - let lastWarn = { time: 0 }; - - ws.on('message', message => { - // Anti-spam - const command = antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '10', 'data_plugins'); - - let messageData; - - try { - messageData = JSON.parse(message); // Attempt to parse the JSON - } catch (error) { - // console.error("Failed to parse message:", error); // Log the error - return; // Exit if parsing fails - } - - const modifiedMessage = JSON.stringify(messageData); - - // Broadcast the message to all other clients - pluginsWss.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(modifiedMessage); // Send the message to all clients - } - }); - }); - - ws.on('close', () => { - // logInfo('WebSocket Extra connection closed'); // Use custom logInfo function - }); - - ws.on('error', error => { - logError('WebSocket Extra error: ' + error); // Use custom logError function - }); -}); - function isPortOpen(host, port, timeout = 1000) { return new Promise((resolve) => { const socket = new net.Socket(); diff --git a/server/server_config.js b/server/server_config.js index fcf4290..d47d7f4 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -155,7 +155,7 @@ function configUpdate(newConfig) { function configSave() { try { fs.writeFileSync(configPath, JSON.stringify(serverConfig, null, 2)); - logInfo('Server config saved successfully.'); + setTimeout(() => logInfo('Server config saved successfully.'), 0); } catch (err) { logError(err); } diff --git a/server/tx_search.js b/server/tx_search.js index 2380cce..ef48664 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -31,7 +31,7 @@ if (typeof algorithms[algoSetting] !== 'undefined') { // IIFE to build the local TX DB cache from the endpoint. (async () => { try { - consoleCmd.logInfo('Fetching transmitter database...'); + setTimeout(() => consoleCmd.logInfo('Fetching transmitter database...'), 0); const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); localDb = await response.json(); diff --git a/web/setup.ejs b/web/setup.ejs index 7d1b68e..c4e1c75 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -76,6 +76,7 @@
<%= memoryUsage %> + (<%= memoryHeap %> heap)

Memory usage

From 2ab7dd33dd334bc2dbbcff33996aa0096ca6796b Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:38:20 +1000 Subject: [PATCH 10/20] add per-ip limit --- server/index.js | 67 +++++++++++++++++++++++++++++++++++++++++++-- web/js/main.js | 13 +++++++-- web/js/websocket.js | 53 ++++++++++++++++++----------------- 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/server/index.js b/server/index.js index d9ea23e..4cfae4c 100644 --- a/server/index.js +++ b/server/index.js @@ -383,6 +383,24 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC * WEBSOCKET BLOCK */ const tunerLockTracker = new WeakMap(); +const ipConnectionCounts = new Map(); // Per-IP limit variables +const ipLogTimestamps = new Map(); +const MAX_CONNECTIONS_PER_IP = 5; +const IP_LOG_INTERVAL_MS = 60000; +// Remove old per-IP limit addresses +setInterval(() => { + const now = Date.now(); + + for (const [ip, count] of ipConnectionCounts.entries()) { + const lastSeen = ipLogTimestamps.get(ip) || 0; + const inactive = now - lastSeen > 60 * 60 * 1000; + + if (count === 0 && inactive) { + ipConnectionCounts.delete(ip); + ipLogTimestamps.delete(ip); + } + } +}, 60 * 60 * 1000); // Run every hour wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; @@ -390,15 +408,44 @@ wss.on('connection', (ws, request) => { const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); - if (serverConfig.webserver.banlist?.includes(clientIp)) { + if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); return; } - if (clientIp.includes(',')) { + if (clientIp && clientIp.includes(',')) { clientIp = clientIp.split(',')[0].trim(); } + // Per-IP limit connection open + if (clientIp) { + const isLocalIp = ( + clientIp === '127.0.0.1' || + clientIp === '::1' || + clientIp === '::ffff:127.0.0.1' || + clientIp.startsWith('192.168.') || + clientIp.startsWith('10.') || + clientIp.startsWith('172.16.') + ); + if (!isLocalIp) { + if (!ipConnectionCounts.has(clientIp)) { + ipConnectionCounts.set(clientIp, 0); + } + const currentCount = ipConnectionCounts.get(clientIp); + if (currentCount >= MAX_CONNECTIONS_PER_IP) { + ws.close(1008, 'Too many open connections from this IP'); + const lastLogTime = ipLogTimestamps.get(clientIp) || 0; + const now = Date.now(); + if (now - lastLogTime > IP_LOG_INTERVAL_MS) { + logWarn(`Web client \x1b[31mclosed: limit exceeded\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); + ipLogTimestamps.set(clientIp, now); + } + return; + } + ipConnectionCounts.set(clientIp, currentCount + 1); + } + } + if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers++; } @@ -472,6 +519,22 @@ wss.on('connection', (ws, request) => { }); ws.on('close', (code, reason) => { + // Per-IP limit connection closed + if (clientIp) { + const isLocalIp = ( + clientIp === '127.0.0.1' || + clientIp === '::1' || + clientIp === '::ffff:127.0.0.1' || + clientIp.startsWith('192.168.') || + clientIp.startsWith('10.') || + clientIp.startsWith('172.16.') + ); + if (!isLocalIp) { + const current = ipConnectionCounts.get(clientIp) || 1; + ipConnectionCounts.set(clientIp, Math.max(0, current - 1)); + } + } + if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers--; } diff --git a/web/js/main.js b/web/js/main.js index 532bce2..ffde72d 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -6,6 +6,7 @@ var parsedData, signalChart, previousFreq; var data = []; var signalData = []; let updateCounter = 0; +let lastReconnectAttempt = 0; let messageCounter = 0; // Count for WebSocket data length returning 0 let messageData = 800; // Initial value anything above 0 let messageLength = 800; // Retain value of messageData until value is updated @@ -375,10 +376,16 @@ function sendPingRequest() { messageCounter = 0; } - // Automatic reconnection on WebSocket close - if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) { + // Automatic reconnection on WebSocket close with cooldown + const now = Date.now(); + if ( + (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) && + (now - lastReconnectAttempt > TIMEOUT_DURATION) + ) { + lastReconnectAttempt = now; + socket = new WebSocket(socketAddress); - + socket.onopen = () => { sendToast('info', 'Connected', 'Reconnected successfully!', false, false); }; diff --git a/web/js/websocket.js b/web/js/websocket.js index d965d33..a120ce4 100644 --- a/web/js/websocket.js +++ b/web/js/websocket.js @@ -1,30 +1,29 @@ -var url = new URL('text', window.location.href); -url.protocol = url.protocol.replace('http', 'ws'); -var socketAddress = url.href; -var socket = new WebSocket(socketAddress); +if (!window.socket || window.socket.readyState === WebSocket.CLOSED || window.socket.readyState === WebSocket.CLOSING) { + var url = new URL('text', window.location.href); + url.protocol = url.protocol.replace('http', 'ws'); + var socketAddress = url.href; + var socket = new WebSocket(socketAddress); -const socketPromise = new Promise((resolve, reject) => { - // Event listener for when the WebSocket connection is open - socket.addEventListener('open', () => { - console.log('WebSocket connection open'); - resolve(socket); // Resolve the promise with the WebSocket instance + window.socket = socket; + + const socketPromise = new Promise((resolve, reject) => { + socket.addEventListener('open', () => { + console.log('WebSocket connection open'); + resolve(socket); + }); + + socket.addEventListener('error', (error) => { + console.error('WebSocket error', error); + reject(error); + }); + + socket.addEventListener('close', () => { + setTimeout(() => { + console.warn('WebSocket connection closed'); + }, 100); + reject(new Error('WebSocket connection closed')); + }); }); - // Event listener for WebSocket errors - socket.addEventListener('error', (error) => { - console.error('WebSocket error', error); - reject(error); // Reject the promise on error - }); - - // Event listener for WebSocket connection closure - socket.addEventListener('close', () => { - console.warn('WebSocket connection closed'); - reject(new Error('WebSocket connection closed')); // Reject with closure warning - }); -}); - -// Assign the socketPromise to window.socketPromise for global access -window.socketPromise = socketPromise; - -// Assign the socket instance to window.socket for global access -window.socket = socket; + window.socketPromise = socketPromise; +} From c3e18de6e4c307014d12eacc8e09f7b64a0a10cf Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:58:56 +1000 Subject: [PATCH 11/20] Added reconnect logic for FFmpeg on Windows --- server/stream/index.js | 114 ++++++++++++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/server/stream/index.js b/server/stream/index.js index 353ec52..bed4e72 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -141,39 +141,93 @@ checkFFmpeg().then((ffmpegPath) => { if (process.platform === 'win32') { // Windows (FFmpeg DirectShow Capture) - const commandDef = buildCommand(ffmpegPath); + let ffmpeg; + let restartTimer = null; + let lastTimestamp = null; + let lastCheckTime = Date.now(); + let audioErrorLogged = false; + let staleCount = 0; - let ffmpegArgs = commandDef.args; + 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'); + // 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) → FFmpeg (process) → 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); + } + }); } - - logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${ffmpegArgs.join(' ')}`); - const ffmpeg = spawn(ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', 'pipe'] }); - - audioServer.waitUntilReady.then(() => { - audioServer.Server.StdIn = ffmpeg.stdout; - audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) → FFmpeg (process) → Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); - }); - - ffmpeg.stderr.on('data', (data) => { - logFfmpeg(`[FFmpeg stderr]: ${data}`); - - if (data.includes('I/O error') && !audioErrorLogged) { - audioErrorLogged = true; - logError(`${consoleLogTitle} Audio device "${serverConfig.audio.audioDevice}" failed to start.`); - } - }); - - ffmpeg.on('exit', (code) => { - logFfmpeg(`[FFmpeg exited] with code ${code}`); - if (code !== 0) { - logWarn(`${consoleLogTitle} FFmpeg exited unexpectedly with code ${code}`); - } - }); + launchFFmpeg(); // Initial launch } else if (process.platform === 'darwin') { // macOS (rec --> 3las.server.js --> FFmpeg) const commandDef = buildCommand(ffmpegPath); From 8ab01ee8498466e8497c9b49ba294f0c196dbbfe Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Tue, 15 Jul 2025 19:23:11 +1000 Subject: [PATCH 12/20] fix minor typos, potential mem leaks --- web/setup.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/setup.ejs b/web/setup.ejs index c4e1c75..c1b2e80 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -76,7 +76,7 @@
<%= memoryUsage %> - (<%= memoryHeap %> heap) + (<%= memoryHeap %> heap)

Memory usage

From 3436d3171e880277be24e8158039a9474cd4f16c Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:12:38 +1000 Subject: [PATCH 13/20] add delayed antenna change option --- server/index.js | 22 +++++++++++++--------- server/server_config.js | 3 ++- web/setup.ejs | 10 ++++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/server/index.js b/server/index.js index 27fc145..636842f 100644 --- a/server/index.js +++ b/server/index.js @@ -103,6 +103,7 @@ const proxy = httpProxy.createProxyServer({ let currentUsers = 0; let serialport; +let timeoutAntenna; app.use(bodyParser.urlencoded({ extended: true })); const sessionMiddleware = session({ @@ -526,15 +527,18 @@ wss.on('connection', (ws, request) => { } // Handle Antenna selection - if (serverConfig.antennaNoUnsers === "1") { - output.write("Z0\n"); - } else if (serverConfig.antennaNoUnsers === "2") { - output.write("Z1\n"); - } else if (serverConfig.antennaNoUnsers === "3") { - output.write("Z2\n"); - } else if (serverConfig.antennaNoUnsers === "4") { - output.write("Z3\n"); - } + if (timeoutAntenna) clearTimeout(timeoutAntenna); + timeoutAntenna = setTimeout(() => { + if (serverConfig.antennaNoUsers === "1") { + output.write("Z0\n"); + } else if (serverConfig.antennaNoUsers === "2") { + output.write("Z1\n"); + } else if (serverConfig.antennaNoUsers === "3") { + output.write("Z2\n"); + } else if (serverConfig.antennaNoUsers === "4") { + output.write("Z3\n"); + } + }, serverConfig.antennaNoUsersDelay ? 15000 : 0); } if (tunerLockTracker.has(ws)) { diff --git a/server/server_config.js b/server/server_config.js index 5f754dc..8e8a344 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -115,7 +115,8 @@ let serverConfig = { stereoStartup: "0", stereoNoUsers: "0", antennaStartup: "0", - antennaNoUnsers: "0" + antennaNoUsers: "0", + antennaNoUsersDelay: false }; // Function to add missing fields without overwriting existing values diff --git a/web/setup.ejs b/web/setup.ejs index bfc20e0..ee723d3 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -227,7 +227,7 @@

Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.

<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
-
+

Sample rate offset

Using a negative value could eliminate audio buffering issues during long periods of listening.
However, a value that’s too low might increase the buffer over time.

@@ -479,7 +479,7 @@
-
+

On no active users

Settings take effect immediately after saving

@@ -530,7 +530,8 @@ }) %>
- <%- include('_components', { component: 'dropdown', id: 'antennaNoUnsers-dropdown', inputId: 'antennaNoUnsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Delayed Antenna Change', id: 'antennaNoUsersDelay'}) %>
+ <%- include('_components', { component: 'dropdown', id: 'antennaNoUsers-dropdown', inputId: 'antennaNoUsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', options: [ { value: '0', label: 'Unchanged' }, { value: '1', label: 'Antenna 0' }, @@ -538,7 +539,8 @@ { value: '3', label: 'Antenna 2' }, { value: '4', label: 'Antenna 3' }, ] - }) %>
+ }) %> +
From a3f107fe6462ded289b7a9ee32596016cf11214d Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:31:04 +1000 Subject: [PATCH 14/20] move to startup category --- web/setup.ejs | 241 ++++++++++++++++++++++++++------------------------ 1 file changed, 123 insertions(+), 118 deletions(-) diff --git a/web/setup.ejs b/web/setup.ejs index ee723d3..f156ac8 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -39,6 +39,9 @@
  • User management
  • +
  • + Startup +
  • Extras
  • @@ -426,124 +429,6 @@
    -
    -
    -

    On startup

    -

    Settings take effect on server launch

    -
    - <% if (device === 'tef') { %> - <%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled', - options: [ - { value: '0', label: 'Disabled' }, - { value: '1', label: 'Enabled' }, - ] - }) %>
    - - <%- include('_components', { component: 'dropdown', id: 'imsStartup-dropdown', inputId: 'imsStartup', label: 'iMS', cssClass: '', placeholder: 'Disabled', - options: [ - { value: '0', label: 'Disabled' }, - { value: '1', label: 'Enabled' }, - ] - }) %>
    - <% } else if (device === 'xdr') { %> - <%- include('_components', { component: 'dropdown', id: 'rfStartup-dropdown', inputId: 'ceqStartup', label: 'RF+', cssClass: '', placeholder: 'Disabled', - options: [ - { value: '0', label: 'Disabled' }, - { value: '1', label: 'Enabled' }, - ] - }) %>
    - <%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled', - options: [ - { value: '0', label: 'Disabled' }, - { value: '1', label: 'Enabled' }, - ] - }) %>
    - <% } %> - <%- include('_components', { component: 'dropdown', id: 'stereoStartup-dropdown', inputId: 'stereoStartup', label: 'Stereo Mode', cssClass: '', placeholder: 'Stereo (Default)', - options: [ - { value: '0', label: 'Stereo (Default)' }, - { value: '1', label: 'Mono' }, - ] - }) %>
    -
    -
    - <%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)', - options: [ - { value: '0', label: 'Antenna 0 (Default)' }, - { value: '1', label: 'Antenna 1' }, - { value: '2', label: 'Antenna 2' }, - { value: '3', label: 'Antenna 3' }, - ] - }) %>
    -
    -
    -
    -
    -
    -

    On no active users

    -

    Settings take effect immediately after saving

    -
    - <%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Enabled' }, - ] - }) %>
    - <% if (device === 'tef') { %> - <%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Disabled' }, - { value: '2', label: 'Enabled' }, - ] - }) %>
    - - <%- include('_components', { component: 'dropdown', id: 'imsNoUsers-dropdown', inputId: 'imsNoUsers', label: 'iMS', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Disabled' }, - { value: '2', label: 'Enabled' }, - ] - }) %>
    - <% } else if (device === 'xdr') { %> - <%- include('_components', { component: 'dropdown', id: 'rfNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'RF+', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Disabled' }, - { value: '2', label: 'Enabled' }, - ] - }) %>
    - <%- include('_components', { component: 'dropdown', id: 'ifNoUsers-dropdown', inputId: 'imsNoUsers', label: 'IF+', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Disabled' }, - { value: '2', label: 'Enabled' }, - ] - }) %>
    - <% } %> - <%- include('_components', { component: 'dropdown', id: 'stereoNoUsers-dropdown', inputId: 'stereoNoUsers', label: 'Stereo Mode', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Stereo' }, - { value: '2', label: 'Mono' }, - ] - }) %>
    -
    -
    - <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Delayed Antenna Change', id: 'antennaNoUsersDelay'}) %>
    - <%- include('_components', { component: 'dropdown', id: 'antennaNoUsers-dropdown', inputId: 'antennaNoUsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', - options: [ - { value: '0', label: 'Unchanged' }, - { value: '1', label: 'Antenna 0' }, - { value: '2', label: 'Antenna 1' }, - { value: '3', label: 'Antenna 2' }, - { value: '4', label: 'Antenna 3' }, - ] - }) %> -
    -
    -
    -
    @@ -671,6 +556,126 @@
    +
    +

    Startup settings

    +
    +
    +

    On startup

    +

    Settings take effect on server launch

    +
    + <% if (device === 'tef') { %> + <%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
    + <%- include('_components', { component: 'dropdown', id: 'imsStartup-dropdown', inputId: 'imsStartup', label: 'iMS', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
    + <% } else if (device === 'xdr') { %> + <%- include('_components', { component: 'dropdown', id: 'rfStartup-dropdown', inputId: 'ceqStartup', label: 'RF+', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
    + <%- include('_components', { component: 'dropdown', id: 'ifStartup-dropdown', inputId: 'imsStartup', label: 'IF+', cssClass: '', placeholder: 'Disabled', + options: [ + { value: '0', label: 'Disabled' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
    + <% } %> + <%- include('_components', { component: 'dropdown', id: 'stereoStartup-dropdown', inputId: 'stereoStartup', label: 'Stereo Mode', cssClass: '', placeholder: 'Stereo (Default)', + options: [ + { value: '0', label: 'Stereo (Default)' }, + { value: '1', label: 'Mono' }, + ] + }) %>
    +
    +
    + <%- include('_components', { component: 'dropdown', id: 'antennaStartup-dropdown', inputId: 'antennaStartup', label: 'Antenna', cssClass: '', placeholder: 'Antenna 0 (Default)', + options: [ + { value: '0', label: 'Antenna 0 (Default)' }, + { value: '1', label: 'Antenna 1' }, + { value: '2', label: 'Antenna 2' }, + { value: '3', label: 'Antenna 3' }, + ] + }) %>
    +
    +
    +
    +
    +
    +

    On no active users

    +

    Settings take effect immediately after saving

    +
    + <%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Enabled' }, + ] + }) %>
    + <% if (device === 'tef') { %> + <%- include('_components', { component: 'dropdown', id: 'ceqNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'cEQ', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
    + <%- include('_components', { component: 'dropdown', id: 'imsNoUsers-dropdown', inputId: 'imsNoUsers', label: 'iMS', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
    + <% } else if (device === 'xdr') { %> + <%- include('_components', { component: 'dropdown', id: 'rfNoUsers-dropdown', inputId: 'ceqNoUsers', label: 'RF+', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
    + <%- include('_components', { component: 'dropdown', id: 'ifNoUsers-dropdown', inputId: 'imsNoUsers', label: 'IF+', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Disabled' }, + { value: '2', label: 'Enabled' }, + ] + }) %>
    + <% } %> + <%- include('_components', { component: 'dropdown', id: 'stereoNoUsers-dropdown', inputId: 'stereoNoUsers', label: 'Stereo Mode', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Stereo' }, + { value: '2', label: 'Mono' }, + ] + }) %>
    +
    +
    + <%- include('_components', {component: 'checkbox', cssClass: '', label: 'Delayed Antenna Change', id: 'antennaNoUsersDelay'}) %>
    + <%- include('_components', { component: 'dropdown', id: 'antennaNoUsers-dropdown', inputId: 'antennaNoUsers', label: 'Antenna', cssClass: '', placeholder: 'Unchanged', + options: [ + { value: '0', label: 'Unchanged' }, + { value: '1', label: 'Antenna 0' }, + { value: '2', label: 'Antenna 1' }, + { value: '3', label: 'Antenna 2' }, + { value: '4', label: 'Antenna 3' }, + ] + }) %> +
    +
    +
    +
    +
    +

    Extras

    From edf440828352df7a893ec52c784b6fdf4d6b2df5 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:49:29 +1000 Subject: [PATCH 15/20] change startup icon --- web/setup.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/setup.ejs b/web/setup.ejs index f156ac8..a25489e 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -40,7 +40,7 @@ User management
  • - Startup + Startup
  • Extras From 60c73a138cf0266b51f57d74d3dc0a561bbd4fe7 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:19:16 +1000 Subject: [PATCH 16/20] make sure antenna timeout is cleared on user connect --- server/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/index.js b/server/index.js index 636842f..6b1c708 100644 --- a/server/index.js +++ b/server/index.js @@ -418,6 +418,8 @@ wss.on('connection', (ws, request) => { currentUsers++; } + if (timeoutAntenna) clearTimeout(timeoutAntenna); + helpers.handleConnect(clientIp, currentUsers, ws, (result) => { if (result === "User banned") { ws.close(1008, 'Banned IP'); From 4179d394671cfbbe95366e91f935b6e78397dd24 Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:51:12 +1000 Subject: [PATCH 17/20] Replace arrow character with Unicode escape sequence --- server/stream/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/stream/index.js b/server/stream/index.js index bed4e72..40a878a 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -163,7 +163,7 @@ checkFFmpeg().then((ffmpegPath) => { audioServer.waitUntilReady.then(() => { audioServer.Server.StdIn = ffmpeg.stdout; audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) → FFmpeg (process) → Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); + connectMessage(`${consoleLogTitle} Connected FFmpeg (capture) \u2192 FFmpeg (process) \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); }); ffmpeg.stderr.on('data', (data) => { @@ -248,7 +248,7 @@ checkFFmpeg().then((ffmpegPath) => { audioServer.waitUntilReady.then(() => { audioServer.Server.StdIn = rec.stdout; audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected rec → FFmpeg → Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`); + connectMessage(`${consoleLogTitle} Connected rec \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`); }); process.on('exit', () => { @@ -283,7 +283,7 @@ checkFFmpeg().then((ffmpegPath) => { audioServer.waitUntilReady.then(() => { audioServer.Server.StdIn = ffmpeg.stdout; audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected FFmpeg stdout → Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); + connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); }); process.on('SIGINT', () => { @@ -328,7 +328,7 @@ checkFFmpeg().then((ffmpegPath) => { audioServer.waitUntilReady.then(() => { audioServer.Server.StdIn = arecord.stdout; audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected arecord → FFmpeg → Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`); + connectMessage(`${consoleLogTitle} Connected arecord \u2192 FFmpeg \u2192 Server.StdIn${serverConfig.audio.audioBoost && serverConfig.audio.ffmpeg ? ' (audio boost)' : ''}`); }); process.on('exit', () => { @@ -363,7 +363,7 @@ checkFFmpeg().then((ffmpegPath) => { audioServer.waitUntilReady.then(() => { audioServer.Server.StdIn = ffmpeg.stdout; audioServer.Server.Run(); - connectMessage(`${consoleLogTitle} Connected FFmpeg stdout → Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); + connectMessage(`${consoleLogTitle} Connected FFmpeg stdout \u2192 Server.StdIn${serverConfig.audio.audioBoost ? ' (audio boost)' : ''}`); }); process.on('SIGINT', () => { From 5d3045fe33b3adf0a17f82864d80150309e7a421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Farka=C5=A1?= Date: Sat, 30 Aug 2025 17:45:59 +0200 Subject: [PATCH 18/20] ver bump + fixes for screen readers --- package-lock.json | 48 ++------------------------------------------- package.json | 3 +-- web/index.ejs | 2 +- web/js/webserver.js | 4 ++-- web/setup.ejs | 6 +++--- 5 files changed, 9 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1659271..bb75cd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fm-dx-webserver", - "version": "1.3.9", + "version": "1.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fm-dx-webserver", - "version": "1.3.9", + "version": "1.3.10", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "2.0.0", @@ -16,7 +16,6 @@ "express-session": "1.18.1", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", - "http-proxy": "1.18.1", "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", @@ -800,11 +799,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -985,26 +979,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1132,19 +1106,6 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-response-object": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", @@ -1561,11 +1522,6 @@ "node": ">= 6" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", diff --git a/package.json b/package.json index 49c5d65..9291bcf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.3.9", + "version": "1.3.10", "description": "FM DX Webserver", "main": "index.js", "scripts": { @@ -19,7 +19,6 @@ "express-session": "1.18.1", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", - "http-proxy": "1.18.1", "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", diff --git a/web/index.ejs b/web/index.ejs index 044d2ed..37a1e01 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -547,7 +547,7 @@
  • -
    +
    <% if (!noPlugins) { %> diff --git a/web/js/webserver.js b/web/js/webserver.js index 3475241..f7df278 100644 --- a/web/js/webserver.js +++ b/web/js/webserver.js @@ -1,5 +1,5 @@ -const versionDate = new Date('May 30, 2025 21:00:00'); -const currentVersion = `v1.3.9 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`; +const versionDate = new Date('Aug 30, 2025 21:00:00'); +const currentVersion = `v10 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`; function loadScript(src) { diff --git a/web/setup.ejs b/web/setup.ejs index 3e65604..00ee44e 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -568,7 +568,7 @@

    On startup

    -

    Settings take effect on server launch

    +

    These settings will be applied after a server launch or restart.

    <% if (device === 'tef') { %> <%- include('_components', { component: 'dropdown', id: 'ceqStartup-dropdown', inputId: 'ceqStartup', label: 'cEQ', cssClass: '', placeholder: 'Disabled', @@ -618,8 +618,8 @@
    -

    On no active users

    -

    Settings take effect immediately after saving

    +

    Empty server defaults

    +

    These settings will apply once the last user disconnects from the server, so the server can be ready for a new user with default settings.

    <%- include('_components', { component: 'dropdown', id: 'bwAutoNoUsers-dropdown', inputId: 'bwAutoNoUsers', label: 'Auto BW', cssClass: '', placeholder: 'Unchanged', options: [ From c196cfc53e61b5f06996878df7a9d7124c5e87ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Farka=C5=A1?= Date: Sat, 30 Aug 2025 19:23:53 +0200 Subject: [PATCH 19/20] lowered tef audio boost --- server/stream/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/stream/index.js b/server/stream/index.js index 40a878a..d8bc45f 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -154,7 +154,7 @@ checkFFmpeg().then((ffmpegPath) => { // Apply audio boost if enabled if (serverConfig.audio.audioBoost) { - ffmpegArgs.splice(ffmpegArgs.indexOf('pipe:1'), 0, '-af', 'volume=3.5'); + ffmpegArgs.splice(ffmpegArgs.indexOf('pipe:1'), 0, '-af', 'volume=2.5'); } logDebug(`${consoleLogTitle} Launching FFmpeg with args: ${ffmpegArgs.join(' ')}`); @@ -234,7 +234,7 @@ checkFFmpeg().then((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'); + commandDef.args.splice(commandDef.recArgs.indexOf('pipe:1'), 0, '-af', 'volume=2.5'); } function startRec() { @@ -314,7 +314,7 @@ checkFFmpeg().then((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'); + commandDef.args.splice(commandDef.args.indexOf('pipe:1'), 0, '-af', 'volume=2.5'); } function startArecord() { From 70e829583174554b13899903b7feaafb12a40c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Farka=C5=A1?= Date: Sat, 30 Aug 2025 20:13:14 +0200 Subject: [PATCH 20/20] ver update --- web/js/webserver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/js/webserver.js b/web/js/webserver.js index f7df278..b4ce8bc 100644 --- a/web/js/webserver.js +++ b/web/js/webserver.js @@ -1,5 +1,5 @@ const versionDate = new Date('Aug 30, 2025 21:00:00'); -const currentVersion = `v10 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`; +const currentVersion = `v1.3.10 [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`; function loadScript(src) {