diff --git a/package-lock.json b/package-lock.json index 564daf4..2ddc1c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "fm-dx-webserver", - "version": "1.1.9", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fm-dx-webserver", - "version": "1.1.9", + "version": "1.2.2", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "1.0.11", - "body-parser": "^1.20.2", + "body-parser": "1.20.2", "ejs": "3.1.9", "express": "4.18.3", "express-session": "1.18.0", diff --git a/package.json b/package.json index aee9f75..6b04dd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.2.2", + "version": "1.2.3", "description": "FM DX Webserver", "main": "index.js", "scripts": { diff --git a/server/datahandler.js b/server/datahandler.js index 43e950f..60bce67 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -200,7 +200,6 @@ const decode_errors = function(string) { }; const updateInterval = 75; -const clientUpdateIntervals = new Map(); // Store update intervals for each client // Initialize the data object var dataToSend = { @@ -247,13 +246,13 @@ const filterMappings = { var legacyRdsPiBuffer = null; +var lastUpdateTime = Date.now(); const initialData = { ...dataToSend }; const resetToDefault = dataToSend => Object.assign(dataToSend, initialData); -function handleData(ws, receivedData) { +function handleData(wss, receivedData) { // Retrieve the last update time for this client - let lastUpdateTime = clientUpdateIntervals.get(ws) || 0; const currentTime = Date.now(); let modifiedData, parsedValue; @@ -374,8 +373,10 @@ function handleData(ws, receivedData) { // Send the updated data to the client const dataToSendJSON = JSON.stringify(dataToSend); if (currentTime - lastUpdateTime >= updateInterval) { - clientUpdateIntervals.set(ws, currentTime); // Update the last update time for this client - ws.send(dataToSendJSON); + wss.clients.forEach((client) => { + client.send(dataToSendJSON); + }); + lastUpdateTime = Date.now(); } } diff --git a/server/fmdx_list.js b/server/fmdx_list.js index 977d7ea..fe7e919 100644 --- a/server/fmdx_list.js +++ b/server/fmdx_list.js @@ -8,7 +8,7 @@ var pjson = require('../package.json'); let timeoutID = null; function send(request) { - const url = "https://list.fmdx.pl/api/"; + const url = "https://servers.fmdx.org/api/"; const options = { method: 'POST', @@ -53,7 +53,7 @@ function sendKeepalive() { const request = { token: serverConfig.identification.token, - status: (serverConfig.lockToAdmin ? 2 : 1) + status: ((serverConfig.lockToAdmin || !serverConfig.publicTuner) ? 2 : 1) }; send(request); diff --git a/server/helpers.js b/server/helpers.js index 8709173..e31b5fc 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -73,12 +73,8 @@ function resolveDataBuffer(data, wss) { } if (receivedData.length) { - wss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - dataHandler.handleData(client, receivedData); - } - }); - } + dataHandler.handleData(wss, receivedData); + }; } function kickClient(ipAddress) { diff --git a/server/index.js b/server/index.js index c803fe5..3997c61 100644 --- a/server/index.js +++ b/server/index.js @@ -64,6 +64,7 @@ connectToSerial(); // Serial Connection function connectToSerial() { + let okReceived = false; if (serverConfig.xdrd.wirelessConnection === false) { serialport = new SerialPort({path: serverConfig.xdrd.comPort, baudRate: 115200 }); @@ -71,27 +72,34 @@ function connectToSerial() { serialport.on('open', () => { logInfo('Using COM device: ' + serverConfig.xdrd.comPort); serialport.write('x\n'); - serialport.write('Q0\n'); - serialport.write('M0\n'); - serialport.write('Z0\n'); - if(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true) { - serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); - dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); - dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); - } else { - serialport.write('T87500\n'); - } - - serialport.write('A0\n'); - serialport.write('F-1\n'); - serialport.write('W0\n'); - serialport.write('D0\n'); - serialport.write('G00\n'); - serverConfig.audio.startupVolume ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') : serialport.write('Y100\n'); - serialport.on('data', (data) => { - helpers.resolveDataBuffer(data, wss); + if (receivedData.startsWith('OK')) { + okReceived = true; + + // Send the remaining commands + serialport.write('Q0\n'); + serialport.write('M0\n'); + serialport.write('Z0\n'); + + if(serverConfig.defaultFreq && serverConfig.enableDefaultFreq === true) { + serialport.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); + dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); + dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + } else { + serialport.write('T87500\n'); + } + + serialport.write('A0\n'); + serialport.write('F-1\n'); + serialport.write('W0\n'); + serialport.write('D0\n'); + serialport.write('G00\n'); + serverConfig.audio.startupVolume ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') : serialport.write('Y100\n'); + } else { + // Continue handling data normally if it's not the "OK" message + helpers.resolveDataBuffer(data, wss); + } }); }); @@ -273,8 +281,8 @@ wss.on('connection', (ws, request) => { const command = message.toString(); logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); - if (command.startsWith('X')) { - logWarn(`Remote tuner shutdown attempted by \x1b[90m${clientIp}\x1b[0m. You may consider blocking this user.`); + if ((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) { + logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command. You may consider blocking this user.`); return; } diff --git a/server/plugins.js b/server/plugins.js index 3b87e6b..1b26022 100644 --- a/server/plugins.js +++ b/server/plugins.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const consoleCmd = require('./console'); const { serverConfig } = require('./server_config'); // Function to read all .js files in a directory @@ -31,7 +32,9 @@ function parsePluginConfig(filePath) { // Copy the file to the destination directory const destinationFile = path.join(destinationDir, path.basename(sourcePath)); fs.copyFileSync(sourcePath, destinationFile); - console.log(`File copied from ${sourcePath} to ${destinationFile}`); + setTimeout(function() { + consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); + }, 500) } else { console.error(`Error: frontEndPath is not defined in ${filePath}`); } diff --git a/server/stream/3las.server.js b/server/stream/3las.server.js index e12bbef..330cce6 100644 --- a/server/stream/3las.server.js +++ b/server/stream/3las.server.js @@ -1,46 +1,11 @@ "use strict"; -var fs = require('fs'); -const ffmpegStaticPath = require('ffmpeg-static'); -const {serverConfig} = require('../server_config') +const fs = require('fs'); +const ws = require('ws'); +const { serverConfig } = require('../server_config'); + +// Load settings from config +const Settings = JSON.parse(fs.readFileSync('server/stream/settings.json', 'utf-8')); -/* - Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming) - https://github.com/JoJoBond/3LAS -*/ -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]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -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 FFmpeg_command = ffmpegStaticPath; class StreamClient { constructor(server, socket) { this.Server = server; @@ -51,155 +16,127 @@ class StreamClient { }; this.Socket.on('error', this.OnError.bind(this)); this.Socket.on('message', this.OnMessage.bind(this)); + this.Socket.on('close', this.OnClose.bind(this)); } - OnMessage(message, isBinary) { + + OnMessage(message) { try { let request = JSON.parse(message.toString()); - if (request.type == "answer") { - - } - else if (request.type == "fallback") { - this.Server.SetFallback(this, request.data); - } - else if (request.type == "stats") { - if (Settings.AdminKey && request.data == Settings.AdminKey) { + if (request.type === "answer") { + // Handle answer type messages if needed + } else if (request.type === "fallback") { + // Assuming SetFallback is not needed or replace with the correct method + console.warn('SetFallback method not defined. Fallback request ignored.'); + } else if (request.type === "stats") { + if (Settings.AdminKey && request.data === Settings.AdminKey) { this.SendText(JSON.stringify({ "type": "stats", "data": this.Server.GetStats(), })); } + } else { + this.OnError(new Error("Invalid message type")); } - else { - this.OnError(null); - return; - } - } - catch (_a) { - this.OnError(null); - return; + } catch (error) { + this.OnError(error); } } - OnError(_err) { + + OnError(error) { + console.error('WebSocket error:', error); this.Server.DestroyClient(this); } + + OnClose() { + console.log('WebSocket connection closed'); + this.Server.DestroyClient(this); + } + Destroy() { - try { - this.Socket.close(); - } - catch (ex) { + if (this.Socket.readyState === ws.OPEN) { + try { + this.Socket.close(); + } catch (ex) { + console.error('Error while closing socket:', ex); + } } } + SendBinary(buffer) { - if (this.Socket.readyState != ws.OPEN) { - this.OnError(null); + if (this.Socket.readyState !== ws.OPEN) { + this.OnError(new Error('Socket is not open')); return; } this.Socket.send(buffer, this.BinaryOptions); } + SendText(text) { - if (this.Socket.readyState != ws.OPEN) { - this.OnError(null); + if (this.Socket.readyState !== ws.OPEN) { + this.OnError(new Error('Socket is not open')); return; } this.Socket.send(text); } - OnIceCandidate(e) { - if (e.candidate) { - this.SendText(JSON.stringify({ - "type": "candidate", - "data": e.candidate - })); - } - } } + class StreamServer { constructor(port, channels, sampleRate) { this.Port = port; this.Channels = channels; this.SampleRate = sampleRate; this.Clients = new Set(); - this.FallbackClients = { - "wav": new Set(), - "mp3": new Set() - }; - this.FallbackProvider = {}; - if (Settings.FallbackUseMp3) { - this.FallbackProvider["mp3"] = AFallbackProvider.Create(this, "mp3"); - } - if (Settings.FallbackUseWav) { - this.FallbackProvider["wav"] = AFallbackProvider.Create(this, "wav"); - } this.StdIn = process.stdin; - this.SamplesCount = this.SampleRate / 100; - this.Samples = new Int16Array(this.Channels * this.SamplesCount); - this.SamplesPosition = 0; + this.Buffer = Buffer.alloc(0); } + Run() { this.Server = new ws.Server({ - "host": ["127.0.0.1", "::1"], - "port": this.Port, - "clientTracking": true, - "perMessageDeflate": false + host: ["127.0.0.1", "::1"], + port: this.Port, + clientTracking: true, + perMessageDeflate: false }); this.Server.on('connection', this.OnServerConnection.bind(this)); this.StdIn.on('data', this.OnStdInData.bind(this)); this.StdIn.resume(); } - BroadcastBinary(format, buffer) { - this.FallbackClients[format].forEach((function each(client) { - client.SendBinary(buffer); - }).bind(this)); - } + OnStdInData(buffer) { - for (let i = 0; i < buffer.length; i += 2) { - this.Samples[this.SamplesPosition] = buffer.readInt16LE(i); - this.SamplesPosition++; - if (this.SamplesPosition >= this.Samples.length) { - let data = { - "samples": this.Samples, - "sampleRate": this.SampleRate, - "bitsPerSample": 16, - "channelCount": this.Channels, - "numberOfFrames": this.SamplesCount, - }; - this.Samples = new Int16Array(this.Channels * this.SamplesCount); - this.SamplesPosition = 0; + this.Buffer = Buffer.concat([this.Buffer, buffer]); + if (this.Buffer.length >= 8192) { // Adjust the buffer size as needed + this.SendAudioData(this.Buffer); + this.Buffer = Buffer.alloc(0); + } + } + + SendAudioData(buffer) { + this.Clients.forEach(client => { + try { + client.SendBinary(buffer); + } catch (error) { + console.error('Error sending data to client:', error); } - } - for (let format in this.FallbackProvider) { - this.FallbackProvider[format].InsertData(buffer); - } + }); } - OnServerConnection(socket, _request) { - this.Clients.add(new StreamClient(this, socket)); - } - SetFallback(client, format) { - if (format != "mp3" && format != "wav") { - this.DestroyClient(client); - return; - } - this.FallbackClients[format].add(client); - this.FallbackProvider[format].PrimeClient(client); + + OnServerConnection(socket) { + const client = new StreamClient(this, socket); + this.Clients.add(client); + console.log('New client connected'); } + DestroyClient(client) { - this.FallbackClients["mp3"].delete(client); - this.FallbackClients["wav"].delete(client); this.Clients.delete(client); client.Destroy(); + console.log('Client connection destroyed'); } + GetStats() { - let fallback = { - "wav": (this.FallbackClients["wav"] ? this.FallbackClients["wav"].size : 0), - "mp3": (this.FallbackClients["mp3"] ? this.FallbackClients["mp3"].size : 0), - }; - for (let format in fallback) { - total += fallback[format]; - } return { - "Total": total, - "Fallback": fallback, + "Total": this.Clients.size, }; } + static Create(options) { if (!options["-port"]) throw new Error("Port undefined. Please use -port to define the port."); @@ -208,7 +145,7 @@ class StreamServer { 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"]) || - !(options["-channels"] == 1 || options["-channels"] == 2)) + !(options["-channels"] === 1 || options["-channels"] === 2)) throw new Error("Invalid channels. Must be either 1 or 2."); if (!options["-samplerate"]) throw new Error("Sample rate undefined. Please use -samplerate to define the sample rate."); @@ -217,114 +154,21 @@ class StreamServer { return new StreamServer(options["-port"], options["-channels"], options["-samplerate"]); } } -class AFallbackProvider { - constructor(server) { - this.Server = server; - this.Process = (0, child_process_1.spawn)(FFmpeg_command, this.GetFFmpegArguments(), { shell: false, detached: false, stdio: ['pipe', 'pipe', 'ignore'] }); - this.Process.stdout.addListener('data', this.OnData.bind(this)); - } - InsertData(buffer) { - this.Process.stdin.write(buffer); - } - static Create(server, format) { - if (format == "mp3") { - return new FallbackProviderMp3(server); - } - else if (format == "wav") { - return new FallbackProviderWav(server, 384); - } - } -} -class FallbackProviderMp3 extends AFallbackProvider { - constructor(server) { - super(server); - } - GetFFmpegArguments() { - return [ - "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", - "-f", "s16le", - "-ar", this.Server.SampleRate.toString(), - "-ac", this.Server.Channels.toString(), - "-i", "pipe:0", - "-c:a", "libmp3lame", - "-b:a", serverConfig.audio.audioBitrate, - "-ac", this.Server.Channels.toString(), - "-reservoir", "0", - "-f", "mp3", "-write_xing", "0", "-id3v2_version", "0", - "-fflags", "+nobuffer", "-flush_packets", "1", - "pipe:1" - ]; - } - OnData(chunk) { - this.Server.BroadcastBinary("mp3", chunk); - } - PrimeClient(_) { - } -} -class FallbackProviderWav extends AFallbackProvider { - constructor(server, chunkSize) { - super(server); - if (typeof chunkSize !== "number" || chunkSize !== Math.floor(chunkSize) || chunkSize < 1) - throw new Error("Invalid ChunkSize. Must be natural number greater than or equal to 1."); - this.ChunkSize = chunkSize; - this.ChunkBuffer = Buffer.alloc(0); - this.HeaderBuffer = new Array(); - } - GetFFmpegArguments() { - return [ - "-fflags", "+nobuffer+flush_packets", "-flags", "low_delay", "-rtbufsize", "32", "-probesize", "32", - "-f", "s16le", - "-ar", this.Server.SampleRate.toString(), - "-ac", this.Server.Channels.toString(), - "-i", "pipe:0", - "-c:a", "pcm_s16le", - "-ar", Settings.FallbackWavSampleRate.toString(), - "-ac", "1", - "-f", "wav", - "-flush_packets", "1", "-fflags", "+nobuffer", "-chunk_size", "384", "-packetsize", "384", - "pipe:1" - ]; - } - OnData(chunk) { - // Check if riff for wav - if (this.HeaderBuffer.length == 0) { - // Check if chunk is a header page - let isHeader = (chunk[0] == 0x52 && chunk[1] == 0x49 && chunk[2] == 0x46 && chunk[3] == 0x46); - if (isHeader) { - this.HeaderBuffer.push(chunk); - this.Server.BroadcastBinary("wav", chunk); - } - } - else { - this.ChunkBuffer = Buffer.concat(new Array(this.ChunkBuffer, chunk), this.ChunkBuffer.length + chunk.length); - if (this.ChunkBuffer.length >= this.ChunkSize) { - let chunkBuffer = this.ChunkBuffer; - this.ChunkBuffer = Buffer.alloc(0); - this.Server.BroadcastBinary("wav", chunkBuffer); - } - } - } - PrimeClient(client) { - let headerBuffer = this.HeaderBuffer; - for (let i = 0; i < headerBuffer.length; i++) { - client.SendBinary(headerBuffer[i]); - } - } -} + const OptionParser = { - "-port": function (txt) { return parseInt(txt, 10); }, - "-channels": function (txt) { return parseInt(txt, 10); }, - "-samplerate": function (txt) { return parseInt(txt, 10); } + "-port": txt => parseInt(txt, 10), + "-channels": txt => parseInt(txt, 10), + "-samplerate": txt => parseInt(txt, 10) }; + const Options = {}; -// Parse parameters -for (let i = 2; i < (process.argv.length - 1); i += 2) { +for (let i = 2; i < process.argv.length; i += 2) { if (!OptionParser[process.argv[i]]) throw new Error("Invalid argument: '" + process.argv[i] + "'."); if (Options[process.argv[i]]) 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 \ No newline at end of file diff --git a/server/stream/index.js b/server/stream/index.js index 9979df9..6a84dde 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -8,17 +8,17 @@ function enableAudioStream() { serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`; - const codec = `-acodec pcm_s16le -ar 48000 -ac ${serverConfig.audio.audioChannels}`; - const output = `-f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960`; + const codec = `-acodec libmp3lame -ar 48000 -ac ${serverConfig.audio.audioChannels} -b:a ${serverConfig.audio.audioBitrate}`; + const output = `-f mp3 -fflags +nobuffer+flush_packets -flush_packets 1 -bufsize 960`; if (process.platform === 'win32') { // Windows ffmpegCommand = "\"" + ffmpeg.replace(/\\/g, '\\\\') + "\""; - ffmpegParams = `${flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; - } else { + ffmpegParams = `${flags} -f dshow -audio_buffer_size 200 -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 44100 -channels ${serverConfig.audio.audioChannels}`; + } else { // Linux ffmpegCommand = 'ffmpeg'; - ffmpegParams = `${flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 48000 -channels ${serverConfig.audio.audioChannels}`; + ffmpegParams = `${flags} -f alsa -i "${serverConfig.audio.softwareMode && serverConfig.audio.softwareMode == true ? 'plug' : ''}${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node server/stream/3las.server.js -port ${serverConfig.webserver.webserverPort + 10} -samplerate 44100 -channels ${serverConfig.audio.audioChannels}`; } logInfo("Trying to start audio stream on device: \x1b[35m" + serverConfig.audio.audioDevice); diff --git a/web/index.ejs b/web/index.ejs index b8ef67b..a382ad7 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -333,7 +333,7 @@