mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-07-30 00:39:16 +02:00
Compare commits
22
Commits
94710dbd9e
...
0c723702f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c723702f2
|
||
|
|
96a78958dc
|
||
|
|
a4a6dc7370
|
||
|
|
8f977c912d
|
||
|
|
96fec88b52
|
||
|
|
98b48d5811
|
||
|
|
aa9f7a4cbd
|
||
|
|
772f8c9cc1
|
||
|
|
cdd8a1008f
|
||
|
|
c1d044435f
|
||
|
|
b2e9342f2e
|
||
|
|
4c5ab5aaef
|
||
|
|
3cda783deb
|
||
|
|
d4c13dfc28
|
||
|
|
dab1a35abd
|
||
|
|
ffd7b9cf8b
|
||
|
|
8b5b7965b4
|
||
|
|
5968cd54c3
|
||
|
|
537a680073
|
||
|
|
b462a0640c
|
||
|
|
af9428ccf9
|
||
|
|
28f5796ee5
|
@@ -1,10 +1 @@
|
||||
require('./server/index.js');
|
||||
|
||||
/**
|
||||
* FM-DX Webserver
|
||||
*
|
||||
* Github repo: https://github.com/KubaPro010/fm-dx-webserver
|
||||
* Server files: /server
|
||||
* Client files (web): /web
|
||||
* Plugin files: /plugins
|
||||
*/
|
||||
require('./server/index.js');
|
||||
+4
-6
@@ -19,11 +19,6 @@ function createChatServer(storage) {
|
||||
const clientIp = helpers.getIpAddress(request);
|
||||
const userCommandHistory = {};
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send chat history safely
|
||||
storage.chatHistory.forEach((message) => {
|
||||
const historyMessage = { ...message, history: true };
|
||||
@@ -77,7 +72,10 @@ function createChatServer(storage) {
|
||||
const now = new Date();
|
||||
messageData.time = String(now.getHours()).padStart(2, '0') + ":" + String(now.getMinutes()).padStart(2, '0');
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.session?.isAdminAuthenticated === true) messageData.admin = true;
|
||||
if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32);
|
||||
|
||||
+31
-27
@@ -7,7 +7,7 @@ const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
// File Imports
|
||||
const { parseAudioDevice } = require('./stream/parser');
|
||||
const parseAudioDevice = require('./stream/parser');
|
||||
const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config');
|
||||
const helpers = require('./helpers');
|
||||
const storage = require('./storage');
|
||||
@@ -22,9 +22,9 @@ router.get('/', (req, res) => {
|
||||
let requestIp = helpers.getIpAddress(req);
|
||||
|
||||
const ipList = (requestIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them (No we don't)
|
||||
|
||||
|
||||
const banEntry = serverConfig.webserver.banlist.find(banEntry => ipList.includes(banEntry[0]));
|
||||
|
||||
|
||||
if (banEntry) {
|
||||
const reason = banEntry[3];
|
||||
res.render('403', { reason });
|
||||
@@ -36,14 +36,14 @@ router.get('/', (req, res) => {
|
||||
|
||||
if (configExists() === false) {
|
||||
let serialPorts;
|
||||
|
||||
|
||||
SerialPort.list()
|
||||
.then((deviceList) => {
|
||||
serialPorts = deviceList.map(port => ({
|
||||
path: port.path,
|
||||
friendlyName: port.friendlyName,
|
||||
}));
|
||||
|
||||
|
||||
parseAudioDevice((result) => {
|
||||
res.render('wizard', { // Magical utility wizard
|
||||
isAdminAuthenticated: true,
|
||||
@@ -91,9 +91,13 @@ router.get('/403', (req, res) => {
|
||||
res.render('403', { reason });
|
||||
})
|
||||
|
||||
router.get('/audioonly', (req, res) => {
|
||||
res.render('audioonly');
|
||||
})
|
||||
|
||||
router.get('/wizard', (req, res) => {
|
||||
let serialPorts;
|
||||
|
||||
|
||||
if(!req.session.isAdminAuthenticated) {
|
||||
res.render('login');
|
||||
return;
|
||||
@@ -105,7 +109,7 @@ router.get('/wizard', (req, res) => {
|
||||
path: port.path,
|
||||
friendlyName: port.friendlyName,
|
||||
}));
|
||||
|
||||
|
||||
parseAudioDevice((result) => {
|
||||
res.render('wizard', {
|
||||
isAdminAuthenticated: req.session.isAdminAuthenticated,
|
||||
@@ -121,9 +125,9 @@ router.get('/wizard', (req, res) => {
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
router.get('/setup', (req, res) => {
|
||||
let serialPorts;
|
||||
let serialPorts;
|
||||
function loadConfig() {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configFileContents = fs.readFileSync(configPath, 'utf8');
|
||||
@@ -136,18 +140,18 @@ router.get('/setup', (req, res) => {
|
||||
res.render('login');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SerialPort.list()
|
||||
.then((deviceList) => {
|
||||
serialPorts = deviceList.map(port => ({
|
||||
path: port.path,
|
||||
friendlyName: port.friendlyName,
|
||||
}));
|
||||
|
||||
|
||||
parseAudioDevice((result) => {
|
||||
const processUptimeInSeconds = Math.floor(process.uptime());
|
||||
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
|
||||
|
||||
|
||||
const updatedConfig = loadConfig(); // Reload the config every time
|
||||
res.render('setup', {
|
||||
isAdminAuthenticated: req.session.isAdminAuthenticated,
|
||||
@@ -171,9 +175,9 @@ router.get('/setup', (req, res) => {
|
||||
}))
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.get('/rds', (req, res) => {
|
||||
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
|
||||
@@ -197,7 +201,7 @@ router.get('/api', (req, res) => {
|
||||
|
||||
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
|
||||
const MAX_ATTEMPTS = 10;
|
||||
const WINDOW_MS = 15 * 60 * 1000;
|
||||
const WINDOW_MS = 15 * 60 * 1000;
|
||||
|
||||
const authenticate = (req, res, next) => {
|
||||
const ip = helpers.getIpAddress(req);
|
||||
@@ -305,7 +309,7 @@ router.post('/saveData', (req, res) => {
|
||||
if(req.session.isAdminAuthenticated || !configExists()) {
|
||||
configUpdate(data);
|
||||
fmdxList.update();
|
||||
|
||||
|
||||
if(!configExists()) firstSetup = true;
|
||||
logInfo('Server config changed successfully.');
|
||||
if(firstSetup === true) res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
|
||||
@@ -313,9 +317,9 @@ router.post('/saveData', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getData', (req, res) => {
|
||||
router.get('/getData', (req, res) => {
|
||||
if (configExists() === false) res.json(serverConfig);
|
||||
|
||||
|
||||
if(req.session.isAdminAuthenticated) {
|
||||
// Check if the file exists
|
||||
fs.access(configPath, fs.constants.F_OK, (err) => {
|
||||
@@ -360,7 +364,7 @@ router.get('/server_time', (req, res) => {
|
||||
|
||||
router.get('/ping', (req, res) => {
|
||||
res.send('pong');
|
||||
});
|
||||
});
|
||||
|
||||
const logHistory = {};
|
||||
|
||||
@@ -368,27 +372,27 @@ const logHistory = {};
|
||||
function canLog(id) {
|
||||
const now = Date.now();
|
||||
const sixtyMinutes = 60 * 60 * 1000; // 60 minutes in milliseconds
|
||||
|
||||
|
||||
// Remove expired entries
|
||||
for (const [entryId, timestamp] of Object.entries(logHistory)) {
|
||||
if ((now - timestamp) >= sixtyMinutes) {
|
||||
delete logHistory[entryId];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed
|
||||
logHistory[id] = now; // Update with the current timestamp
|
||||
return true;
|
||||
}
|
||||
|
||||
router.get('/log_fmlist', (req, res) => {
|
||||
if (dataHandler.dataToSend.txInfo.tx.length === 0) {
|
||||
res.status(500).send('No suitable transmitter to log.');
|
||||
if (serverConfig.extras.fmlistIntegration === false || (serverConfig.extras.fmlistAdminOnly && !req.session.isTuneAuthenticated)) {
|
||||
res.status(500).send('FMLIST Integration is not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverConfig.extras.fmlistIntegration === false || (serverConfig.extras.fmlistAdminOnly && !req.session.isTuneAuthenticated)) {
|
||||
res.status(500).send('FMLIST Integration is not available.');
|
||||
if (dataHandler.dataToSend.txInfo.tx.length === 0) {
|
||||
res.status(500).send('No suitable transmitter to log.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -468,7 +472,7 @@ router.get('/tunnelservers', async (req, res) => {
|
||||
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" },
|
||||
{ value: "pldx", host: "pldx.duckdns.org", label: "Poland (k201)" },
|
||||
];
|
||||
|
||||
|
||||
const results = await Promise.all(
|
||||
servers.map(async s => {
|
||||
const latency = await helpers.checkLatency(s.host);
|
||||
@@ -478,7 +482,7 @@ router.get('/tunnelservers', async (req, res) => {
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
|
||||
+21
-45
@@ -42,7 +42,7 @@ const plugins = findServerFiles(serverConfig.plugins);
|
||||
if (plugins.length > 0) {
|
||||
setTimeout(() => {
|
||||
startPluginsWithDelay(plugins, 3000); // Start plugins with 3 seconds interval
|
||||
}, 3000); // Initial delay of 3 seconds for the first plugin
|
||||
}, 4000); // Initial delay of 4 seconds for the first plugin
|
||||
}
|
||||
|
||||
const terminalWidth = readline.createInterface({input: process.stdin, output: process.stdout}).output.columns;
|
||||
@@ -52,8 +52,7 @@ console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org + KubaPro010\x1b[0m');
|
||||
console.log("v" + pjson.version)
|
||||
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
|
||||
|
||||
const audioWss = require('./stream/ws.js');
|
||||
storage.websocket_delegation.set("/audio", audioWss);
|
||||
require('./stream/ws.js');
|
||||
require('./stream/index');
|
||||
require('./plugins');
|
||||
|
||||
@@ -97,16 +96,13 @@ setInterval(() => {
|
||||
function connectToSerial() {
|
||||
if (serverConfig.xdrd.wirelessConnection === true) return;
|
||||
|
||||
// Configure the SerialPort with DTR and RTS options
|
||||
serialport = new SerialPort({
|
||||
path: serverConfig.xdrd.comPort,
|
||||
baudRate: 115200,
|
||||
autoOpen: false, // Prevents automatic opening
|
||||
dtr: false, // Disable DTR
|
||||
rts: false // Disable RTS
|
||||
autoOpen: false,
|
||||
dtr: false, rts: false
|
||||
});
|
||||
|
||||
// Open the port manually after configuring DTR and RTS
|
||||
serialport.open((err) => {
|
||||
if (err) {
|
||||
logError('Error opening port: ' + err.message);
|
||||
@@ -121,7 +117,7 @@ function connectToSerial() {
|
||||
pluginsApi.setOutput(serialport);
|
||||
setTimeout(() => {
|
||||
serialport.write('x\n');
|
||||
}, 3000);
|
||||
}, 2500);
|
||||
|
||||
setTimeout(() => {
|
||||
serialport.write('Q0\n');
|
||||
@@ -141,13 +137,8 @@ function connectToSerial() {
|
||||
serialport.write('F-1\n');
|
||||
serialport.write('W0\n');
|
||||
serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n');
|
||||
// cEQ and iMS combinations
|
||||
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") serialport.write("G00\n"); // Both Disabled
|
||||
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") serialport.write(`G10\n`);
|
||||
else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") serialport.write(`G01\n`);
|
||||
else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") serialport.write("G11\n"); // Both Enabled
|
||||
// Handle stereo mode
|
||||
if (serverConfig.stereoStartup === "1") serialport.write("B1\n"); // Mono
|
||||
serialport.write(`G${serverConfig.ceqStartup}${serverConfig.imsStartup}\n`);
|
||||
serialport.write(`B${serverConfig.stereoStartup}\n`); // Mono
|
||||
serverConfig.audio.startupVolume
|
||||
? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
|
||||
: serialport.write('Y100\n');
|
||||
@@ -223,11 +214,10 @@ client.on('data', (data) => {
|
||||
authFlags.authMsg = true;
|
||||
logInfo('Authentication with xdrd successful.');
|
||||
} else if (line.startsWith('G')) {
|
||||
const value = line.substring(1);
|
||||
dataHandler.initialData.eq = value.charAt(0);
|
||||
dataHandler.dataToSend.eq = value.charAt(0);
|
||||
dataHandler.initialData.ims = value.charAt(1);
|
||||
dataHandler.dataToSend.ims = value.charAt(1);
|
||||
dataHandler.initialData.eq = line.charAt(1);
|
||||
dataHandler.dataToSend.eq = line.charAt(1);
|
||||
dataHandler.initialData.ims = line.charAt(2);
|
||||
dataHandler.dataToSend.ims = line.charAt(2);
|
||||
} else if (line.startsWith('Z')) {
|
||||
let modifiedLine = line.slice(1);
|
||||
dataHandler.initialData.ant = modifiedLine;
|
||||
@@ -306,13 +296,6 @@ wss.on('connection', (ws, request) => {
|
||||
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
|
||||
let clientIp = helpers.getIpAddress(request);
|
||||
const userCommandHistory = {};
|
||||
|
||||
if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientIp && clientIp.includes(',')) clientIp = clientIp.split(',')[0].trim();
|
||||
|
||||
// Per-IP limit connection open
|
||||
if (clientIp) {
|
||||
@@ -336,8 +319,8 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
if (clientIp !== '127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers++;
|
||||
}
|
||||
@@ -409,7 +392,6 @@ wss.on('connection', (ws, request) => {
|
||||
const isLocalIp = (
|
||||
clientIp === '127.0.0.1' ||
|
||||
clientIp === '::1' ||
|
||||
clientIp === '::ffff:127.0.0.1' ||
|
||||
clientIp.startsWith('192.168.') ||
|
||||
clientIp.startsWith('10.') ||
|
||||
clientIp.startsWith('172.16.')
|
||||
@@ -420,8 +402,8 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
if (clientIp !== '::ffff:127.0.0.1' ||
|
||||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
|
||||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
|
||||
currentUsers--;
|
||||
}
|
||||
@@ -490,10 +472,6 @@ wss.on('connection', (ws, request) => {
|
||||
pluginsWss.on('connection', (ws, request) => {
|
||||
const clientIp = helpers.getIpAddress(request);
|
||||
const userCommandHistory = {};
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
// Anti-spam tracking for each client
|
||||
const userCommands = {};
|
||||
let lastWarn = { time: 0 };
|
||||
@@ -504,7 +482,7 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
|
||||
try {
|
||||
let messageData = JSON.parse(message); // Attempt to parse the JSON
|
||||
|
||||
|
||||
if (messageData.type === "GPS" && messageData.value) {
|
||||
const gpsData = messageData.value;
|
||||
const { status, time, lat, lon, alt, mode } = gpsData;
|
||||
@@ -515,7 +493,7 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
|
||||
// Broadcast the message to all other clients
|
||||
pluginsWss.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) client.send(message); // Send the message to all clients
|
||||
@@ -527,14 +505,13 @@ pluginsWss.on('connection', (ws, request) => {
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
const clientIp = helpers.getIpAddress(request);
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
httpServer.on('upgrade', (request, socket, head) => {
|
||||
if (serverConfig.webserver.banlist?.includes(helpers.getIpAddress(request))) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const upgradeWss = storage.websocket_delegation.get(request.url);
|
||||
const upgradeWss = storage.websocket_delegation.get(request.url);
|
||||
if(upgradeWss) {
|
||||
sessionMiddleware(request, {}, () => {
|
||||
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
|
||||
@@ -564,5 +541,4 @@ const startServer = (address) => {
|
||||
|
||||
startServer(ipv4Address);
|
||||
tunnel.connect();
|
||||
fmdxList.update();
|
||||
module.exports = { wss, pluginsWss, httpServer, serverConfig };
|
||||
fmdxList.update();
|
||||
+52
-29
@@ -152,11 +152,11 @@ class RDSDecoder {
|
||||
clear() {
|
||||
this.data.pi = '?';
|
||||
this.ps = Array(8).fill(' ');
|
||||
this.ps_errors = Array(8).fill("0");
|
||||
this.ps_errors = Array(8).fill("10");
|
||||
this.rt0 = Array(64).fill(' ');
|
||||
this.rt0_errors = Array(64).fill("0");
|
||||
this.rt0_errors = Array(64).fill("10");
|
||||
this.rt1 = Array(64).fill(' ');
|
||||
this.rt1_errors = Array(64).fill("0");
|
||||
this.rt1_errors = Array(64).fill("10");
|
||||
this.data.ps = '';
|
||||
this.data.rt1 = '';
|
||||
this.data.rt0 = '';
|
||||
@@ -168,10 +168,12 @@ class RDSDecoder {
|
||||
this.rt1_to_clear = false;
|
||||
this.rt0_to_clear = false;
|
||||
this.data.ecc = null;
|
||||
this.ecc_error = 3;
|
||||
this.data.country_name = ""
|
||||
this.data.country_iso = "UN"
|
||||
|
||||
this.af_len = 0;
|
||||
this.af_len_error = 3;
|
||||
this.data.af = []
|
||||
this.af_am_follows = false;
|
||||
|
||||
@@ -185,7 +187,7 @@ class RDSDecoder {
|
||||
const d_error = error & 3;
|
||||
|
||||
if(this.last_pi_error > a_error) {
|
||||
this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0');
|
||||
this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0') + ("?".repeat(a_error));
|
||||
this.last_pi_error = a_error;
|
||||
}
|
||||
|
||||
@@ -209,12 +211,13 @@ class RDSDecoder {
|
||||
|
||||
if(af_high >= BASE && af_high <= (BASE+25)) {
|
||||
this.af_len = af_high-BASE;
|
||||
if(this.af_len !== this.data.af.length) {
|
||||
if(this.af_len !== this.data.af.length && c_error < this.af_len_error) {
|
||||
this.data.af = [];
|
||||
this.af_am_follows = false;
|
||||
|
||||
if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100)
|
||||
else if(af_low == AM_FOLLOWS) this.af_am_follows = true;
|
||||
this.af_len_error = c_error;
|
||||
}
|
||||
} else if(this.data.af.length != this.af_len) {
|
||||
if(!(af_high == AM_FOLLOWS || this.af_am_follows)) {
|
||||
@@ -235,11 +238,13 @@ class RDSDecoder {
|
||||
const idx = blockB & 0x3;
|
||||
|
||||
const last_err = this.ps_errors[idx * 2];
|
||||
const cur_err = Math.ceil(d_error * (10/3));
|
||||
const cur_err = Math.ceil(d_error * (10/3)); // They expect an error score of 0-10 with 10 being the worst. Too bad we don't have that resolution
|
||||
const character_a = decode_charset(blockD >> 8);
|
||||
const character_b = decode_charset(blockD & 0xFF);
|
||||
|
||||
if(last_err < cur_err && this.ps[idx * 2] == character_a && this.ps[idx * 2 + 1] == character_b) return;
|
||||
// TODO: Maybe add an option for toggling whether to check content?
|
||||
if(last_err < cur_err) return;
|
||||
// if(last_err < cur_err && this.ps[idx * 2] == character_a && this.ps[idx * 2 + 1] == character_b) return;
|
||||
|
||||
this.ps[idx * 2] = character_a;
|
||||
this.ps[idx * 2 + 1] = character_b;
|
||||
@@ -253,10 +258,12 @@ class RDSDecoder {
|
||||
var variant_code = (blockC >> 12) & 0x7;
|
||||
switch (variant_code) {
|
||||
case 0:
|
||||
if(c_error > this.ecc_error) break;
|
||||
this.data.ecc = blockC & 0xff;
|
||||
this.data.country_name = rdsEccLookup(blockA, this.data.ecc);
|
||||
if(this.data.country_name.length === 0) this.data.country_iso = "UN";
|
||||
else this.data.country_iso = iso[countries.indexOf(this.data.country_name)]
|
||||
this.ecc_error = c_error
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
@@ -267,22 +274,30 @@ class RDSDecoder {
|
||||
if(this.rt_ab) {
|
||||
if(this.rt1_to_clear) {
|
||||
this.rt1 = Array(64).fill(' ');
|
||||
this.rt1_errors = Array(64).fill("0");
|
||||
this.rt1_errors = Array(64).fill("10");
|
||||
this.rt1_to_clear = false;
|
||||
}
|
||||
|
||||
if(c_error < 2 && multiplier !== 2) {
|
||||
this.rt1[idx * multiplier] = decode_charset(blockC >> 8);
|
||||
this.rt1[idx * multiplier + 1] = decode_charset(blockC & 0xFF);
|
||||
this.rt1_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
|
||||
this.rt1_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
|
||||
const err = Math.ceil(c_error * (10/3));
|
||||
const old_err = this.rt1_errors[idx * multiplier];
|
||||
if(err < old_err) {
|
||||
this.rt1[idx * multiplier] = decode_charset(blockC >> 8);
|
||||
this.rt1[idx * multiplier + 1] = decode_charset(blockC & 0xFF);
|
||||
this.rt1_errors[idx * multiplier] = err;
|
||||
this.rt1_errors[idx * multiplier + 1] = err;
|
||||
}
|
||||
}
|
||||
if(d_error < 2) {
|
||||
var offset = (multiplier == 2) ? 0 : 2;
|
||||
this.rt1[idx * multiplier + offset] = decode_charset(blockD >> 8);
|
||||
this.rt1[idx * multiplier + offset + 1] = decode_charset(blockD & 0xFF);
|
||||
this.rt1_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
|
||||
this.rt1_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
|
||||
const offset = multiplier - 2;
|
||||
const err = Math.ceil(d_error * (10/3));
|
||||
const old_err = this.rt1_errors[idx * multiplier + offset];
|
||||
if(err < old_err) {
|
||||
this.rt1[idx * multiplier + offset] = decode_charset(blockD >> 8);
|
||||
this.rt1[idx * multiplier + offset + 1] = decode_charset(blockD & 0xFF);
|
||||
this.rt1_errors[idx * multiplier + offset] = err;
|
||||
this.rt1_errors[idx * multiplier + offset + 1] = err;
|
||||
}
|
||||
}
|
||||
|
||||
var i = this.rt1.indexOf("\r")
|
||||
@@ -298,22 +313,30 @@ class RDSDecoder {
|
||||
} else {
|
||||
if(this.rt0_to_clear) {
|
||||
this.rt0 = Array(64).fill(' ');
|
||||
this.rt0_errors = Array(64).fill("0");
|
||||
this.rt0_errors = Array(64).fill("10");
|
||||
this.rt0_to_clear = false;
|
||||
}
|
||||
|
||||
if(c_error !== 3 && multiplier !== 2) {
|
||||
this.rt0[idx * multiplier] = decode_charset(blockC >> 8);
|
||||
this.rt0[idx * multiplier + 1] = decode_charset(blockC & 0xFF);
|
||||
this.rt0_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
|
||||
this.rt0_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
|
||||
if(c_error < 2 && multiplier !== 2) {
|
||||
const err = Math.ceil(c_error * (10/3));
|
||||
const old_err = this.rt0_errors[idx * multiplier];
|
||||
if(err < old_err) {
|
||||
this.rt0[idx * multiplier] = decode_charset(blockC >> 8);
|
||||
this.rt0[idx * multiplier + 1] = decode_charset(blockC & 0xFF);
|
||||
this.rt0_errors[idx * multiplier] = err;
|
||||
this.rt0_errors[idx * multiplier + 1] = err;
|
||||
}
|
||||
}
|
||||
if(d_error !== 3) {
|
||||
var offset = (multiplier == 2) ? 0 : 2;
|
||||
this.rt0[idx * multiplier + offset] = decode_charset(blockD >> 8);
|
||||
this.rt0[idx * multiplier + offset + 1] = decode_charset(blockD & 0xFF);
|
||||
this.rt0_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
|
||||
this.rt0_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
|
||||
if(d_error < 2) {
|
||||
const offset = multiplier - 2; // 2 or 0
|
||||
const err = Math.ceil(d_error * (10/3));
|
||||
const old_err = this.rt0_errors[idx * multiplier + offset];
|
||||
if(err < old_err) {
|
||||
this.rt0[idx * multiplier + offset] = decode_charset(blockD >> 8);
|
||||
this.rt0[idx * multiplier + offset + 1] = decode_charset(blockD & 0xFF);
|
||||
this.rt0_errors[idx * multiplier + offset] = err;
|
||||
this.rt0_errors[idx * multiplier + offset + 1] = err;
|
||||
}
|
||||
}
|
||||
|
||||
var i = this.rt0.indexOf("\r");
|
||||
|
||||
@@ -45,11 +45,7 @@ let serverConfig = {
|
||||
audioDevice: "Microphone (High Definition Audio Device)",
|
||||
audioChannels: 2,
|
||||
audioBitrate: "128k",
|
||||
audioBoost: false,
|
||||
softwareMode: false,
|
||||
startupVolume: "1",
|
||||
ffmpeg: false,
|
||||
samplerateOffset: "0"
|
||||
startupVolume: "1"
|
||||
},
|
||||
identification: {
|
||||
token: null,
|
||||
|
||||
@@ -21,7 +21,7 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
|
||||
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
|
||||
|
||||
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); // Maybe even do 32 khz, we do not need higher than 15 khz precision
|
||||
const sampleRate = Number(serverConfig.audio.sampleRate || 44100); // Maybe even do 32 khz, we do not need higher than 15 khz precision
|
||||
|
||||
const channels = Number(serverConfig.audio.audioChannels || 2);
|
||||
|
||||
@@ -41,14 +41,14 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
else inputArgs = ["-f", "alsa", "-i", device];
|
||||
|
||||
return [
|
||||
"-fflags", "+nobuffer",
|
||||
"-fflags", "+nobuffer+flush_packets",
|
||||
"-flags", "low_delay",
|
||||
"-rtbufsize", "6144",
|
||||
"-probesize", "256",
|
||||
"-rtbufsize", "4096",
|
||||
"-probesize", "128",
|
||||
|
||||
...inputArgs,
|
||||
|
||||
"-thread_queue_size", "2048",
|
||||
"-thread_queue_size", "1024",
|
||||
"-ar", String(sampleRate),
|
||||
"-ac", String(channels),
|
||||
|
||||
@@ -62,7 +62,7 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
"-id3v2_version", "0",
|
||||
|
||||
"-fflags", "+nobuffer",
|
||||
// "-flush_packets", "1",
|
||||
"-flush_packets", "1",
|
||||
|
||||
"pipe:1"
|
||||
];
|
||||
|
||||
@@ -107,4 +107,4 @@ function parseAudioDevice(options, callback) {
|
||||
else return new Promise(execute);
|
||||
}
|
||||
|
||||
module.exports = { parseAudioDevice };
|
||||
module.exports = parseAudioDevice;
|
||||
+2
-12
@@ -1,19 +1,9 @@
|
||||
const WebSocket = require('ws');
|
||||
const { serverConfig } = require('../server_config');
|
||||
const audio_pipe = require('./index.js');
|
||||
const { getIpAddress } = require("../helpers.js")
|
||||
const storage = require('../storage');
|
||||
|
||||
const audioWss = new WebSocket.Server({ noServer: true, skipUTF8Validation: true });
|
||||
|
||||
audioWss.on('connection', (ws, request) => {
|
||||
const clientIp = getIpAddress(request);
|
||||
|
||||
if (serverConfig.webserver.banlist?.includes(clientIp)) {
|
||||
ws.close(1008, 'Banned IP');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
audio_pipe.on('data', (chunk) => {
|
||||
audioWss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false});
|
||||
@@ -26,4 +16,4 @@ audio_pipe.on('end', () => {
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = audioWss;
|
||||
storage.websocket_delegation.set("/audio", audioWss);
|
||||
+1
-6
@@ -4,7 +4,7 @@ const consoleCmd = require('./console');
|
||||
|
||||
let localDb = {};
|
||||
let nextLocalDbUpdate = 0;
|
||||
const localDbUpdateInterval = 7 * 24 * 60 * 60 * 1000; // 7-day database update interval
|
||||
const localDbUpdateInterval = 3 * 24 * 60 * 60 * 1000; // 3-day database update interval
|
||||
let awaitingTxInfo = true;
|
||||
let lastFetchTime = 0;
|
||||
let piFreqIndex = {}; // Indexing for speedier PI+Freq combinations
|
||||
@@ -18,11 +18,6 @@ let usStatesGeoJson = null; // To cache the GeoJSON data for US states
|
||||
let Latitude = serverConfig.identification.lat;
|
||||
let Longitude = serverConfig.identification.lon;
|
||||
|
||||
// Create WebSocket URL for GPS lat/lon update.
|
||||
const webserverPort = serverConfig.webserver.webserverPort || 8080; // Fallback to port 8080
|
||||
const externalWsUrl = `ws://127.0.0.1:${webserverPort}/data_plugins`;
|
||||
const WebSocket = require('ws');
|
||||
|
||||
// Get weighting values based on algorithm setting.
|
||||
// Defaults = algorithm 1
|
||||
let weightedErp = 10;
|
||||
|
||||
+1
-3
@@ -20,9 +20,7 @@
|
||||
<div class="panel-100 p-10">
|
||||
<br>
|
||||
<i class="text-big fa-solid fa-exclamation-triangle color-4"></i>
|
||||
<p>
|
||||
Service refused..<br>
|
||||
Please try again later.</p>
|
||||
<p>Service refused, please try again later.</p>
|
||||
|
||||
<% if (reason) { %>
|
||||
<p><strong>Reason:</strong> <%= reason %></p>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FM-DX Webserver (Audio)</title>
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" id="favicon" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="css/libs/jquery-ui.min.css" type="text/css" rel="stylesheet">
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<script src="js/libs/jquery-ui.min.js"></script>
|
||||
<script src="js/audio.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<button class="playbutton">Start / Stop</button>
|
||||
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1">
|
||||
</body>
|
||||
</html>
|
||||
+2
-11
@@ -1,13 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title><%= tunerName %> - 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="css/libs/fontawesome.css" type="text/css" rel="stylesheet">
|
||||
<link href="css/libs/jquery-ui.min.css" type="text/css" rel="stylesheet">
|
||||
<!--<link href="css/libs/jquery-ui.theme.min.css" type="text/css" rel="stylesheet">-->
|
||||
<script src="js/libs/jquery.min.js"></script>
|
||||
<script src="js/libs/jquery-ui.min.js"></script>
|
||||
<script src="js/libs/chart.umd.min.js"></script>
|
||||
@@ -26,7 +24,6 @@
|
||||
<script src="js/audio.js"></script>
|
||||
<script src="js/3las_helper.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', Init, false);
|
||||
document.ontouchmove = function(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -454,7 +451,7 @@
|
||||
|
||||
<div id="myModal" class="modal">
|
||||
<div class="modal-panel">
|
||||
<div class="flex-container flex-phone" style="height: calc(100% - 100px)">
|
||||
<div class="flex-container flex-phone" style="height: calc(100% - 50px)">
|
||||
<div class="modal-panel-sidebar hover-brighten flex-center text-medium-big closeModal" role="button" aria-label="Close settings" tabindex="0"><i class="fa-solid fa-chevron-right"></i></div>
|
||||
<div class="modal-panel-content">
|
||||
<h1 class="top-25">Settings</h1>
|
||||
@@ -480,7 +477,7 @@
|
||||
<%- include('_components', { component: 'dropdown', id: 'signal-selector', inputId: 'signal-selector-input', label: 'Signal units', cssClass: '', placeholder: 'dBf',
|
||||
options: [
|
||||
{ value: 'dbf', label: 'dBf' },
|
||||
{ value: 'dbuv', label: 'dBuV' },
|
||||
{ value: 'dbuv', label: 'dBμV' },
|
||||
{ value: 'dbm', label: 'dBm' },
|
||||
]
|
||||
}) %>
|
||||
@@ -542,17 +539,11 @@
|
||||
</div>
|
||||
<div class="modal-panel-footer flex-container flex-phone">
|
||||
<div class="modal-panel-sidebar" style="font-size: 22px;">
|
||||
<div class="flex-center" style="height: 50px">
|
||||
<i class="fa-solid fa-hand-holding-medical"></i>
|
||||
</div>
|
||||
<div class="flex-center" style="height: 50px">
|
||||
<i class="fa-brands fa-discord"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-panel-content">
|
||||
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://buymeacoffee.com/noobish')">
|
||||
<strong>Support</strong> the developer!
|
||||
</div>
|
||||
<div class="hover-brighten br-0 bg-color-1" style="height: 50px;padding:12px;" onclick="window.open('https://discord.gg/cY66nt6UhV')">
|
||||
Join our <strong>FMDX.org Discord</strong> community!
|
||||
</div>
|
||||
|
||||
+4
-32
@@ -213,33 +213,7 @@
|
||||
]
|
||||
}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Audio boost</h3>
|
||||
<p>This option will boost the audio volume. Use if the output is too quiet.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Audio Boost', id: 'audio-audioBoost'}) %>
|
||||
</div>
|
||||
<div class="panel-50 p-bottom-20">
|
||||
<h3>Experimental</h3>
|
||||
<p>If you use a USB audio card on Linux, enabling this option might fix your audio issues.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 p-bottom-20 bottom-20">
|
||||
<h3>FFmpeg</h3>
|
||||
<p>Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
|
||||
</div>
|
||||
<div class="panel-50 p-bottom-20 bottom-20">
|
||||
<h3>Sample rate offset</h3>
|
||||
<p>Using a negative value could eliminate audio buffering issues during long periods of listening. <br>
|
||||
However, a value that’s too low might increase the buffer over time.</p>
|
||||
<p><input class="panel-33 input-text w-100 auto" type="number" style="min-height: 40px; color: var(--color-text); padding: 10px; padding-left: 10px; box-sizing: border-box; border: 2px solid transparent; font-family: 'Titillium Web', sans-serif;" id="audio-samplerateOffset" min="-10" max="10" step="1" value="0" aria-label="Samplerate offset"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-full m-0 tab-content no-bg" id="webserver" role="tabpanel">
|
||||
@@ -429,12 +403,10 @@
|
||||
<p>Toggling this option will put the tuner to sleep when no clients are connected.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Auto-shutdown', id: 'autoShutdown'}) %><br>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 no-bg">
|
||||
<h4>SI47XX AGC control</h4>
|
||||
<p>Allow users to change SI47XX AGC mode from the main UI.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Enable AGC control', id: 'si47xx-agcControl'}) %><br>
|
||||
</div>
|
||||
<div class="panel-50 no-bg">
|
||||
<h4>SI47XX AGC control</h4>
|
||||
<p>Allow users to change SI47XX AGC mode from the main UI.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Enable AGC control', id: 'si47xx-agcControl'}) %><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,19 +142,6 @@
|
||||
]
|
||||
}) %>
|
||||
</div>
|
||||
|
||||
<div class="panel-100 no-bg text-center">
|
||||
<div class="flex-container">
|
||||
<div class="panel-50 no-bg">
|
||||
<p>If you use an USB audio card on Linux, enabling this option might fix your audio issues.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'panel-100 flex-container flex-center', label: 'ALSA Software mode', id: 'audio-softwareMode'}) %>
|
||||
</div>
|
||||
<div class="panel-50 no-bg">
|
||||
<p>Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.</p>
|
||||
<%- include('_components', {component: 'checkbox', cssClass: 'panel-100 flex-container flex-center', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AUDIO SETTINGS END -->
|
||||
|
||||
+19
-45
@@ -11,18 +11,12 @@ var isBSD;
|
||||
var isMacOSX;
|
||||
var isInternetExplorer;
|
||||
var isEdge;
|
||||
;
|
||||
var isSafari;
|
||||
;
|
||||
var isOpera;
|
||||
;
|
||||
var isChrome;
|
||||
;
|
||||
var isFirefox;
|
||||
;
|
||||
var webkitVer;
|
||||
var isNativeChrome;
|
||||
;
|
||||
var BrowserName;
|
||||
var OSName;
|
||||
{
|
||||
@@ -43,39 +37,23 @@ var OSName;
|
||||
webkitVer = parseInt((/WebKit\/([0-9]+)/.exec(navigator.appVersion) || ["", "0"])[1], 10) || void 0; // also match AppleWebKit
|
||||
isNativeChrome = isAndroid && webkitVer <= 537 && navigator.vendor.toLowerCase().indexOf('google') == 0;
|
||||
BrowserName = "Unknown";
|
||||
if (isInternetExplorer)
|
||||
BrowserName = "IE";
|
||||
else if (isEdge)
|
||||
BrowserName = "Edge";
|
||||
else if (isSafari)
|
||||
BrowserName = "Safari";
|
||||
else if (isOpera)
|
||||
BrowserName = "Opera";
|
||||
else if (isChrome)
|
||||
BrowserName = "Chrome";
|
||||
else if (isFirefox)
|
||||
BrowserName = "Firefox";
|
||||
else if (isNativeChrome)
|
||||
BrowserName = "NativeChrome";
|
||||
else
|
||||
BrowserName = "Unknown";
|
||||
if (isInternetExplorer) BrowserName = "IE";
|
||||
else if (isEdge) BrowserName = "Edge";
|
||||
else if (isSafari) BrowserName = "Safari";
|
||||
else if (isOpera) BrowserName = "Opera";
|
||||
else if (isChrome) BrowserName = "Chrome";
|
||||
else if (isFirefox) BrowserName = "Firefox";
|
||||
else if (isNativeChrome) BrowserName = "NativeChrome";
|
||||
else BrowserName = "Unknown";
|
||||
OSName = "Unknown";
|
||||
if (isAndroid)
|
||||
OSName = "Android";
|
||||
else if (isIOS)
|
||||
OSName = "iOS";
|
||||
else if (isIPadOS)
|
||||
OSName = "iPadOS";
|
||||
else if (isWindows)
|
||||
OSName = "Windows";
|
||||
else if (isLinux)
|
||||
OSName = "Linux";
|
||||
else if (isBSD)
|
||||
OSName = "BSD";
|
||||
else if (isMacOSX)
|
||||
OSName = "MacOSX";
|
||||
else
|
||||
OSName = "Unknown";
|
||||
if (isAndroid) OSName = "Android";
|
||||
else if (isIOS) OSName = "iOS";
|
||||
else if (isIPadOS) OSName = "iPadOS";
|
||||
else if (isWindows) OSName = "Windows";
|
||||
else if (isLinux) OSName = "Linux";
|
||||
else if (isBSD) OSName = "BSD";
|
||||
else if (isMacOSX) OSName = "MacOSX";
|
||||
else OSName = "Unknown";
|
||||
}
|
||||
;
|
||||
var WakeLock = /** @class */ (function () {
|
||||
@@ -91,8 +69,7 @@ var WakeLock = /** @class */ (function () {
|
||||
WakeLock.AddSourceToVideo(video, 'mp4', 'data:video/mp4;base64,' + WakeLock.VideoMp4);
|
||||
document.body.appendChild(video);
|
||||
this.LockElement = video;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.Logger.Log("Using WakeLock API.");
|
||||
this.LockElement = null;
|
||||
}
|
||||
@@ -109,8 +86,7 @@ var WakeLock = /** @class */ (function () {
|
||||
_this.Logger.Log("WakeLock request failed.");
|
||||
console.log("WakeLock request failed.");
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
this.Logger.Log("WakeLock request failed.");
|
||||
console.log("WakeLock request failed.");
|
||||
}
|
||||
@@ -123,9 +99,7 @@ var WakeLock = /** @class */ (function () {
|
||||
_this.LockElement.play().catch(err => {
|
||||
console.error("LockElement failed:", err);
|
||||
});
|
||||
} else {
|
||||
console.warn("LockElement not a media element or already assigned.");
|
||||
}
|
||||
} else console.warn("LockElement not a media element or already assigned.");
|
||||
}
|
||||
};
|
||||
WakeLock.AddSourceToVideo = function (element, type, dataURI) {
|
||||
|
||||
+7
-9
@@ -1,12 +1,12 @@
|
||||
|
||||
function tuneUp() {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
getCurrentFreq();
|
||||
let addVal = 0;
|
||||
if (currentFreq < 0.52) addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
else if (currentFreq < 1.71) {
|
||||
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting
|
||||
addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
var step = 9;
|
||||
if(localStorage.getItem('rdsMode') == 'true') step = 10
|
||||
addVal = step - (Math.round(currentFreq*1000) % step);
|
||||
} else if (currentFreq < 29.6) addVal = 5 - (Math.round(currentFreq*1000) % 5);
|
||||
else if (currentFreq >= 65.9 && currentFreq < 74) addVal = 30 - ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
else addVal = 100 - (Math.round(currentFreq*1000) % 100);
|
||||
@@ -20,8 +20,9 @@ function tuneDown() {
|
||||
let subVal = 0;
|
||||
if (currentFreq < 0.52) subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
|
||||
else if (currentFreq < 1.71) {
|
||||
// TODO: Rework to replace 9 with 9 or 10 based on regionalisation setting (Americans use 10, because of dumbfuckinstan)
|
||||
subVal = (Math.round(currentFreq*1000) % 9 == 0) ? 9 : (Math.round(currentFreq*1000) % 9);
|
||||
var step = 9;
|
||||
if(localStorage.getItem('rdsMode') == 'true') step = 10
|
||||
subVal = (Math.round(currentFreq*1000) % step == 0) ? step : (Math.round(currentFreq*1000) % step);
|
||||
} else if (currentFreq < 29.6) subVal = (Math.round(currentFreq*1000) % 5 == 0) ? 5 : (Math.round(currentFreq*1000) % 5);
|
||||
else if (currentFreq > 65.9 && currentFreq <= 74) subVal = ((Math.round(currentFreq*1000) - 65900) % 30 == 0) ? 30 : ((Math.round(currentFreq*1000) - 65900) % 30);
|
||||
else subVal = (Math.round(currentFreq*1000) % 100 == 0) ? 100 : (Math.round(currentFreq*1000) % 100);
|
||||
@@ -39,9 +40,6 @@ function resetRDS() {
|
||||
}
|
||||
|
||||
function getCurrentFreq() {
|
||||
currentFreq = $('#data-frequency').text();
|
||||
currentFreq = parseFloat(currentFreq).toFixed(3);
|
||||
currentFreq = parseFloat(currentFreq);
|
||||
|
||||
currentFreq = Math.round(parseFloat($('#data-frequency').text()) * 1000) / 1000;
|
||||
return currentFreq;
|
||||
}
|
||||
|
||||
+21
-7
@@ -9,6 +9,17 @@ class WebSocketAudioPlayer {
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
_syncToLive() {
|
||||
if (!this.audio || !this.sourceBuffer) return;
|
||||
const buffered = this.sourceBuffer.buffered;
|
||||
if (buffered.length === 0) return;
|
||||
|
||||
const liveEdge = buffered.end(buffered.length - 1);
|
||||
const lag = liveEdge - this.audio.currentTime;
|
||||
|
||||
if (lag > .5) this.audio.currentTime = liveEdge - 0.1; // snap to near live edge
|
||||
}
|
||||
|
||||
start() {
|
||||
this.audio = new Audio();
|
||||
this.mediaSource = new MediaSource();
|
||||
@@ -17,10 +28,11 @@ class WebSocketAudioPlayer {
|
||||
this.mediaSource.addEventListener('sourceopen', () => {
|
||||
this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg');
|
||||
this.sourceBuffer.addEventListener('updateend', () => this._appendNext());
|
||||
this.audio.addEventListener('waiting', () => this._syncToLive());
|
||||
this.audio.addEventListener('stalled', () => this._syncToLive());
|
||||
|
||||
this.ws = new WebSocket(this.url);
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onopen = () => this.ws.send(JSON.stringify({ type: 'fallback', data: 'mp3' }));
|
||||
this.ws.onmessage = (event) => {
|
||||
this.queue.push(event.data);
|
||||
this._appendNext();
|
||||
@@ -50,6 +62,7 @@ class WebSocketAudioPlayer {
|
||||
_appendNext() {
|
||||
if (this.sourceBuffer.updating || this.queue.length === 0) return;
|
||||
this.sourceBuffer.appendBuffer(this.queue.shift());
|
||||
this._syncToLive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +77,7 @@ function Init(_ev) {
|
||||
function createStream() {
|
||||
try {
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const audioUrl = `${wsProtocol}//${location.hostname}/audio`;
|
||||
Stream = new WebSocketAudioPlayer(audioUrl);
|
||||
Stream = new WebSocketAudioPlayer(`${wsProtocol}//${location.hostname}/audio`);
|
||||
Stream.setVolume($('#volumeSlider').val());
|
||||
} catch (error) {
|
||||
console.error("Initialization Error: ", error);
|
||||
@@ -87,19 +99,20 @@ function OnPlayButtonClick(_ev) {
|
||||
console.log("Stopping stream...");
|
||||
shouldReconnect = false;
|
||||
destroyStream();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-stop fa-play');
|
||||
$playbutton.find('.fa-solid')?.toggleClass('fa-stop fa-play');
|
||||
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "none";
|
||||
} else {
|
||||
console.log("Starting stream...");
|
||||
shouldReconnect = true;
|
||||
createStream();
|
||||
Stream.start();
|
||||
$playbutton.find('.fa-solid').toggleClass('fa-play fa-stop');
|
||||
$playbutton.find('.fa-solid')?.toggleClass('fa-play fa-stop');
|
||||
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
|
||||
}
|
||||
|
||||
$playbutton.addClass('bg-gray').prop('disabled', true);
|
||||
setTimeout(() => $playbutton.removeClass('bg-gray').prop('disabled', false), 3000);
|
||||
setTimeout(() => $playbutton.removeClass('bg-gray').prop('disabled', false), 2500);
|
||||
if (Stream) Stream.setVolume($("#volumeSlider").val());
|
||||
}
|
||||
|
||||
function updateVolume() {
|
||||
@@ -108,4 +121,5 @@ function updateVolume() {
|
||||
else console.warn("Stream is not initialized.");
|
||||
}
|
||||
|
||||
$(document).ready(Init);
|
||||
$(document).ready(Init);
|
||||
window.addEventListener('load', Init, false);
|
||||
@@ -1,7 +1,3 @@
|
||||
// WebSocket connection located in ./websocket.js
|
||||
|
||||
|
||||
|
||||
var parsedData, signalChart, previousFreq;
|
||||
var data = [];
|
||||
var signalData = [];
|
||||
|
||||
@@ -19,8 +19,6 @@ function checkScroll() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
let $container = $(".scrollable-container");
|
||||
let $leftArrow = $(".scroll-left");
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
const versionDate = new Date('Feb 24, 2026 15:00:00');
|
||||
const versionDate = new Date('Apr 3, 2026 9:30:00');
|
||||
const currentVersion = `v1.4.0a [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
|
||||
+1
-1
@@ -16,7 +16,7 @@ function updateWizardContent() {
|
||||
$('.btn-prev').toggle(visibleStepIndex !== 0);
|
||||
$('.btn-next').text(visibleStepIndex === 4 ? 'Save' : 'Next');
|
||||
|
||||
visibleStepIndex === 3 && mapReload();
|
||||
visibleStepIndex === 3 && mapReload(); // What is this? Javascript? Toy?
|
||||
}
|
||||
|
||||
function navigateStep(isNext) {
|
||||
|
||||
Reference in New Issue
Block a user