diff --git a/.gitignore b/.gitignore index 8512382..cd2319d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules/ /example.js -/userconfig.json -/userconfig_backup.js \ No newline at end of file +/config.json \ No newline at end of file diff --git a/README.md b/README.md index 5a07b72..df77279 100644 --- a/README.md +++ b/README.md @@ -45,32 +45,11 @@ Version >=21.6.0 is currently not working correctly. npm install ``` -4. Update your config in userconfig.js: - - ```js - const webServerHost = '0.0.0.0'; // IP of the web server - const webServerPort = 8080; // web server port - const webServerName = "Noobish's Server"; // web server name (will be displayed in title, bookmarks...) - - const audioDeviceName = "Microphone (High Definition Audio Device)"; // Audio device name in your OS - const audioPort = 8081; // Port for the audio stream - - const xdrdServerHost = '127.0.0.1'; // xdrd server IP (if it's running on the same machine, use 127.0.0.1) - const xdrdServerPort = 7373; // xdrd server port - const xdrdPassword = 'password'; // xdrd password (optional) - - const qthLatitude = '50.123456'; // your latitude, useful for maps.fmdx.pl integration - const qthLongitude = '15.123456'; // your longitude, useful for maps.fmdx.pl integration - - const verboseMode = false; // if true, console will display extra messages - - ``` - 4. Start the server: ```bash - node . + npm run webserver ``` 4. Open your web browser and navigate to `http://web-server-ip:web-server-port` to access the web interface. diff --git a/console.js b/console.js index 0d585bf..461b3a6 100644 --- a/console.js +++ b/console.js @@ -1,4 +1,4 @@ -const { verboseMode } = require('./userconfig'); +const verboseMode = process.argv.includes('--debug'); const getCurrentTime = () => { const currentTime = new Date(); diff --git a/datahandler.js b/datahandler.js index 9ae0f2c..4095237 100644 --- a/datahandler.js +++ b/datahandler.js @@ -7,7 +7,6 @@ const os = require('os'); const win32 = (os.platform() == "win32"); const unicode_type = (win32 ? 'int16_t' : 'int32_t'); const lib = koffi.load(path.join(__dirname, "librdsparser." + (win32 ? "dll" : "so"))); -const config = require('./userconfig'); koffi.proto('void callback_pi(void *rds, void *user_data)'); koffi.proto('void callback_pty(void *rds, void *user_data)'); diff --git a/index.js b/index.js index 27f3f7e..6247510 100644 --- a/index.js +++ b/index.js @@ -1,27 +1,77 @@ -/* Libraries / Imports */ +/** + * LIBRARIES AND IMPORTS + */ + +// Web handling const express = require('express'); -const app = express(); +const session = require('express-session'); +const bodyParser = require('body-parser'); const http = require('http'); const https = require('https'); +const app = express(); const httpServer = http.createServer(app); +const ejs = require('ejs'); + +// Websocket handling const WebSocket = require('ws'); const wss = new WebSocket.Server({ noServer: true }); const path = require('path'); const net = require('net'); const client = new net.Socket(); + +// Other files and libraries const crypto = require('crypto'); +const fs = require('fs'); const commandExists = require('command-exists-promise'); const dataHandler = require('./datahandler'); const consoleCmd = require('./console'); -const config = require('./userconfig'); const audioStream = require('./stream/index.js'); +const { parseAudioDevice } = require('./stream/parser.js'); +const configPath = path.join(__dirname, 'config.json'); -const { webServerHost, webServerPort, webServerName, audioPort, xdrdServerHost, xdrdServerPort, xdrdPassword, qthLatitude, qthLongitude } = config; const { logDebug, logError, logInfo, logWarn } = consoleCmd; let currentUsers = 0; let streamEnabled = false; let incompleteDataBuffer = ''; +let serverConfig = { + webserver: { + webserverIp: "0.0.0.0", + webserverPort: "8080", + audioPort: "8081" + }, + xdrd: { + xdrdIp: "127.0.0.1", + xdrdPort: "7373", + xdrdPassword: "password" + }, + identification: { + tunerName: "", + tunerDesc: "", + lat: "", + lon: "" + }, + password: { + tunePass: "", + adminPass: "" + }, + publicTuner: true, + lockToAdmin: false +}; + +if(fs.existsSync('config.json')) { + const configFileContents = fs.readFileSync('config.json', 'utf8'); + serverConfig = JSON.parse(configFileContents); +} + +app.use(bodyParser.urlencoded({ extended: true })); +const sessionMiddleware = session({ + secret: 'GTce3tN6U8odMwoI', + resave: false, + saveUninitialized: true, +}); +app.use(sessionMiddleware); +app.use(bodyParser.json()); /* Audio Stream */ commandExists('ffmpeg') @@ -38,56 +88,6 @@ commandExists('ffmpeg') // Should never happen but better handle it just in case }) -/* webSocket handlers */ -wss.on('connection', (ws, request) => { - const clientIp = request.connection.remoteAddress; - currentUsers++; - dataHandler.showOnlineUsers(currentUsers); - - // Use ipinfo.io API to get geolocation information - https.get(`https://ipinfo.io/${clientIp}/json`, (response) => { - let data = ''; - - response.on('data', (chunk) => { - data += chunk; - }); - - response.on('end', () => { - try { - const locationInfo = JSON.parse(data); - if(locationInfo.country === undefined) { - logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); - } else { - logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`); - } - } catch (error) { - logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); - } - }); - }); - - ws.on('message', (message) => { - logDebug('Command received from \x1b[90m' + request.connection.remoteAddress + '\x1b[0m:', message.toString()); - command = message.toString(); - - if(command.startsWith('X')) { - logWarn('Remote tuner shutdown attempted by \x1b[90m' + request.connection.remoteAddress + '\x1b[0m. You may consider blocking this user.'); - } else { - client.write(command + "\n"); - } - }); - - ws.on('close', (code, reason) => { - currentUsers--; - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); - }); - - ws.on('error', console.error); -}); - -/* Serving of HTML files */ -app.use(express.static(path.join(__dirname, 'web'))); - // Function to authenticate with the xdrd server function authenticateWithXdrd(client, salt, password) { const sha1 = crypto.createHash('sha1'); @@ -101,8 +101,8 @@ function authenticateWithXdrd(client, salt, password) { client.write('x\n'); } -// WebSocket client connection -client.connect(xdrdServerPort, xdrdServerHost, () => { +// xdrd connection +client.connect(serverConfig.xdrd.xdrdPort, serverConfig.xdrd.xdrdIp, () => { logInfo('Connection to xdrd established successfully.'); const authFlags = { @@ -119,7 +119,7 @@ client.connect(xdrdServerPort, xdrdServerHost, () => { if (!authFlags.receivedPassword) { authFlags.receivedSalt = line.trim(); - authenticateWithXdrd(client, authFlags.receivedSalt, xdrdPassword); + authenticateWithXdrd(client, authFlags.receivedSalt, serverConfig.xdrd.xdrdPassword); authFlags.receivedPassword = true; } else { if (line.startsWith('a')) { @@ -176,40 +176,237 @@ client.connect(xdrdServerPort, xdrdServerHost, () => { }); client.on('close', () => { - console.log('Disconnected from xdrd'); + logWarn('Disconnected from xdrd.'); }); client.on('error', (err) => { switch (true) { case err.message.includes("ECONNRESET"): logError("Connection to xdrd lost. Exiting..."); - break; + process.exit(1); case err.message.includes("ETIMEDOUT"): - logError("Connection to xdrd @ " + xdrdServerHost + ":" + xdrdServerPort + " timed out."); + logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xrd.xdrdPort + " timed out."); + process.exit(1); + + case err.message.includes("ECONNREFUSED"): + logError("Connection to xdrd @ " + serverConfig.xdrd.xdrdIp + ":" + serverConfig.xdrd.xdrdPort + " failed. Is xdrd running?"); break; default: logError("Unhandled error: ", err.message); } - - process.exit(1); -}); - - -/* HTTP Server */ - -httpServer.on('upgrade', (request, socket, head) => { - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit('connection', ws, request); - }); -}); - -httpServer.listen(webServerPort, webServerHost, () => { - logInfo(`Web server is running at \x1b[34mhttp://${webServerHost}:${webServerPort}\x1b[0m.`); }); /* Static data are being sent through here on connection - these don't change when the server is running */ app.get('/static_data', (req, res) => { - res.json({ qthLatitude, qthLongitude, webServerName, audioPort, streamEnabled}); + res.json({ + qthLatitude: serverConfig.identification.lat, + qthLongitude: serverConfig.identification.lon, + audioPort: serverConfig.webserver.audioPort, + streamEnabled: streamEnabled + }); }); + + +/** + * AUTHENTICATION BLOCK + */ +const authenticate = (req, res, next) => { + const { password } = req.body; + + // Check if the entered password matches the admin password + if (password === serverConfig.password.adminPass) { + req.session.isAdminAuthenticated = true; + req.session.isTuneAuthenticated = true; + logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.'); + next(); + } else if (password === serverConfig.password.tunePass) { + req.session.isAdminAuthenticated = false; + req.session.isTuneAuthenticated = true; + logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.'); + next(); + } else { + res.status(403).json({ message: 'Login failed. Wrong password?' }); + } +}; + +app.set('view engine', 'ejs'); // Set EJS as the template engine +app.set('views', path.join(__dirname, '/web')) + +app.get('/', (req, res) => { + if (!fs.existsSync("config.json")) { + parseAudioDevice((result) => { + res.render('setup', { + isAdminAuthenticated: true, + videoDevices: result.audioDevices, + audioDevices: result.videoDevices }); + });; + } else { + res.render('index', { + isAdminAuthenticated: req.session.isAdminAuthenticated, + isTuneAuthenticated: req.session.isTuneAuthenticated, + tunerName: serverConfig.identification.tunerName, + tunerDesc: serverConfig.identification.tunerDesc, + tunerLock: serverConfig.lockToAdmin + }) + } +}); + +app.get('/setup', (req, res) => { + parseAudioDevice((result) => { + res.render('setup', { + isAdminAuthenticated: req.session.isAdminAuthenticated, + videoDevices: result.audioDevices, + audioDevices: result.videoDevices }); + }); +}); + + +// Route for login +app.post('/login', authenticate, (req, res) => { + // Redirect to the main page after successful login + res.status(200).json({ message: 'Logged in successfully, refreshing the page...' }); +}); + +app.get('/logout', (req, res) => { + // Clear the session and redirect to the main page + req.session.destroy(() => { + res.status(200).json({ message: 'Logged out successfully, refreshing the page...' }); + }); +}); + +app.post('/saveData', (req, res) => { + const data = req.body; + let firstSetup; + if(req.session.isAdminAuthenticated || !fs.existsSync('config.json')) { + if(!fs.existsSync('config.json')) { + firstSetup = true; + } + // Save data to a JSON file + fs.writeFile('config.json', JSON.stringify(data, null, 2), (err) => { + if (err) { + logError(err); + res.status(500).send('Internal Server Error'); + } else { + logInfo('Server config changed successfully.'); + const configFileContents = fs.readFileSync('config.json', 'utf8'); + serverConfig = JSON.parse(configFileContents); + if(firstSetup === true) { + res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.'); + } else { + res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.'); + } + } + }); + } +}); + +// Serve the data.json file when the /getData endpoint is accessed +app.get('/getData', (req, res) => { + if(req.session.isAdminAuthenticated) { + // Check if the file exists + fs.access(configPath, fs.constants.F_OK, (err) => { + if (err) { + // File does not exist + res.status(404).send('Data not found'); + } else { + // File exists, send it as the response + res.sendFile(configPath); + } + }); + } +}); + +app.get('/getDevices', (req, res) => { + if (req.session.isAdminAuthenticated || !fs.existsSync('config.json')) { + parseAudioDevice((result) => { + console.log(result); + res.json(result); + }); + } else { + res.status(403).json({ error: 'Unauthorized' }); + } +}); + +/** + * WEBSOCKET BLOCK + */ + +wss.on('connection', (ws, request) => { + const clientIp = request.connection.remoteAddress; + currentUsers++; + dataHandler.showOnlineUsers(currentUsers); + + // Use ipinfo.io API to get geolocation information + https.get(`https://ipinfo.io/${clientIp}/json`, (response) => { + let data = ''; + + response.on('data', (chunk) => { + data += chunk; + }); + + response.on('end', () => { + try { + const locationInfo = JSON.parse(data); + if(locationInfo.country === undefined) { + logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); + } else { + logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${locationInfo.city}, ${locationInfo.region}, ${locationInfo.country}`); + } + } catch (error) { + logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m`); + } + }); + }); + + ws.on('message', (message) => { + logDebug('Command received from \x1b[90m' + request.connection.remoteAddress + '\x1b[0m:', message.toString()); + command = message.toString(); + + if(command.startsWith('X')) { + logWarn('Remote tuner shutdown attempted by \x1b[90m' + request.connection.remoteAddress + '\x1b[0m. You may consider blocking this user.'); + return; + } + + if((serverConfig.publicTuner === true) || (request.session && request.session.isTuneAuthenticated === true)) { + + if(serverConfig.lockToAdmin === true) { + if(request.session && request.session.isAdminAuthenticated === true) { + client.write(command + "\n"); + } else { + return; + } + } else { + client.write(command + "\n"); + } + } + }); + + ws.on('close', (code, reason) => { + currentUsers--; + logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); + }); + + ws.on('error', console.error); +}); + + +httpServer.on('upgrade', (request, socket, head) => { + sessionMiddleware(request, {}, () => { + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request); + }); + }); +}); + +/* Serving of HTML files */ +app.use(express.static(path.join(__dirname, 'web'))); + +httpServer.listen(serverConfig.webserver.webserverPort, serverConfig.webserver.webserverIp, () => { + logInfo(`Web server is running at \x1b[34mhttp://${serverConfig.webserver.webserverIp}:${serverConfig.webserver.webserverPort}\x1b[0m.`); +}); + +module.exports = { + serverConfig +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2617b85..b0562ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,11 @@ "license": "ISC", "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.2", + "express-session": "^1.18.0", "http": "^0.0.1-security", "https": "1.0.0", "koffi": "2.7.2", @@ -98,6 +101,20 @@ "node": ">=8" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -120,18 +137,23 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -139,7 +161,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -190,6 +212,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -198,6 +235,22 @@ "node": ">=10" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -331,6 +384,20 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", + "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -431,6 +498,74 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", + "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "dependencies": { + "cookie": "0.6.0", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -444,6 +579,33 @@ "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -575,6 +737,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", @@ -728,6 +898,23 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/jake": { + "version": "10.8.7", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", + "integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/koffi": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz", @@ -983,6 +1170,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1038,6 +1233,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1047,9 +1250,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -1249,6 +1452,17 @@ "node": ">=8" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -1303,6 +1517,17 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 3106c4f..cbc61c8 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,18 @@ "description": "", "main": "index.js", "scripts": { - "debug": "node index.js", - "start": "node index.js" + "debug": "node index.js --debug", + "webserver": "node index.js" }, "author": "", "license": "ISC", "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.2", + "express-session": "^1.18.0", "http": "^0.0.1-security", "https": "1.0.0", "koffi": "2.7.2", diff --git a/stream/3las.server.js b/stream/3las.server.js index 92783d8..80cb9d9 100644 --- a/stream/3las.server.js +++ b/stream/3las.server.js @@ -1,4 +1,17 @@ "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); +} + /* Stdin streamer is part of 3LAS (Low Latency Live Audio Streaming) https://github.com/JoJoBond/3LAS @@ -340,7 +353,7 @@ class FallbackProviderMp3 extends AFallbackProvider { "-ac", this.Server.Channels.toString(), "-i", "pipe:0", "-c:a", "libmp3lame", - "-b:a", Settings.FallbackMp3Bitrate.toString() + "k", + "-b:a", serverConfig.audio.audioBitrate, "-ac", this.Server.Channels.toString(), "-reservoir", "0", "-f", "mp3", "-write_xing", "0", "-id3v2_version", "0", diff --git a/stream/index.js b/stream/index.js index a5f96a2..6032a4c 100644 --- a/stream/index.js +++ b/stream/index.js @@ -1,27 +1,44 @@ const { spawn } = require('child_process'); -const config = require('../userconfig.js'); +const fs = require('fs'); const consoleCmd = require('../console.js'); +let serverConfig = { + webserver: { + audioPort: "8081" + }, + audio: { + audioDevice: "Microphone (High Definition Audio Device)", + audioChannels: 2, + audioBitrate: "128k" + }, +}; + +if(fs.existsSync('./config.json')) { + const configFileContents = fs.readFileSync('./config.json', 'utf8'); + serverConfig = JSON.parse(configFileContents); +} + function enableAudioStream() { var ffmpegCommand; // Specify the command and its arguments const command = 'ffmpeg'; - const flags = '-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 64'; - const codec = '-acodec pcm_s16le -ar 48000 -ac 2'; - const output = '-f s16le -fflags +nobuffer+flush_packets -packetsize 384 -flush_packets 1 -bufsize 960'; + const flags = `-fflags +nobuffer+flush_packets -flags low_delay -rtbufsize 6192 -probesize 32`; + const codec = `-acodec pcm_s16le -ar 32000 -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 -i audio="${config.audioDeviceName}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${config.audioPort} -samplerate 48000 -channels 2`; + ffmpegCommand = `${flags} -f dshow -i audio="${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 32000 -channels ${serverConfig.audio.audioChannels}`; } else { // Linux - ffmpegCommand = `${flags} -f alsa -i "${config.audioDeviceName}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${config.audioPort} -samplerate 48000 -channels 2`; + ffmpegCommand = `${flags} -f alsa -i "${serverConfig.audio.audioDevice}" ${codec} ${output} pipe:1 | node stream/3las.server.js -port ${serverConfig.webserver.audioPort} -samplerate 32000 -channels ${serverConfig.audio.audioChannels}`; } - consoleCmd.logInfo("Launching audio stream on port " + config.audioPort + "."); + consoleCmd.logInfo("Using audio device: " + serverConfig.audio.audioDevice); + consoleCmd.logInfo("Launching audio stream on port " + serverConfig.webserver.audioPort + "."); // Spawn the child process - if(config.audioDeviceName.length > 2) { + if(serverConfig.audio.audioDevice.length > 2) { const childProcess = spawn(command, [ffmpegCommand], { shell: true }); // Handle the output of the child process (optional) diff --git a/stream/parser.js b/stream/parser.js new file mode 100644 index 0000000..6a88b7f --- /dev/null +++ b/stream/parser.js @@ -0,0 +1,107 @@ +'use strict'; + +const exec = require('child_process').exec; +const platform = process.platform; + +function parseAudioDevice(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + const ffmpegPath = options.ffmpegPath || 'ffmpeg'; + const callbackExists = typeof callback === 'function'; + + let inputDevice, prefix, audioSeparator, alternativeName, deviceParams; + switch (platform) { + case 'win32': + inputDevice = 'dshow'; + prefix = /\[dshow/; + audioSeparator = /DirectShow\saudio\sdevices/; + alternativeName = /Alternative\sname\s*?\"(.*?)\"/; + deviceParams = /\"(.*?)\"/; + break; + case 'darwin': + inputDevice = 'avfoundation'; + prefix = /^\[AVFoundation/; + audioSeparator = /AVFoundation\saudio\sdevices/; + deviceParams = /^\[AVFoundation.*?\]\s\[(\d*?)\]\s(.*)$/; + break; + case 'linux': + exec("cat /proc/asound/cards | sed -r 's/^ *([0-9]+) \\[(.*) *\\]: (.*)/hw:\\2/' | grep -E '^hw:'", (err, stdout) => { + audioDevices = stdout.trim().split('\n').map(device => ({ name: device })); + const result = { audioDevices }; + if (callbackExists) { + callback(result); + } else { + Promise.resolve(result); + } + }); + break; + } + + + const searchPrefix = (line) => (line.search(prefix) > -1); + const searchAudioSeparator = (line) => isVideo && (line.search(audioSeparator) > -1); + const searchAlternativeName = (line) => (platform === 'win32') && (line.search(/Alternative\sname/) > -1); + + let videoDevices = []; + let audioDevices = []; + let isVideo = true; + + const execute = (fulfill, reject) => { + exec(`${ffmpegPath} -f ${inputDevice} -list_devices true -i ""`, (err, stdout, stderr) => { + stderr.split("\n") + .filter(searchPrefix) + .forEach((line) => { + const deviceList = isVideo ? videoDevices : audioDevices; + if (searchAudioSeparator(line)) { + isVideo = false; + return; + } + if (searchAlternativeName(line)) { + const lastDevice = deviceList[deviceList.length - 1]; + lastDevice.alternativeName = line.match(alternativeName)[1]; + return; + } + const params = line.match(deviceParams); + if (params) { + let device; + switch (platform) { + case 'win32': + device = { + name: params[1] + }; + break; + case 'darwin': + device = { + id: parseInt(params[1]), + name: params[2] + }; + break; + case 'linux': + device = { + name: params[1] + }; + break; + } + deviceList.push(device); + } + }); + const result = { videoDevices, audioDevices }; + if (callbackExists) { + callback(result); + } else { + fulfill(result); + } + }); + }; + + if (callbackExists) { + execute(); + } else { + return new Promise(execute); + } +} + +module.exports = { parseAudioDevice }; \ No newline at end of file diff --git a/web/css/breadcrumbs.css b/web/css/breadcrumbs.css index b03403b..af2405e 100644 --- a/web/css/breadcrumbs.css +++ b/web/css/breadcrumbs.css @@ -1,3 +1,15 @@ +h1 { + color: var(--color-4); + font-size: 52px; + font-weight: 300; + margin-top: 0; + margin-bottom: 0; +} + +h1#tuner-name { + font-size: 32px; +} + h2 { color: var(--color-4); margin-bottom: 0; @@ -8,9 +20,22 @@ h3 { font-size: 22px; } +p#tuner-desc { + margin: 0; +} + +label { + font-size: 12px; + font-weight: bold; + text-transform: uppercase; + display: block; + text-align: left; + color: var(--color-4); +} + .canvas-container { width: 100%; - height: 200px; + height: 175px; } #data-ant { @@ -36,7 +61,7 @@ h3 { margin-right: 5px; } -#color-settings, #settings { +#settings, #back-btn { background: transparent; border: 0; color: white; @@ -53,12 +78,13 @@ h3 { cursor: pointer; } -#color-settings { - top: 96px; +#settings:hover, #back-btn:hover { + background: var(--color-3); } -#settings:hover, #color-settings:hover { - background: var(--color-3); +#back-btn { + left: 15px; + right: auto; } #af-list ul { @@ -79,6 +105,49 @@ h3 { display: none; } +.checkbox input[type="checkbox"] { + padding: 0; + height: initial; + width: initial; + margin-bottom: 0; + display: none; + cursor: pointer; + } + + .checkbox label { + position: relative; + cursor: pointer; + } + + .checkbox label:before { + content:''; + appearance: none; + -webkit-appearance: none; + background-color: transparent; + border: 2px solid var(--color-4); + padding: 10px; + display: inline-block; + position: relative; + vertical-align: middle; + cursor: pointer; + margin-right: 5px; + } + + .form-group input:checked + label:after { + content: '✓'; + display: block; + position: absolute; + top: 2px; + left: 6px; + width: 16px; + height: 16px; + } + + .tuner-info { + margin-top: 0px !important; + margin-bottom: 0px !important; + } + @media (max-width: 768px) { canvas, #flags-container { display: none; @@ -153,6 +222,9 @@ h3 { .button-ims { order: 3; } + .tuner-info { + margin-bottom: -60px !important; + } } @media only screen and (min-width: 960px) and (max-height: 860px) { @@ -165,4 +237,13 @@ h3 { .canvas-container { height: 120px; } + .tuner-info #tuner-name { + float: left; + font-size: 24px; + } + + .tuner-info #tuner-desc { + float: right; + text-align: right; + } } \ No newline at end of file diff --git a/web/css/buttons.css b/web/css/buttons.css index cbaaaa4..4f573a9 100644 --- a/web/css/buttons.css +++ b/web/css/buttons.css @@ -1,4 +1,4 @@ -button { +button, input[type="submit"] { width: 100%; height: 100%; border: 0; @@ -18,6 +18,18 @@ button:hover { opacity: 0.6; } +input[type="text"], textarea, input[type="password"] { + width: 300px; + min-height: 46px; + padding-left: 20px; + box-sizing: border-box; + border: 2px solid transparent; + outline: 0; + color: white; + background-color: var(--color-1); + font-family: 'Titillium Web', sans-serif; +} + #tune-buttons input[type="text"] { width: 50%; height: 100%; diff --git a/web/css/dropdown.css b/web/css/dropdown.css index b65bf8c..6b8a7a6 100644 --- a/web/css/dropdown.css +++ b/web/css/dropdown.css @@ -53,8 +53,7 @@ border: none; font-size: 16px; overflow: hidden; - opacity: 0; - visibility: hidden; + display: none; background: var(--color-main); color: var(--color-4); border: 1px solid var(--color-4); @@ -70,9 +69,9 @@ background: var(--color-4); } .dropdown.opened .options { - opacity: 1; - visibility: visible; + display:block; transform: translateY(0); + position:absolute; } .dropdown.opened::before { transform: rotate(-225deg); diff --git a/web/css/entry.css b/web/css/entry.css index c8e27c9..aecf56a 100644 --- a/web/css/entry.css +++ b/web/css/entry.css @@ -6,4 +6,5 @@ @import url("dropdown.css"); /* Custom dropdown menus */ @import url("panels.css"); /* Different panels and their sizes */ @import url("modal.css"); /* Modal window */ +@import url("setup.css"); /* Web setup interface */ @import url("helpers.css"); /* Stuff that is used often such as text changers etc */ \ No newline at end of file diff --git a/web/css/helpers.css b/web/css/helpers.css index 21eb5f5..2afe077 100644 --- a/web/css/helpers.css +++ b/web/css/helpers.css @@ -30,6 +30,10 @@ color: var(--color-4); } +.br-0 { + border-radius: 0px; +} + .br-5 { border-radius: 5px; } @@ -137,6 +141,14 @@ padding: 10px; } +.p-bottom-20 { + padding-bottom: 20px; +} + +.input-text { + background-color: var(--color-2) !important; +} + @media only screen and (max-width: 960px) { .text-medium-big { font-size: 32px; diff --git a/web/css/main.css b/web/css/main.css index 61c1bae..7dc33ad 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -55,6 +55,11 @@ body { width: 1180px; max-width: 1180px; } +#wrapper.setup-wrapper { + margin: auto; + position: static; + transform: none; +} @media (max-width: 1180px) { #wrapper { diff --git a/web/css/modal.css b/web/css/modal.css index fc21df7..adfa5c6 100644 --- a/web/css/modal.css +++ b/web/css/modal.css @@ -68,13 +68,6 @@ background: var(--color-5); } -.modal label { - font-size: 12px; - font-weight: bold; - text-transform: uppercase; - display: block; -} - @media only screen and (max-width: 768px) { .modal-content { min-width: 90% !important; diff --git a/web/css/panels.css b/web/css/panels.css index ca20d07..53571fb 100644 --- a/web/css/panels.css +++ b/web/css/panels.css @@ -17,6 +17,10 @@ width: 33%; } +.panel-50 { + width: 50%; +} + .panel-75 { width: 68%; } diff --git a/web/css/setup.css b/web/css/setup.css new file mode 100644 index 0000000..95d4d0d --- /dev/null +++ b/web/css/setup.css @@ -0,0 +1,42 @@ + +.setup-wrapper .form-group, .setup-wrapper textarea { + display: inline-block; + float: none; +} + +.setup-wrapper .form-group { + margin-right: 5px; + margin-left: 5px; +} + + +.setup-wrapper textarea { + width: 100%; + max-width: 768px; + background-color: var(--color-2); + height: 100px; + font-size: 14px; + padding-top: 10px; +} + +#map { + height:400px; + width:100%; + overflow: hidden; + max-width:800px; + margin: auto; + margin-bottom: 20px; +} + +.setup-wrapper h3 { + font-weight: 300; + margin: 8px; +} + +.w-150 { + width: 150px !important +} + +.w-100 { + width: 100px !important; +} \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index c86330d..0000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/images/flags-16.png b/web/images/flags-16.png index fab9711..7d26ac5 100644 Binary files a/web/images/flags-16.png and b/web/images/flags-16.png differ diff --git a/web/images/flags-47.png b/web/images/flags-47.png index 76ca657..2f99573 100644 Binary files a/web/images/flags-47.png and b/web/images/flags-47.png differ diff --git a/web/index.html b/web/index.ejs similarity index 88% rename from web/index.html rename to web/index.ejs index ce6effa..22230c4 100644 --- a/web/index.html +++ b/web/index.ejs @@ -1,7 +1,7 @@
-<%= tunerDesc %>
+ +