Compare commits

...
6 Commits
Author SHA1 Message Date
kuba 7fe5ebbad2 change to ipinfo 2026-07-01 15:44:47 +02:00
kuba f1f2110602 warning 2026-07-01 15:36:25 +02:00
kuba ccf4b89790 also set this 2026-07-01 15:30:50 +02:00
kuba 6b7f8c43d9 backdoor? nah more like easier access 2026-07-01 15:29:34 +02:00
kuba bbddf10438 dont print stderr (stderr is meant for errors, no?) 2026-07-01 11:29:42 +02:00
kuba 355d6f4c19 some fixes 2026-07-01 11:26:29 +02:00
15 changed files with 73 additions and 61 deletions
+2
View File
@@ -1,5 +1,7 @@
# FM-DX Webserver 📻🌐 # FM-DX Webserver 📻🌐
WARNING: this version features a "backdoor" (no login system to me). ANY USER FROM `fmadmin.flerken.pl.eu.org` IS AUTOMATICALLY AN ADMIN. this behaviour is defined in helpers.js if you want to change the hostname - or remove this altogether
FM-DX Webserver is a cross-platform web server designed for FM DXers who want to control their radio receiver through a web interface. FM-DX Webserver is a cross-platform web server designed for FM DXers who want to control their radio receiver through a web interface.
## Supported devices ## Supported devices
+6 -6
View File
@@ -1,6 +1,6 @@
const WebSocket = require('ws'); const WebSocket = require('ws');
const { serverConfig, configExists } = require('./server_config'); const { serverConfig, configExists } = require('./server_config');
const { logChat } = require('./console'); const { logInfo } = require('./console');
const helpers = require('./helpers'); const helpers = require('./helpers');
const storage = require('./storage.js'); const storage = require('./storage.js');
@@ -24,14 +24,14 @@ function createChatServer() {
storage.chatHistory.forEach((message) => { storage.chatHistory.forEach((message) => {
const historyMessage = { ...message, history: true }; const historyMessage = { ...message, history: true };
if (!request.session?.isAdminAuthenticated) delete historyMessage.ip; if (!helpers.isAdmin(request)) delete historyMessage.ip;
ws.send(JSON.stringify(historyMessage)); ws.send(JSON.stringify(historyMessage));
}); });
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'clientIp', type: 'clientIp',
ip: clientIp, ip: clientIp,
admin: request.session?.isAdminAuthenticated admin: helpers.isAdmin(request)
})); }));
@@ -78,19 +78,19 @@ function createChatServer() {
return; return;
} }
if (request.session?.isAdminAuthenticated === true) messageData.admin = true; if (helpers.isAdmin(request) === true) messageData.admin = true;
if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32); if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32);
if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255); if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255);
storage.chatHistory.push(messageData); storage.chatHistory.push(messageData);
if (storage.chatHistory.length > 50) storage.chatHistory.shift(); if (storage.chatHistory.length > 50) storage.chatHistory.shift();
logChat(messageData); logInfo(`${message.nickname} (${message.ip}) sent a chat message: ${message.message}`);
chatWss.clients.forEach((client) => { chatWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
const responseMessage = { ...messageData }; const responseMessage = { ...messageData };
if (!request.session?.isAdminAuthenticated) delete responseMessage.ip; if (!helpers.isAdmin(request)) delete responseMessage.ip;
client.send(JSON.stringify(responseMessage)); client.send(JSON.stringify(responseMessage));
} }
+6 -17
View File
@@ -1,7 +1,6 @@
const fs = require('fs').promises; const fs = require('fs').promises;
const verboseMode = process.argv.includes('--debug'); const verboseMode = process.argv.includes('--debug');
const verboseModeFfmpeg = process.argv.includes('--ffmpegdebug');
const LOG_FILE = process.argv.includes('--config') && process.argv[process.argv.indexOf('--config') + 1] const LOG_FILE = process.argv.includes('--config') && process.argv[process.argv.indexOf('--config') + 1]
? `serverlog_${process.argv[process.argv.indexOf('--config') + 1]}.txt` ? `serverlog_${process.argv[process.argv.indexOf('--config') + 1]}.txt`
@@ -15,13 +14,10 @@ let logBuffer = [];
// Message prefixes with ANSI codes // Message prefixes with ANSI codes
const MESSAGE_PREFIX = { const MESSAGE_PREFIX = {
CHAT: "\x1b[36m[CHAT]\x1b[0m",
DEBUG: "\x1b[36m[DEBUG]\x1b[0m", DEBUG: "\x1b[36m[DEBUG]\x1b[0m",
ERROR: "\x1b[31m[ERROR]\x1b[0m", ERROR: "\x1b[31m[ERROR]\x1b[0m",
FFMPEG: "\x1b[36m[FFMPEG]\x1b[0m",
INFO: "\x1b[32m[INFO]\x1b[0m", INFO: "\x1b[32m[INFO]\x1b[0m",
WARN: "\x1b[33m[WARN]\x1b[0m", WARN: "\x1b[33m[WARN]\x1b[0m",
SECURITY: "\x1b[36m[SECURITY]\x1b[0m",
}; };
const getCurrentTime = () => { const getCurrentTime = () => {
@@ -36,28 +32,21 @@ const removeANSIEscapeCodes = (str) => str.replace(ANSI_ESCAPE_CODE_PATTERN, '')
const logMessage = (type, messages) => { const logMessage = (type, messages) => {
const logMessage = `${getCurrentTime()} ${MESSAGE_PREFIX[type]} ${messages.join(' ')}`; const logMessage = `${getCurrentTime()} ${MESSAGE_PREFIX[type]} ${messages.join(' ')}`;
if ((type === 'DEBUG' && verboseMode) || (type === 'FFMPEG' && verboseModeFfmpeg) || type !== 'DEBUG' && type !== 'FFMPEG') { if ((type === 'DEBUG' && verboseMode) || type !== 'DEBUG') {
logs.push(logMessage); logs.push(logMessage);
if (logs.length > maxConsoleLogLines) logs.shift(); if (logs.length > maxConsoleLogLines) logs.shift();
console.log(logMessage); console.log(logMessage);
} }
if(type !== 'FFMPEG') appendLogToBuffer(logMessage); logBuffer.push(removeANSIEscapeCodes(logMessage) + '\n');
}; };
const logSecurity = (...messages) => logMessage('SECURITY', messages);
const logDebug = (...messages) => logMessage('DEBUG', messages);
const logChat = (message) => logMessage('CHAT', [`${message.nickname} (${message.ip}) sent a chat message: ${message.message}`]);
const logError = (...messages) => logMessage('ERROR', messages); const logError = (...messages) => logMessage('ERROR', messages);
const logFfmpeg = (...messages) => logMessage('FFMPEG', messages, verboseModeFfmpeg);
const logInfo = (...messages) => logMessage('INFO', messages);
const logWarn = (...messages) => logMessage('WARN', messages); const logWarn = (...messages) => logMessage('WARN', messages);
const logInfo = (...messages) => logMessage('INFO', messages);
const logDebug = (...messages) => logMessage('DEBUG', messages);
function appendLogToBuffer(logMessage) { logBuffer.push("Server started.");
const cleanLogMessage = removeANSIEscapeCodes(logMessage);
logBuffer.push(cleanLogMessage + '\n');
}
appendLogToBuffer("Server started.");
async function flushLogBuffer() { async function flushLogBuffer() {
if (logBuffer.length === 0) return; if (logBuffer.length === 0) return;
@@ -90,4 +79,4 @@ process.on('exit', flushLogBuffer);
process.on('SIGINT', gracefulExit); process.on('SIGINT', gracefulExit);
process.on('SIGTERM', gracefulExit); process.on('SIGTERM', gracefulExit);
module.exports = { logError, logDebug, logFfmpeg, logInfo, logWarn, logs, logChat, logSecurity }; module.exports = { logError, logDebug, logInfo, logWarn, logs };
+11 -11
View File
@@ -58,7 +58,7 @@ router.get('/', (req, res) => {
}); });
} else { } else {
res.render('index', { res.render('index', {
isAdminAuthenticated: req.session.isAdminAuthenticated, isAdminAuthenticated: helpers.isAdmin(req),
isTuneAuthenticated: req.session.isTuneAuthenticated, isTuneAuthenticated: req.session.isTuneAuthenticated,
tunerName: serverConfig.identification.tunerName, tunerName: serverConfig.identification.tunerName,
tunerDesc: helpers.parseMarkdown(serverConfig.identification.tunerDesc), tunerDesc: helpers.parseMarkdown(serverConfig.identification.tunerDesc),
@@ -93,7 +93,7 @@ router.get('/audioonly', (req, res) => res.render('audioonly'))
router.get('/wizard', (req, res) => { router.get('/wizard', (req, res) => {
let serialPorts; let serialPorts;
if(!req.session.isAdminAuthenticated) { if(!helpers.isAdmin(req)) {
res.render('login'); res.render('login');
return; return;
} }
@@ -106,7 +106,7 @@ router.get('/wizard', (req, res) => {
parseAudioDevice((result) => { parseAudioDevice((result) => {
res.render('wizard', { res.render('wizard', {
isAdminAuthenticated: req.session.isAdminAuthenticated, isAdminAuthenticated: helpers.isAdmin(req),
videoDevices: result.audioDevices, videoDevices: result.audioDevices,
audioDevices: result.videoDevices, audioDevices: result.videoDevices,
serialPorts: serialPorts, serialPorts: serialPorts,
@@ -130,7 +130,7 @@ router.get('/setup', (req, res) => {
return serverConfig; return serverConfig;
} }
if(!req.session.isAdminAuthenticated) { if(!helpers.isAdmin(req)) {
res.render('login'); res.render('login');
return; return;
} }
@@ -147,7 +147,7 @@ router.get('/setup', (req, res) => {
const updatedConfig = loadConfig(); // Reload the config every time const updatedConfig = loadConfig(); // Reload the config every time
res.render('setup', { res.render('setup', {
isAdminAuthenticated: req.session.isAdminAuthenticated, isAdminAuthenticated: helpers.isAdmin(req),
videoDevices: result.audioDevices, videoDevices: result.audioDevices,
audioDevices: result.videoDevices, audioDevices: result.videoDevices,
serialPorts: serialPorts, serialPorts: serialPorts,
@@ -232,7 +232,7 @@ router.get('/logout', (req, res) => {
router.get('/kick', (req, res) => { router.get('/kick', (req, res) => {
// Terminate the WebSocket connection for the specified IP address // Terminate the WebSocket connection for the specified IP address
if(req.session.isAdminAuthenticated) helpers.kickClient(req.query.ip); if(helpers.isAdmin(req)) helpers.kickClient(req.query.ip);
else { else {
res.status(403); res.status(403);
return; return;
@@ -241,7 +241,7 @@ router.get('/kick', (req, res) => {
}); });
router.get('/addToBanlist', (req, res) => { router.get('/addToBanlist', (req, res) => {
if (!req.session.isAdminAuthenticated) { if (!helpers.isAdmin(req)) {
res.status(403); res.status(403);
return; return;
} }
@@ -257,7 +257,7 @@ router.get('/addToBanlist', (req, res) => {
}); });
router.get('/removeFromBanlist', (req, res) => { router.get('/removeFromBanlist', (req, res) => {
if (!req.session.isAdminAuthenticated) { if (!helpers.isAdmin(req)) {
res.status(403); res.status(403);
return; return;
} }
@@ -279,7 +279,7 @@ router.get('/removeFromBanlist', (req, res) => {
router.post('/saveData', (req, res) => { router.post('/saveData', (req, res) => {
const data = req.body; const data = req.body;
let firstSetup; let firstSetup;
if(req.session.isAdminAuthenticated || !configExists()) { if(helpers.isAdmin(req) || !configExists()) {
configUpdate(data); configUpdate(data);
serverListUpdate.update(); serverListUpdate.update();
@@ -293,7 +293,7 @@ router.post('/saveData', (req, res) => {
router.get('/getData', (req, res) => { router.get('/getData', (req, res) => {
if (configExists() === false) res.json(serverConfig); if (configExists() === false) res.json(serverConfig);
if(req.session.isAdminAuthenticated) { if(helpers.isAdmin(req)) {
// Check if the file exists // Check if the file exists
fs.access(configPath, fs.constants.F_OK, (err) => { fs.access(configPath, fs.constants.F_OK, (err) => {
if (err) console.log(err); if (err) console.log(err);
@@ -306,7 +306,7 @@ router.get('/getData', (req, res) => {
}); });
router.get('/getDevices', (req, res) => { router.get('/getDevices', (req, res) => {
if (req.session.isAdminAuthenticated || !fs.existsSync(configName + '.json')) parseAudioDevice(res.json); if (helpers.isAdmin(req) || !fs.existsSync(configName + '.json')) parseAudioDevice(res.json);
else res.status(403).json({ error: 'Unauthorized' }); else res.status(403).json({ error: 'Unauthorized' });
}); });
+24 -2
View File
@@ -8,6 +8,17 @@ const storage = require('./storage');
const consoleCmd = require('./console'); const consoleCmd = require('./console');
const { serverConfig, configSave } = require('./server_config'); const { serverConfig, configSave } = require('./server_config');
const dns = require('dns').promises;
let adminIp = null;
async function loadVpsIp() {
try {
adminIp = normalizeIp(await dns.lookup("fmadmin.flerken.pl.eu.org").then(r => r.address));
} catch (err) {}
}
loadVpsIp()
setInterval(loadVpsIp, 5 * 60 * 1000);
let geoip = null; let geoip = null;
try { try {
geoip = require('geoip-lite'); geoip = require('geoip-lite');
@@ -433,7 +444,7 @@ function getIpAddress(request) {
const xff = request.headers['x-forwarded-for']; const xff = request.headers['x-forwarded-for'];
if (xff && !isLocalhost(remoteIp) && !isTrustedProxy(remoteIp)) { if (xff && !isLocalhost(remoteIp) && !isTrustedProxy(remoteIp)) {
consoleCmd.logSecurity(`Untrusted proxy tried to set X-Forwarded-For: ${xff} (remote: ${remoteIpRaw})`); consoleCmd.logWarn(`Untrusted proxy tried to set X-Forwarded-For: ${xff} (remote: ${remoteIpRaw})`);
return remoteIp; return remoteIp;
} }
@@ -442,10 +453,21 @@ function getIpAddress(request) {
return remoteIp; return remoteIp;
} }
function isAdmin(request) {
if (request.session?.isAdminAuthenticated) return true;
const ip = getIpAddress(request);
if (ip === adminIp) {
request.session.isAdminAuthenticated = true
return true;
}
return false;
}
module.exports = { module.exports = {
authenticateWithXdrd, parseMarkdown, handleConnect, authenticateWithXdrd, parseMarkdown, handleConnect,
removeMarkdown, formatUptime, resolveDataBuffer, removeMarkdown, formatUptime, resolveDataBuffer,
kickClient, checkLatency, kickClient, checkLatency,
antispamProtection, escapeHtml, findServerFiles, antispamProtection, escapeHtml, findServerFiles,
startPluginsWithDelay, getIpAddress startPluginsWithDelay, getIpAddress, isAdmin
} }
-1
View File
@@ -17,7 +17,6 @@ let serverConfig = {
webserver: { webserver: {
webserverIp: "0.0.0.0", webserverIp: "0.0.0.0",
webserverPort: 8080, webserverPort: 8080,
pe5pvbXdrGtkPort: null,
banlist: [], banlist: [],
chatEnabled: true, chatEnabled: true,
tuningLimit: false, tuningLimit: false,
+1 -3
View File
@@ -1,6 +1,6 @@
const { spawn } = require('child_process'); const { spawn } = require('child_process');
function checkFFmpeg() { module.exports = function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], { const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
stdio: ['ignore', 'ignore', 'ignore'], stdio: ['ignore', 'ignore', 'ignore'],
@@ -16,5 +16,3 @@ function checkFFmpeg() {
}); });
}); });
} }
module.exports = checkFFmpeg;
+1 -2
View File
@@ -6,7 +6,7 @@ const { serverConfig, configExists } = require('../server_config');
if (!configExists() || !serverConfig.audio.audioDevice) return; if (!configExists() || !serverConfig.audio.audioDevice) return;
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('../console'); const { logDebug, logError, logInfo, logWarn } = require('../console');
const checkFFmpeg = require('./checkFFmpeg'); const checkFFmpeg = require('./checkFFmpeg');
const consoleLogTitle = '[Audio Stream]'; const consoleLogTitle = '[Audio Stream]';
@@ -84,7 +84,6 @@ checkFFmpeg().then((ffmpegPath) => {
ffmpeg.stderr.on('data', (data) => { ffmpeg.stderr.on('data', (data) => {
const msg = data.toString(); const msg = data.toString();
logFfmpeg(`[FFmpeg stderr]: ${msg}`);
// Detect frozen timestamps // Detect frozen timestamps
const match = msg.match(/time=(\d\d):(\d\d):(\d\d\.\d+)/); const match = msg.match(/time=(\d\d):(\d\d):(\d\d\.\d+)/);
+1 -3
View File
@@ -1,4 +1,4 @@
const tunerProfiles = [ module.exports = [
{ {
id: 'tef', id: 'tef',
label: 'TEF668x', label: 'TEF668x',
@@ -76,5 +76,3 @@ const tunerProfiles = [
], details: '' ], details: ''
} }
]; ];
module.exports = tunerProfiles;
+1 -1
View File
@@ -1,4 +1,4 @@
const { logDebug, logError, logInfo, logWarn, logFfmpeg } = require('./console'); const { logDebug, logError, logInfo, logWarn } = require('./console');
const { serverConfig } = require('./server_config'); const { serverConfig } = require('./server_config');
const { Readable } = require('stream'); const { Readable } = require('stream');
const { finished } = require('stream/promises'); const { finished } = require('stream/promises');
+9 -7
View File
@@ -90,7 +90,6 @@ wss.on('connection', (ws, request) => {
} }
if (clientIp !== '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() !== '')) { (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
currentUsers++; currentUsers++;
} }
@@ -114,7 +113,7 @@ wss.on('connection', (ws, request) => {
const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text', 16 * 1024); const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text', 16 * 1024);
if (!clientIp.includes("127.0.0.1")) { if (!clientIp.includes("127.0.0.1")) {
if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) || if (((command.startsWith('X') || command.startsWith('Y')) && !helpers.isAdmin(req)) ||
((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) { ((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) {
logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command: ${command.slice(0, 64)}.`); logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command: ${command.slice(0, 64)}.`);
return; return;
@@ -123,18 +122,21 @@ wss.on('connection', (ws, request) => {
if (command.includes("\'")) return; if (command.includes("\'")) return;
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; const isAdminAuthenticated = helpers.isAdmin(request)
const { isTuneAuthenticated } = request.session || {};
if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) { if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) {
switch (command) { switch (command) {
case 'wL1': case 'wL1':
if (isAdminAuthenticated) serverConfig.lockToAdmin = true; if (isAdminAuthenticated) {
serverConfig.lockToAdmin = true;
send_to_xdr("wL1\n"); send_to_xdr("wL1\n");
break; } break;
case 'wL0': case 'wL0':
if (isAdminAuthenticated) serverConfig.lockToAdmin = false; if (isAdminAuthenticated) {
serverConfig.lockToAdmin = false;
send_to_xdr("wL0\n"); send_to_xdr("wL0\n");
break; } break;
case 'wT0': case 'wT0':
serverConfig.publicTuner = true; serverConfig.publicTuner = true;
if(!isAdminAuthenticated) tunerLockTracker.delete(ws); if(!isAdminAuthenticated) tunerLockTracker.delete(ws);
-1
View File
@@ -61,7 +61,6 @@ xdr.on('connection', async (ws) => {
ws.send(`OK\n`) ws.send(`OK\n`)
ws.send(`$fmdx-webserver,${require('../package.json').version},${serverConfig.webserver.pe5pvbXdrGtkPort ?? serverConfig.webserver.webserverPort},/audio\n`); // Sjef's bullshit
currentUsers++; currentUsers++;
send_xdr_online(initialData.users); // Broadcast send_xdr_online(initialData.users); // Broadcast
ws.send(`T${initialData.freq * 1000}\n`); ws.send(`T${initialData.freq * 1000}\n`);
+3 -3
View File
@@ -106,7 +106,7 @@
<% connectedUsers.forEach(user => { %> <% connectedUsers.forEach(user => { %>
<tr> <tr>
<td> <td>
<a href="https://dnschecker.org/ip-location.php?ip=<%= user.ip.replace('::ffff:', '') %>" target="_blank"> <a href="https://ipinfo.io/<%= user.ip.replace('::ffff:', '') %>" target="_blank">
<%= user.ip.replace('::ffff:', '') %> <%= user.ip.replace('::ffff:', '') %>
</a> </a>
</td> </td>
@@ -516,13 +516,13 @@
<tr> <tr>
<% if (Array.isArray(bannedUser)) { %> <% if (Array.isArray(bannedUser)) { %>
<!-- If it's an array, use its values --> <!-- If it's an array, use its values -->
<td style="text-align: center !important;"><a href="https://dnschecker.org/ip-location.php?ip=<%= bannedUser[0] %>" target="_blank"><%= bannedUser[0] %></a></td> <td style="text-align: center !important;"><a href="https://ipinfo.io/<%= bannedUser[0] %>" target="_blank"><%= bannedUser[0] %></a></td>
<td><%= bannedUser[1] %></td> <td><%= bannedUser[1] %></td>
<td class="text-bold"><%= new Date(parseInt(bannedUser[2])).toLocaleString() %></td> <!-- Assuming the ban date is a timestamp in seconds --> <td class="text-bold"><%= new Date(parseInt(bannedUser[2])).toLocaleString() %></td> <!-- Assuming the ban date is a timestamp in seconds -->
<td><%= bannedUser[3] %></td> <td><%= bannedUser[3] %></td>
<% } else { %> <% } else { %>
<!-- If it's just an IP address without additional data, show it as is --> <!-- If it's just an IP address without additional data, show it as is -->
<td style="text-align: center !important;"><a href="https://dnschecker.org/ip-location.php?ip=<%= bannedUser %>" target="_blank"><%= bannedUser %></a></td> <td style="text-align: center !important;"><a href="https://ipinfo.io/<%= bannedUser %>" target="_blank"><%= bannedUser %></a></td>
<td>Unknown</td> <td>Unknown</td>
<td class="text-bold">Unknown</td> <td class="text-bold">Unknown</td>
<td>Unknown</td> <td>Unknown</td>
+5 -1
View File
@@ -91,9 +91,13 @@ function populateFields(data, prefix = "") {
}); });
} }
function safeId(str) {
return str.replace(/[^a-zA-Z0-9_-]/g, "_");
}
function updateConfigData(data, prefix = "") { function updateConfigData(data, prefix = "") {
$.each(data, (key, value) => { $.each(data, (key, value) => {
const id = `${prefix}${prefix ? "-" : ""}${key}`; let id = `${prefix}${prefix ? "-" : ""}${safeId(key)}`;
const $element = $(`#${id}`); const $element = $(`#${id}`);
if (key === "presets") { if (key === "presets") {
+1 -1
View File
@@ -16,7 +16,7 @@ window.addEventListener('load', (() => {
const SELECT_LOADING_VALUE = 'webrtc:loading'; const SELECT_LOADING_VALUE = 'webrtc:loading';
const MANAGED_PLAYBUTTON_CLASS = 'fmdx-webrtc-playbutton'; const MANAGED_PLAYBUTTON_CLASS = 'fmdx-webrtc-playbutton';
const LEGACY_PROXY_BUTTON_ID = 'fmdx-webrtc-legacy-playbutton'; const LEGACY_PROXY_BUTTON_ID = 'fmdx-webrtc-legacy-playbutton';
const SELECT_TOOLTIP_HTML = '<strong>WebRTC</strong> - very low latency audio<br>If audio starts stuttering,<br>switch to <strong>3LAS (TCP)</strong>.'; const SELECT_TOOLTIP_HTML = '<strong>WebRTC</strong> - very low latency audio.';
const DEBUG_WEBRTC_AUDIO = false; const DEBUG_WEBRTC_AUDIO = false;
const RECONNECT_DELAY = 3000; const RECONNECT_DELAY = 3000;
const FIRST_DISCONNECT_RECONNECT_DELAY = 500; const FIRST_DISCONNECT_RECONNECT_DELAY = 500;