diff --git a/package-lock.json b/package-lock.json index 44a447d..07db793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,10 @@ "dependencies": { "@mapbox/node-pre-gyp": "1.0.11", "body-parser": "1.20.2", - "command-exists-promise": "2.0.2", "ejs": "3.1.9", "express": "4.18.3", "express-session": "1.18.0", + "ffmpeg-static": "^5.2.0", "http": "0.0.1-security", "http-proxy": "1.18.1", "https": "1.0.0", @@ -25,6 +25,20 @@ "ws": "8.14.2" } }, + "node_modules/@derhuerst/http-basic": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@derhuerst/http-basic/-/http-basic-8.2.4.tgz", + "integrity": "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==", + "dependencies": { + "caseless": "^0.12.0", + "concat-stream": "^2.0.0", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", @@ -306,6 +320,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -441,6 +460,11 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/bufferutil": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", @@ -476,6 +500,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -523,19 +552,25 @@ "color-support": "bin.js" } }, - "node_modules/command-exists-promise": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/command-exists-promise/-/command-exists-promise-2.0.2.tgz", - "integrity": "sha512-T6PB6vdFrwnHXg/I0kivM3DqaCGZLjjYSOe0a5WgFKcz1sOnmOeIjnhQPXVXX3QjVbLyTJ85lJkX6lUpukTzaA==", - "engines": { - "node": ">=6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -666,6 +701,14 @@ "node": ">= 0.8" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -756,6 +799,21 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, + "node_modules/ffmpeg-static": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz", + "integrity": "sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==", + "hasInstallScript": true, + "dependencies": { + "@derhuerst/http-basic": "^8.2.0", + "env-paths": "^2.2.0", + "https-proxy-agent": "^5.0.0", + "progress": "^2.0.3" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -1023,6 +1081,14 @@ "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", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", + "dependencies": { + "@types/node": "^10.0.3" + } + }, "node_modules/https": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", @@ -1384,6 +1450,11 @@ "wrappy": "1" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1405,6 +1476,14 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1755,6 +1834,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", diff --git a/package.json b/package.json index 83331df..363000e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.1.5", + "version": "1.1.6", "description": "", "main": "index.js", "scripts": { @@ -14,10 +14,10 @@ "dependencies": { "@mapbox/node-pre-gyp": "1.0.11", "body-parser": "1.20.2", - "command-exists-promise": "2.0.2", "ejs": "3.1.9", "express": "4.18.3", "express-session": "1.18.0", + "ffmpeg-static": "^5.2.0", "http": "0.0.1-security", "http-proxy": "1.18.1", "https": "1.0.0", diff --git a/server/datahandler.js b/server/datahandler.js index 7d5e543..93643ab 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -268,10 +268,15 @@ function handleData(ws, receivedData) { } break; case receivedLine.startsWith('T'): + modifiedData = receivedLine.substring(1).split(",")[0]; + + if((modifiedData / 1000).toFixed(3) == dataToSend.freq) { + return; // Prevent tune spamming using scrollwheel + } + resetToDefault(dataToSend); dataToSend.af.length = 0; rdsparser.clear(rds); - modifiedData = receivedLine.substring(1); parsedValue = parseFloat(modifiedData); if (!isNaN(parsedValue)) { @@ -293,17 +298,20 @@ function handleData(ws, receivedData) { dataToSend.ims = mapping.ims; } break; - case receivedData.startsWith('Sm'): - processSignal(receivedData, false, false); + case receivedLine.startsWith('W'): + console.log(receivedLine); break; - case receivedData.startsWith('Ss'): - processSignal(receivedData, true, false); + case receivedLine.startsWith('Sm'): + processSignal(receivedLine, false, false); break; - case receivedData.startsWith('SS'): - processSignal(receivedData, true, true); + case receivedLine.startsWith('Ss'): + processSignal(receivedLine, true, false); break; - case receivedData.startsWith('SM'): - processSignal(receivedData, false, true); + case receivedLine.startsWith('SS'): + processSignal(receivedLine, true, true); + break; + case receivedLine.startsWith('SM'): + processSignal(receivedLine, false, true); break; case receivedLine.startsWith('R'): modifiedData = receivedLine.slice(1); diff --git a/server/index.js b/server/index.js index adcafba..f7d80fd 100644 --- a/server/index.js +++ b/server/index.js @@ -25,7 +25,6 @@ const { logDebug, logError, logInfo, logWarn } = require('./console'); const storage = require('./storage'); const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); const pjson = require('../package.json'); -require('./stream/index'); console.log(`\x1b[32m _____ __ __ ______ __ __ __ _ @@ -37,6 +36,9 @@ console.log(`\x1b[32m console.log('\x1b[0mFM-DX-Webserver', pjson.version); console.log('\x1b[90m======================================================'); +// Start ffmpeg +require('./stream/index'); + // Create a WebSocket proxy instance const proxy = httpProxy.createProxyServer({ target: 'ws://localhost:' + (Number(serverConfig.webserver.webserverPort) + 10), // WebSocket httpServer's address @@ -67,8 +69,8 @@ function connectToSerial() { serialport.on('open', () => { logInfo('Using COM device: ' + serverConfig.xdrd.comPort); - serialport.write('x\n'); + serialport.write('W0\n'); serialport.write('M0\n'); serialport.write('Y100\n'); serialport.write('D0\n'); @@ -232,6 +234,7 @@ app.use('/', endpoints); * WEBSOCKET BLOCK */ wss.on('connection', (ws, request) => { + const output = serverConfig.xdrd.wirelessConnection ? client : serialport; const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; currentUsers++; dataHandler.showOnlineUsers(currentUsers); @@ -310,14 +313,22 @@ wss.on('connection', (ws, request) => { } } - const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; - const { wirelessConnection } = serverConfig.xdrd; + const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; - if ((serverConfig.publicTuner || (isTuneAuthenticated && wirelessConnection)) && - (!serverConfig.lockToAdmin || isAdminAuthenticated)) { - const output = serverConfig.xdrd.wirelessConnection ? client : serialport; - output.write(`${command}\n`); + if (serverConfig.publicTuner && !serverConfig.lockToAdmin) { + output.write(`${command}\n`); + } else { + if (serverConfig.lockToAdmin) { + if(isAdminAuthenticated) { + output.write(`${command}\n`); + } + } else { + if(isTuneAuthenticated) { + output.write(`${command}\n`); + } + } } + }); ws.on('close', (code, reason) => { @@ -333,9 +344,10 @@ wss.on('connection', (ws, request) => { if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { setTimeout(function() { if(currentUsers === 0) { - client.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); + output.write('T' + Math.round(serverConfig.defaultFreq * 1000) +'\n'); dataHandler.resetToDefault(dataHandler.dataToSend); dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); } }, 10000) } diff --git a/server/stream/3las.server.js b/server/stream/3las.server.js index 65cde62..5637ab2 100644 --- a/server/stream/3las.server.js +++ b/server/stream/3las.server.js @@ -1,16 +1,7 @@ "use strict"; var fs = require('fs'); - -let serverConfig = { - audio: { - audioBitrate: "128k" - }, -}; - -if(fs.existsSync('./config.json')) { - const configFileContents = fs.readFileSync('./config.json', 'utf8'); - serverConfig = JSON.parse(configFileContents); -} +const ffmpegStaticPath = require('ffmpeg-static'); +const {serverConfig} = require('../server_config') /* Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming) @@ -50,12 +41,7 @@ const child_process_1 = require("child_process"); const ws = __importStar(require("ws")); const wrtc = require('wrtc'); const Settings = JSON.parse((0, fs_1.readFileSync)('server/stream/settings.json', 'utf-8')); -const FFmpeg_command = (() => { - if (process.platform === 'win32') - return Settings.FallbackFFmpegPath; - else if (process.platform === 'linux') - return "ffmpeg"; -})(); +const FFmpeg_command = ffmpegStaticPath; class RtcProvider { constructor() { this.RtcDistributePeer = new wrtc.RTCPeerConnection(Settings.RtcConfig); diff --git a/server/stream/index.js b/server/stream/index.js index 717c04b..a5b95d7 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -1,29 +1,18 @@ const { spawn } = require('child_process'); const consoleCmd = require('../console.js'); +const ffmpeg = require('ffmpeg-static'); const { configName, serverConfig, configUpdate, configSave } = require('../server_config'); -const { logDebug, logError, logInfo, logWarn } = require('../console'); -const commandExists = require('command-exists-promise'); - -// Check if FFmpeg is installed -commandExists('ffmpeg') - .then(exists => { - if (exists) { - logInfo("An existing installation of ffmpeg found, enabling audio stream."); - enableAudioStream(); - } else { - logError("No ffmpeg installation found. Audio stream won't be available."); - } - }) - +const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); + function enableAudioStream() { var ffmpegCommand; serverConfig.webserver.webserverPort = Number(serverConfig.webserver.webserverPort); - // Specify the command and its arguments - const command = 'ffmpeg'; + + const command = ffmpeg.replace(/\\/g, '\\\\'); 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`; - // Combine all the settings for the ffmpeg command + if (process.platform === 'win32') { // Windows ffmpegCommand = `${flags} -f dshow -audio_buffer_size 50 -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}`; @@ -34,28 +23,27 @@ function enableAudioStream() { consoleCmd.logInfo("Using audio device: " + serverConfig.audio.audioDevice); consoleCmd.logInfo(`Launching audio stream on internal port ${serverConfig.webserver.webserverPort + 10}.`); - // Spawn the child process + // If an audio device is configured, start the stream if(serverConfig.audio.audioDevice.length > 2) { const childProcess = spawn(command, [ffmpegCommand], { shell: true }); - // Handle the output of the child process (optional) childProcess.stdout.on('data', (data) => { - consoleCmd.logFfmpeg(`stdout: ${data}`); + logFfmpeg(`stdout: ${data}`); }); childProcess.stderr.on('data', (data) => { - consoleCmd.logFfmpeg(`stderr: ${data}`); + logFfmpeg(`stderr: ${data}`); }); - // Handle the child process exit event childProcess.on('close', (code) => { - consoleCmd.logFfmpeg(`Child process exited with code ${code}`); + logFfmpeg(`Child process exited with code ${code}`); }); - // You can also listen for the 'error' event in case the process fails to start childProcess.on('error', (err) => { - consoleCmd.logFfmpeg(`Error starting child process: ${err}`); + logFfmpeg(`Error starting child process: ${err}`); }); } -} \ No newline at end of file +} + +enableAudioStream(); \ No newline at end of file diff --git a/server/stream/parser.js b/server/stream/parser.js index a4ac5a4..0770a33 100644 --- a/server/stream/parser.js +++ b/server/stream/parser.js @@ -2,6 +2,7 @@ const exec = require('child_process').exec; const fs = require('fs'); +const ffmpeg = require('ffmpeg-static'); const filePath = '/proc/asound/cards'; const platform = process.platform; @@ -15,7 +16,7 @@ function parseAudioDevice(options, callback) { options = null; } options = options || {}; - const ffmpegPath = options.ffmpegPath || 'ffmpeg'; + const ffmpegPath = ffmpeg.replace(/\\/g, '\\\\'); const callbackExists = typeof callback === 'function'; let inputDevice, prefix, audioSeparator, alternativeName, deviceParams; diff --git a/web/css/helpers.css b/web/css/helpers.css index 4d7a153..3b62ea1 100644 --- a/web/css/helpers.css +++ b/web/css/helpers.css @@ -59,6 +59,10 @@ border-radius: 5px; } +.br-30 { + border-radius: 30px; +} + .m-0 { margin: 0 !important; } diff --git a/web/index.ejs b/web/index.ejs index 73c4aa4..03d4ea6 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -70,7 +70,7 @@