You've already forked fm-dx-webserver
mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-02-26 22:13:53 +01:00
ejs + webadmin + bugfixes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
node_modules/
|
||||
/example.js
|
||||
/userconfig.json
|
||||
/userconfig_backup.js
|
||||
/config.json
|
||||
23
README.md
23
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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { verboseMode } = require('./userconfig');
|
||||
const verboseMode = process.argv.includes('--debug');
|
||||
|
||||
const getCurrentTime = () => {
|
||||
const currentTime = new Date();
|
||||
|
||||
@@ -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)');
|
||||
|
||||
349
index.js
349
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
|
||||
}
|
||||
241
package-lock.json
generated
241
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
107
stream/parser.js
Normal file
107
stream/parser.js
Normal file
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
|
||||
@@ -55,6 +55,11 @@ body {
|
||||
width: 1180px;
|
||||
max-width: 1180px;
|
||||
}
|
||||
#wrapper.setup-wrapper {
|
||||
margin: auto;
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
#wrapper {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.panel-50 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.panel-75 {
|
||||
width: 68%;
|
||||
}
|
||||
|
||||
42
web/css/setup.css
Normal file
42
web/css/setup.css
Normal file
@@ -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;
|
||||
}
|
||||
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 46 KiB |
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FM-DX Webserver</title>
|
||||
<title>FM-DX Webserver [<%= tunerName %>]</title>
|
||||
<link href="css/entry.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/flags.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
|
||||
@@ -9,6 +9,11 @@
|
||||
<link rel="icon" type="image/png" href="favicon2.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<meta property="og:title" content="FM-DX WebServer">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="/favicon2.png">
|
||||
<meta property="og:description" content="This server us running the FM-DX Webserver software by Noobish.">
|
||||
|
||||
<!-- 3LAS Scripts for Audio streaming -->
|
||||
<script src="js/3las/util/3las.helpers.js"></script>
|
||||
<script src="js/3las/util/3las.logging.js"></script>
|
||||
@@ -44,6 +49,11 @@
|
||||
<body>
|
||||
<audio id="audioTag"></audio>
|
||||
<div id="wrapper">
|
||||
<div class="panel-100 no-bg tuner-info">
|
||||
<h1 id="tuner-name"><%= tunerName %> <% if (tunerLock) { %><i class="fa-solid fa-lock" title="Tuner is currently locked to admin."></i><% } %></h1>
|
||||
<p id="tuner-desc"><%= tunerDesc %></p>
|
||||
<div style="clear: both"></div>
|
||||
</div>
|
||||
<div class="canvas-container hide-phone">
|
||||
<canvas id="signal-canvas"></canvas>
|
||||
</div>
|
||||
@@ -187,17 +197,6 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="themes" style="margin-top: 50px;"><i class="fa-solid fa-palette"></i> Theme:</label>
|
||||
<!--<select name="themes" style="margin-bottom: 15px;" id="theme-selector">
|
||||
<option value="theme1">Monochrome</option>
|
||||
<option value="theme2">Red</option>
|
||||
<option value="theme3">Green</option>
|
||||
<option value="theme4">Cyan</option>
|
||||
<option value="theme5">Orange</option>
|
||||
<option value="theme6">Pink</option>
|
||||
<option value="theme7">Blurple</option>
|
||||
<option value="theme8">AMOLED</option>
|
||||
</select>-->
|
||||
|
||||
<div class="dropdown" id="theme-selector">
|
||||
<input type="text" placeholder="Theme" readonly />
|
||||
<ul class="options">
|
||||
@@ -215,13 +214,8 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="signal" style="margin-top: 50px;"><i class="fa-solid fa-signal"></i> Signal units:</label>
|
||||
<!--<select name="signal" style="margin-bottom: 15px;" id="signal-selector">
|
||||
<option value="dbf">dBf</option>
|
||||
<option value="dbuv">dBuV</option>
|
||||
<option value="dbm">dBm</option>
|
||||
</select>-->
|
||||
<div class="dropdown" id="signal-selector">
|
||||
<input type="text" placeholder="Theme" readonly />
|
||||
<input type="text" placeholder="Signal Units" readonly />
|
||||
<ul class="options">
|
||||
<li class="option" data-value="dbf">dBf</li>
|
||||
<li class="option" data-value="dbuv">dBuV</li>
|
||||
@@ -234,13 +228,27 @@
|
||||
<label for="themes"><i class="fa-solid fa-user"></i> Users online:</label>
|
||||
<span id="users-online" name="users-online">0</span>
|
||||
</div>
|
||||
|
||||
<% if (isAdminAuthenticated) { %>
|
||||
<p>You are logged in as an adminstrator. <a href="/setup">Setup</a> | <a class="logout-link" href="#">Logout</a></p>
|
||||
<% } else if (isTuneAuthenticated) { %>
|
||||
<p>You are logged in and can control the receiver. <a class="logout-link" href="#">Logout</a></p>
|
||||
<% } else { %>
|
||||
<form action="/login" method="post" id="login-form">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="submit" class="br-0 w-100" style="height: 44px">Login</button>
|
||||
</form>
|
||||
<% } %>
|
||||
<div id="login-message"></div>
|
||||
|
||||
<div class="flex-container flex-left text-left hover-brighten p-10 br-5" onclick="window.open('https://discord.com/invite/ZAVNdS74mC')">
|
||||
<i class="fa-brands fa-discord"></i> <span>Join our <strong>OpenRadio Discord</strong> community!</span>
|
||||
</div>
|
||||
<div class="flex-container flex-left text-left bottom-20 hover-brighten p-10 br-5" onclick="window.open('https://buymeacoffee.com/noobish')">
|
||||
<i class="fa-solid fa-hand-holding-medical"></i> <span><strong>Support</strong> the developer!</span>
|
||||
</div>
|
||||
<p class="text-small">FM-DX WebServer by <a href="https://noobish.eu" target="_blank">Noobish</a> & the OpenRadio community.</p>
|
||||
<p class="text-small">FM-DX WebServer <span style="color: var(--color-3);">v1.0.0 [4/2/2024]</span> by <a href="https://noobish.eu" target="_blank">Noobish</a> & the OpenRadio community.</p>
|
||||
<p class="text-small bottom-50">This app works thanks to these amazing projects: <br>
|
||||
<span class="text-smaller">- librdsparser by <a href="https://fmdx.pl" target="_blank">Konrad Kosmatka</a></span><br>
|
||||
<span class="text-smaller">- 3LAS by <a href="https://github.com/JoJoBond/3LAS" target="_blank">JoJoBond</a></span><br>
|
||||
@@ -127,11 +127,8 @@ function getInitialSettings() {
|
||||
// Use the received data (data.qthLatitude, data.qthLongitude) as needed
|
||||
localStorage.setItem('qthLatitude', data.qthLatitude);
|
||||
localStorage.setItem('qthLongitude', data.qthLongitude);
|
||||
localStorage.setItem('webServerName', data.webServerName);
|
||||
localStorage.setItem('audioPort', data.audioPort);
|
||||
localStorage.setItem('streamEnabled', data.streamEnabled);
|
||||
|
||||
document.title = 'FM-DX Webserver [' + data.webServerName + ']';
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
@@ -47,6 +47,63 @@
|
||||
signalSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
|
||||
localStorage.setItem('signalUnit', selectedSignalUnit);
|
||||
});
|
||||
|
||||
$('#login-form').submit(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Perform an AJAX request to the /login endpoint
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/login',
|
||||
data: $(this).serialize(),
|
||||
success: function (data) {
|
||||
// Update the content on the page with the message from the response
|
||||
$('#login-message').text(data.message);
|
||||
setTimeout(function () {
|
||||
location.reload(true);
|
||||
}, 1750);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// Handle error response
|
||||
if (xhr.status === 403) {
|
||||
// Update the content on the page with the message from the error response
|
||||
$('#login-message').text(xhr.responseJSON.message);
|
||||
} else {
|
||||
// Handle other types of errors if needed
|
||||
console.error('Error:', status, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Assuming you have an anchor tag with id 'logout-link'
|
||||
$('.logout-link').click(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Perform an AJAX request to the /logout endpoint
|
||||
$.ajax({
|
||||
type: 'GET', // Assuming the logout is a GET request, adjust accordingly
|
||||
url: '/logout',
|
||||
success: function (data) {
|
||||
// Update the content on the page with the message from the response
|
||||
$('#login-message').text(data.message);
|
||||
setTimeout(function () {
|
||||
location.reload(true);
|
||||
}, 1750);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// Handle error response
|
||||
if (xhr.status === 403) {
|
||||
// Update the content on the page with the message from the error response
|
||||
$('#login-message').text(xhr.responseJSON.message);
|
||||
} else {
|
||||
// Handle other types of errors if needed
|
||||
console.error('Error:', status, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
243
web/js/setup.js
Normal file
243
web/js/setup.js
Normal file
@@ -0,0 +1,243 @@
|
||||
var map;
|
||||
var pin;
|
||||
var tilesURL='https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}.png';
|
||||
var mapAttrib='© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, Tiles by <a href="http://stadiamaps.com/" target="_blank">Stadiamaps</a>';
|
||||
var hostParts = window.location.host.split(':');
|
||||
var hostname = hostParts[0]; // Extract the hostname
|
||||
var port = hostParts[1] || '80'; // Extract the port or use a default (e.g., 8080)
|
||||
var serverAddress = 'http://' + hostname + ':' + port;
|
||||
|
||||
// add map container
|
||||
|
||||
$(document).ready(function() {
|
||||
MapCreate();
|
||||
fetchData();
|
||||
|
||||
|
||||
map.on('click', function(ev) {
|
||||
$('#lat').val((ev.latlng.lat).toFixed(6));
|
||||
$('#lng').val((ev.latlng.lng).toFixed(6));
|
||||
|
||||
if (typeof pin == "object") {
|
||||
pin.setLatLng(ev.latlng);
|
||||
} else {
|
||||
pin = L.marker(ev.latlng,{ riseOnHover:true,draggable:true });
|
||||
pin.addTo(map);
|
||||
pin.on('drag',function(ev) {
|
||||
$('#lat').val((ev.latlng.lat).toFixed(6));
|
||||
$('#lng').val((ev.latlng.lng).toFixed(6));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#login-form').submit(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Perform an AJAX request to the /login endpoint
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/login',
|
||||
data: $(this).serialize(),
|
||||
success: function (data) {
|
||||
// Update the content on the page with the message from the response
|
||||
$('#login-message').text(data.message);
|
||||
setTimeout(function () {
|
||||
location.reload(true);
|
||||
}, 1750);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// Handle error response
|
||||
if (xhr.status === 403) {
|
||||
// Update the content on the page with the message from the error response
|
||||
$('#login-message').text(xhr.responseJSON.message);
|
||||
} else {
|
||||
// Handle other types of errors if needed
|
||||
console.error('Error:', status, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Assuming you have an anchor tag with id 'logout-link'
|
||||
$('.logout-link').click(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Perform an AJAX request to the /logout endpoint
|
||||
$.ajax({
|
||||
type: 'GET', // Assuming the logout is a GET request, adjust accordingly
|
||||
url: '/logout',
|
||||
success: function (data) {
|
||||
// Update the content on the page with the message from the response
|
||||
$('#login-message').text(data.message);
|
||||
setTimeout(function () {
|
||||
location.reload(true);
|
||||
}, 1750);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
// Handle error response
|
||||
if (xhr.status === 403) {
|
||||
// Update the content on the page with the message from the error response
|
||||
$('#login-message').text(xhr.responseJSON.message);
|
||||
} else {
|
||||
// Handle other types of errors if needed
|
||||
console.error('Error:', status, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function MapCreate() {
|
||||
// create map instance
|
||||
if (!(typeof map == "object")) {
|
||||
map = L.map('map', {
|
||||
center: [40,0],
|
||||
zoom: 3
|
||||
});
|
||||
}
|
||||
else {
|
||||
map.setZoom(3).panTo([40,0]);
|
||||
}
|
||||
// create the tile layer with correct attribution
|
||||
L.tileLayer(tilesURL, {
|
||||
attribution: mapAttrib,
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
function fetchData() {
|
||||
// Make a GET request to retrieve the data.json file
|
||||
fetch(serverAddress + "/getData")
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Save the received JSON data to a local variable (you may want to handle this differently on the client)
|
||||
console.log('Received data:', data);
|
||||
|
||||
$('#webserver-ip').val(data.webserver.webserverIp);
|
||||
$('#webserver-port').val(data.webserver.webserverPort);
|
||||
$('#audio-port').val(data.webserver.audioPort);
|
||||
|
||||
$('#xdrd-ip').val(data.xdrd.xdrdIp);
|
||||
$('#xdrd-port').val(data.xdrd.xdrdPort);
|
||||
$('#xdrd-password').val(data.xdrd.xdrdPassword);
|
||||
|
||||
$('#audio-devices').val(data.audio.audioDevice);
|
||||
$('#audio-channels').val(data.audio.audioChannels);
|
||||
$('#audio-quality').val(data.audio.audioBitrate);
|
||||
|
||||
$('#webserver-name').val(data.identification.tunerName);
|
||||
$('#webserver-desc').val(data.identification.tunerDesc);
|
||||
$('#lat').val(data.identification.lat);
|
||||
$('#lng').val(data.identification.lon);
|
||||
|
||||
$('#tune-pass').val(data.password.tunePass);
|
||||
$('#admin-pass').val(data.password.adminPass);
|
||||
|
||||
$("#tuner-public").prop("checked", data.publicTuner);
|
||||
$("#tuner-lock").prop("checked", data.lockToAdmin);
|
||||
|
||||
// Check if latitude and longitude are present in the data
|
||||
if (data.identification.lat && data.identification.lon) {
|
||||
// Set the map's center to the received coordinates
|
||||
map.setView([data.identification.lat, data.identification.lon], 13);
|
||||
|
||||
// Add a pin to the map
|
||||
if (typeof pin == "object") {
|
||||
pin.setLatLng([data.identification.lat, data.identification.lon]);
|
||||
} else {
|
||||
pin = L.marker([data.identification.lat, data.identification.lon], { riseOnHover:true, draggable:true });
|
||||
pin.addTo(map);
|
||||
pin.on('drag',function(ev) {
|
||||
$('#lat').val((ev.latlng.lat).toFixed(6));
|
||||
$('#lng').val((ev.latlng.lng).toFixed(6));
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function submitData() {
|
||||
const webserverIp = $('#webserver-ip').val() || '0.0.0.0';
|
||||
const webserverPort = $('#webserver-port').val() || '8080';
|
||||
const audioPort = $('#audio-port').val() || '8081';
|
||||
|
||||
const xdrdIp = $('#xdrd-ip').val() || '127.0.0.1';
|
||||
const xdrdPort = $('#xdrd-port').val() || '7373';
|
||||
const xdrdPassword = $('#xdrd-password').val() || 'password';
|
||||
|
||||
const audioDevice = $('#audio-devices').val() || 'Microphone (High Definition Audio Device)';
|
||||
const audioChannels = ($('.options .option').filter(function() {
|
||||
return $(this).text() === $('#audio-channels').val();
|
||||
}).data('value') || 2);
|
||||
const audioBitrate = $('#audio-quality').val() || '192k';
|
||||
|
||||
const tunerName = $('#webserver-name').val() || 'FM Tuner';
|
||||
const tunerDesc = $('#webserver-desc').val() || 'Default FM tuner description';
|
||||
const lat = $('#lat').val();
|
||||
const lon = $('#lng').val();
|
||||
|
||||
const tunePass = $('#tune-pass').val();
|
||||
const adminPass = $('#admin-pass').val();
|
||||
|
||||
const publicTuner = $("#tuner-public").is(":checked");
|
||||
const lockToAdmin = $("#tuner-lock").is(":checked");
|
||||
|
||||
const data = {
|
||||
webserver: {
|
||||
webserverIp,
|
||||
webserverPort,
|
||||
audioPort
|
||||
},
|
||||
xdrd: {
|
||||
xdrdIp,
|
||||
xdrdPort,
|
||||
xdrdPassword
|
||||
},
|
||||
audio: {
|
||||
audioDevice,
|
||||
audioChannels,
|
||||
audioBitrate,
|
||||
},
|
||||
identification: {
|
||||
tunerName,
|
||||
tunerDesc,
|
||||
lat,
|
||||
lon,
|
||||
},
|
||||
password: {
|
||||
tunePass,
|
||||
adminPass,
|
||||
},
|
||||
publicTuner,
|
||||
lockToAdmin
|
||||
};
|
||||
|
||||
|
||||
if(adminPass.length < 1) {
|
||||
alert('You need to fill in the admin password before continuing further.');
|
||||
return;
|
||||
}
|
||||
// Send data to the server using jQuery
|
||||
$.ajax({
|
||||
url: serverAddress + '/saveData',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function (message) {
|
||||
alert(message);
|
||||
},
|
||||
error: function (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
175
web/setup.ejs
Normal file
175
web/setup.ejs
Normal file
@@ -0,0 +1,175 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FM-DX Webserver</title>
|
||||
<link href="css/entry.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/flags.min.css" type="text/css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||||
<link rel="icon" type="image/png" href="favicon2.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper" class="setup-wrapper">
|
||||
<% if (isAdminAuthenticated) { %>
|
||||
<div class="panel-100 no-bg">
|
||||
<h1>FM-DX WebServer</h1>
|
||||
<h2>[ADMIN PANEL]</h2>
|
||||
<p>This web setup allows you to set up your entire tuner. <br>Some settings will only change after a server restart.</p>
|
||||
<p>In case you are setting up the webserver for the first time, we already filled fail-safe defaults for you.</p>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="panel-50" style="min-height: 120px;margin-bottom: 0;">
|
||||
<h2>BASIC SETTINGS</h2>
|
||||
<h3>Connection to xdrd:</h3>
|
||||
<div class="form-group">
|
||||
<label for="xdrd-ip">xdrd ip address:</label>
|
||||
<input class="input-text w-150" type="text" name="xdrd-ip" id="xdrd-ip" placeholder="127.0.0.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="xdrd-port">xdrd port:</label>
|
||||
<input class="input-text w-100" type="text" name="xdrd-port" id="xdrd-port" placeholder="7373">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="xdrd-password">xdrd server password:</label>
|
||||
<input class="input-text w-150" type="text" name="xdrd-password" id="xdrd-password">
|
||||
</div>
|
||||
<br>
|
||||
<h3>Webserver connection:</h3>
|
||||
<div class="form-group">
|
||||
<label for="webserver-ip">Webserver IP:</label>
|
||||
<input class="input-text w-150" type="text" name="webserver-ip" id="webserver-ip" placeholder="0.0.0.0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webserver-port">Webserver port:</label>
|
||||
<input class="input-text w-100" type="text" name="webserver-port" id="webserver-port" placeholder="8080">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="audio-port">Audio port:</label>
|
||||
<input class="input-text w-150" type="text" name="audio-port" id="audio-port" placeholder="8081">
|
||||
</div>
|
||||
<br>
|
||||
<!--tune password, public/private tuner, admin password, verbose mode-->
|
||||
</div>
|
||||
<div class="panel-50" style="min-height: 120px;margin-bottom: 0;">
|
||||
<h2>AUDIO SETTINGS</h2>
|
||||
|
||||
<div class="panel-100 p-bottom-20">
|
||||
<div class="form-group">
|
||||
<label for="audio-devices"><i class="fa-solid fa-headphones"></i> STREAM AUDIO FROM:</label>
|
||||
<div class="dropdown" style="width: 300px;">
|
||||
<input id="audio-devices" type="text" name="audio-devices" placeholder="Choose your audio device" readonly />
|
||||
<ul class="options" id="deviceList">
|
||||
<% videoDevices.forEach(device => { %>
|
||||
<li data-value="<%= device.name %>" class="option"><%= device.name %></li>
|
||||
<% }); %>
|
||||
<% audioDevices.forEach(device => { %>
|
||||
<li data-value="<%= device.name %>" class="option"><%= device.name %></li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="audio-devices"><i class="fa-solid fa-microphone-lines"></i> AUDIO CHANNELS:</label>
|
||||
<div class="dropdown" style="width: 300px;">
|
||||
<input id="audio-channels" type="text" name="audio-channels" placeholder="Stereo" readonly />
|
||||
<ul class="options">
|
||||
<li data-value="2" class="option">Stereo</li>
|
||||
<li data-value="1" class="option">Mono</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="audio-quality"><i class="fa-solid fa-wave-square"></i> AUDIO QUALITY:</label>
|
||||
<div class="dropdown" style="width: 300px;">
|
||||
<input id="audio-quality" type="text" name="audio-quality" placeholder="128k (saves bandwidth)" readonly />
|
||||
<ul class="options">
|
||||
<li data-value="128k" class="option">128k (saves bandwidth)</li>
|
||||
<li data-value="192k" class="option">192k (higher quality)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container bottom-20">
|
||||
<div class="panel-100" style="padding-bottom: 20px;">
|
||||
<h2>TUNER IDENTIFICATION INFO</h2>
|
||||
|
||||
<div class="panel-100" style="padding-left: 20px; padding-right: 20px;">
|
||||
<label for="webserver-name" style="width: 100%;max-width: 768px; margin:auto;">Webserver name:</label>
|
||||
<input style="width: 100%; max-width: 768px;" class="input-text" type="text" name="webserver-name" id="webserver-name" placeholder="Fill your server name here.">
|
||||
<br>
|
||||
<label for="webserver-desc" style="width: 100%;max-width: 768px; margin: auto;">Webserver description:</label>
|
||||
<textarea id="webserver-desc" name="webserver-desc" placeholder="Fill the server description here. You can put useful info here such as your antenna setup."></textarea>
|
||||
</div>
|
||||
|
||||
<h3>Tuner location:</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="lat">Latitude:</label>
|
||||
<input class="input-text" type="text" name="lat" id="lat">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="lng">Longitude:</label>
|
||||
<input class="input-text" type="text" name="lng" id="lng">
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-33">
|
||||
<h2>MAINTENANCE</h2>
|
||||
<div class="form-group checkbox">
|
||||
<input type="checkbox" id="tuner-public">
|
||||
<label for="tuner-public">Public tuner</label>
|
||||
</div><br>
|
||||
<div class="form-group checkbox">
|
||||
<input type="checkbox" id="tuner-lock">
|
||||
<label for="tuner-lock">Lock to admin</label>
|
||||
</div><br>
|
||||
<div class="form-group">
|
||||
<label for="tune-pass">Tune password:</label>
|
||||
<input class="input-text w-150" type="text" name="tune-pass" id="tune-pass">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 40px;">
|
||||
<label for="admin-pass">Admin setup password:</label>
|
||||
<input class="input-text w-150" type="text" name="admin-pass" id="admin-pass">
|
||||
</div><br>
|
||||
<button style="height:48px; width: 200px;margin-bottom:20px;" onclick="submitData();">Save settings</button>
|
||||
<button style="height: 48px; width: 200px;background:var(--color-3)" class="logout-link">Logout</button>
|
||||
<div id="login-message"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-100 no-bg">
|
||||
<p>Feel free to contact us on <a href="https://discord.gg/ZAVNdS74mC" target="_blank"><strong><i class="fa-brands fa-discord"></i> Discord</strong></a> for community support.</p>
|
||||
</div>
|
||||
<button onclick="document.location.href='/'" id="back-btn" aria-label="Go back to tuning"><i class="fa-solid fa-arrow-left"></i></button>
|
||||
<% } else { %>
|
||||
<div class="panel-100 no-bg">
|
||||
<h1>FM-DX WebServer</h1>
|
||||
<h2>[ADMIN PANEL]</h2>
|
||||
<p>You are currently not logged in as an administrator and therefore can't change the settings.</p>
|
||||
<p>Please login below.</p>
|
||||
</div>
|
||||
<div class="panel-100 p-bottom-20">
|
||||
<h2>LOGIN</h2>
|
||||
<form action="/login" method="post" id="login-form">
|
||||
<input style="background-color: var(--color-2);" type="password" id="password" name="password" required>
|
||||
<button type="submit" class="br-0 w-100" style="height: 44px;">Login</button>
|
||||
</form>
|
||||
<div id="login-message"></div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<script src="js/settings.js"></script>
|
||||
<script src="js/dropdown.js"></script>
|
||||
<script src="js/setup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user