mirror of
https://github.com/KubaPro010/fm-dx-webserver.git
synced 2026-07-29 08:19:19 +02:00
some changes
This commit is contained in:
+15
-66
@@ -6,8 +6,6 @@ const { SerialPort } = require('serialport')
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
// File Imports
|
||||
const parseAudioDevice = require('./stream/parser');
|
||||
const { configName, serverConfig, configUpdate, configSave, configExists, configPath } = require('./server_config');
|
||||
const helpers = require('./helpers');
|
||||
const storage = require('./storage');
|
||||
@@ -70,7 +68,11 @@ router.get('/403', (req, res) => {
|
||||
router.get('/audioonly', (req, res) => res.render('audioonly'))
|
||||
|
||||
router.get('/setup', (req, res) => {
|
||||
let serialPorts;
|
||||
if(!helpers.isAdmin(req)) {
|
||||
res.render('login');
|
||||
return;
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const configFileContents = fs.readFileSync(configPath, 'utf8');
|
||||
@@ -79,45 +81,17 @@ router.get('/setup', (req, res) => {
|
||||
return serverConfig;
|
||||
}
|
||||
|
||||
if(!helpers.isAdmin(req)) {
|
||||
res.render('login');
|
||||
return;
|
||||
}
|
||||
const processUptimeInSeconds = Math.floor(process.uptime());
|
||||
const formattedProcessUptime = helpers.formatUptime(processUptimeInSeconds);
|
||||
|
||||
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: helpers.isAdmin(req),
|
||||
videoDevices: result.audioDevices,
|
||||
audioDevices: result.videoDevices,
|
||||
serialPorts: serialPorts,
|
||||
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
|
||||
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
|
||||
processUptime: formattedProcessUptime,
|
||||
consoleOutput: logs,
|
||||
plugins: require('./plugins'),
|
||||
enabledPlugins: updatedConfig.plugins,
|
||||
onlineUsers: dataHandler.dataToSend.users,
|
||||
connectedUsers: storage.connectedUsers,
|
||||
device: serverConfig.device,
|
||||
banlist: updatedConfig.webserver.banlist, // Updated banlist from the latest config
|
||||
tunerProfiles: tunerProfiles.map((profile) => ({
|
||||
id: profile.id,
|
||||
label: profile.label,
|
||||
detailsHtml: helpers.parseMarkdown(profile.details || '')
|
||||
}))
|
||||
});
|
||||
});
|
||||
})
|
||||
const updatedConfig = loadConfig(); // Reload the config every time
|
||||
res.render('setup', {
|
||||
isAdminAuthenticated: true,
|
||||
memoryUsage: (process.memoryUsage.rss() / 1024 / 1024).toFixed(1) + ' MB',
|
||||
memoryHeap: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(1) + ' MB',
|
||||
processUptime: formattedProcessUptime,
|
||||
consoleOutput: logs,
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/api', (req, res) => {
|
||||
@@ -254,11 +228,6 @@ router.get('/getData', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getDevices', (req, res) => {
|
||||
if (helpers.isAdmin(req) || !fs.existsSync(configName + '.json')) parseAudioDevice(res.json);
|
||||
else res.status(403).json({ error: 'Unauthorized' });
|
||||
});
|
||||
|
||||
/* Static data are being sent through here on connection - these don't change when the server is running */
|
||||
router.get('/static_data', (req, res) => {
|
||||
res.json({
|
||||
@@ -382,24 +351,4 @@ router.get('/log_fmlist', (req, res) => {
|
||||
request.end();
|
||||
});
|
||||
|
||||
router.get('/tunnelservers', async (req, res) => {
|
||||
const servers = [
|
||||
{ value: "eu", host: "eu.fmtuner.org", label: "Europe" },
|
||||
{ value: "us", host: "us.fmtuner.org", label: "Americas" },
|
||||
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" }
|
||||
];
|
||||
|
||||
const results = await Promise.all(
|
||||
servers.map(async s => {
|
||||
const latency = await helpers.checkLatency(s.host);
|
||||
return {
|
||||
value: s.value,
|
||||
label: `${s.label} (${latency ? latency + ' ms' : 'offline'})` // From my tests, the latency via HTTP ping is roughly 2x higher than regular ping
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
const tunnel = require('./tunnel');
|
||||
tunnel.download();
|
||||
|
||||
require('./stream/index');
|
||||
require("./device");
|
||||
|
||||
const { serverConfig } = require('./server_config');
|
||||
@@ -12,7 +8,7 @@ const { serverConfig } = require('./server_config');
|
||||
if (plugins.length > 0) setTimeout(helpers.startPluginsWithDelay, 3000, plugins, 3000);
|
||||
}
|
||||
|
||||
require('./stream/index');
|
||||
require("./web")(serverConfig.webserver.webserverIp);
|
||||
|
||||
tunnel.connect();
|
||||
require('./server_list')();
|
||||
+24
-47
@@ -1,52 +1,29 @@
|
||||
var countries = [
|
||||
"Albania",
|
||||
"Estonia",
|
||||
"Algeria",
|
||||
"Ethiopia",
|
||||
"Andorra",
|
||||
"Angola",
|
||||
"Finland",
|
||||
"Armenia",
|
||||
"France",
|
||||
"Ascension Island",
|
||||
"Gabon",
|
||||
"Austria",
|
||||
"Gambia",
|
||||
"Azerbaijan",
|
||||
"Georgia",
|
||||
"Germany",
|
||||
"Bahrein",
|
||||
"Ghana",
|
||||
"Belarus",
|
||||
"Gibraltar",
|
||||
"Belgium",
|
||||
"Greece",
|
||||
"Benin",
|
||||
"Guinea",
|
||||
"Bosnia Herzegovina",
|
||||
"Guinea-Bissau",
|
||||
"Botswana",
|
||||
"Hungary",
|
||||
"Bulgaria",
|
||||
"Iceland",
|
||||
"Burkina Faso",
|
||||
"Iraq",
|
||||
"Burundi",
|
||||
"Ireland",
|
||||
"Cabinda",
|
||||
"Israel",
|
||||
"Cameroon",
|
||||
"Italy",
|
||||
"Jordan",
|
||||
"Cape Verde",
|
||||
"Albania", "Estonia",
|
||||
"Algeria", "Ethiopia",
|
||||
"Andorra", "Angola",
|
||||
"Finland", "Armenia",
|
||||
"France", "Ascension Island",
|
||||
"Gabon", "Austria",
|
||||
"Gambia", "Azerbaijan",
|
||||
"Georgia", "Germany",
|
||||
"Bahrein", "Ghana",
|
||||
"Belarus", "Gibraltar",
|
||||
"Belgium", "Greece",
|
||||
"Benin", "Guinea",
|
||||
"Bosnia Herzegovina", "Guinea-Bissau",
|
||||
"Botswana", "Hungary",
|
||||
"Bulgaria", "Iceland",
|
||||
"Burkina Faso", "Iraq",
|
||||
"Burundi", "Ireland",
|
||||
"Cabinda", "-",
|
||||
"Cameroon", "Italy",
|
||||
"Jordan", "Cape Verde",
|
||||
"Kazakhstan",
|
||||
"Central African Republic",
|
||||
"Kenya",
|
||||
"Chad",
|
||||
"Kosovo",
|
||||
"Comoros",
|
||||
"Kuwait",
|
||||
"DR Congo",
|
||||
"Kenya", "Chad",
|
||||
"Kosovo", "Comoros",
|
||||
"Kuwait", "DR Congo",
|
||||
"Kyrgyzstan",
|
||||
"Republic of Congo",
|
||||
"Latvia",
|
||||
@@ -402,7 +379,7 @@ const rdsEccD0D4Lut = [
|
||||
const rdsEccE0E5Lut = [
|
||||
// E0
|
||||
[
|
||||
"Germany", "Algeria", "Andorra", "Israel", "Italy",
|
||||
"Germany", "Algeria", "Andorra", "-", "Italy",
|
||||
"Belgium", "Russia", "Palestine", "Albania", "Austria",
|
||||
"Hungary", "Malta", "Germany", "", "Egypt"
|
||||
],
|
||||
|
||||
@@ -89,20 +89,6 @@ let serverConfig = {
|
||||
si47xx: {
|
||||
agcControl: false
|
||||
},
|
||||
tunnel: {
|
||||
enabled: false,
|
||||
username: "",
|
||||
token: "",
|
||||
region: "eu",
|
||||
lowLatencyMode: false,
|
||||
subdomain: "",
|
||||
httpName: "",
|
||||
httpPassword: "",
|
||||
community: {
|
||||
enabled: false,
|
||||
host: ""
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
device: 'tef',
|
||||
defaultFreq: 87.5,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
module.exports = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
|
||||
checkFFmpegProcess.on('error', () => {
|
||||
resolve(require('ffmpeg-static'));
|
||||
});
|
||||
|
||||
checkFFmpegProcess.on('exit', (code) => {
|
||||
if (code === 0) resolve('ffmpeg');
|
||||
else resolve(require('ffmpeg-static'));
|
||||
});
|
||||
});
|
||||
}
|
||||
+18
-6
@@ -7,7 +7,23 @@ if (!configExists() || !serverConfig.audio.audioDevice) return;
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { logDebug, logError, logInfo, logWarn } = require('../console');
|
||||
const checkFFmpeg = require('./checkFFmpeg');
|
||||
|
||||
const checkFFmpeg = function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkFFmpegProcess = spawn('ffmpeg', ['-version'], {
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
});
|
||||
|
||||
checkFFmpegProcess.on('error', () => {
|
||||
resolve(require('ffmpeg-static'));
|
||||
});
|
||||
|
||||
checkFFmpegProcess.on('exit', (code) => {
|
||||
if (code === 0) resolve('ffmpeg');
|
||||
else resolve(require('ffmpeg-static'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const consoleLogTitle = '[Audio Stream]';
|
||||
|
||||
@@ -24,8 +40,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); // Maybe even do 32 khz, we do not need higher than 15 khz precision
|
||||
|
||||
const sampleRate = Number(serverConfig.audio.sampleRate || 32000);
|
||||
const channels = Number(serverConfig.audio.audioChannels || 2);
|
||||
|
||||
let ffmpeg = null;
|
||||
@@ -80,8 +95,6 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
|
||||
ffmpeg.stdout.pipe(audio_pipe, { end: false });
|
||||
|
||||
connectMessage(`${consoleLogTitle} Connected FFmpeg → MP3 → audioWss`);
|
||||
|
||||
ffmpeg.stderr.on('data', (data) => {
|
||||
const msg = data.toString();
|
||||
|
||||
@@ -136,7 +149,6 @@ checkFFmpeg().then((ffmpegPath) => {
|
||||
});
|
||||
|
||||
launchFFmpeg();
|
||||
|
||||
}).catch((err) => {
|
||||
logError(`${consoleLogTitle} Error: ${err.message}`);
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const exec = require('child_process').exec;
|
||||
const fs = require('fs').promises; // Use the Promise-based fs API
|
||||
const ffmpeg = require('ffmpeg-static');
|
||||
const filePath = '/proc/asound/cards';
|
||||
const platform = process.platform;
|
||||
|
||||
function parseAudioDevice(options, callback) {
|
||||
let videoDevices = [];
|
||||
let audioDevices = [];
|
||||
let isVideo = true;
|
||||
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
options = options || {};
|
||||
const ffmpegPath = `"${ffmpeg.replace(/\\/g, '\\\\')}"`;
|
||||
const callbackExists = typeof callback === 'function';
|
||||
|
||||
const execute = async (fulfill, reject) => {
|
||||
try {
|
||||
if (platform === 'linux') {
|
||||
try {
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
const regex = /\[([^\]]+)\]/g;
|
||||
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
|
||||
|
||||
matches.forEach(match => {
|
||||
if (typeof match === 'string') audioDevices.push({ name: match });
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error reading file: ${err.message}`);
|
||||
}
|
||||
|
||||
// Linux doesn't support the `-list_devices` ffmpeg command like macOS/Windows,
|
||||
// so skip the ffmpeg exec for Linux
|
||||
const result = { videoDevices: [], audioDevices };
|
||||
if (callbackExists) return callback(result);
|
||||
return fulfill(result);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
exec(`${ffmpegPath} -f ${inputDevice} -list_devices true -i ""`, (err, stdout, stderr) => {
|
||||
stderr.split("\n")
|
||||
.filter(line => line.search(prefix) > -1)
|
||||
.forEach(line => {
|
||||
const deviceList = isVideo ? videoDevices : audioDevices;
|
||||
if (line.search(audioSeparator) > -1) {
|
||||
isVideo = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (platform === 'win32' && line.search(/Alternative\sname/) > -1) {
|
||||
const lastDevice = deviceList[deviceList.length - 1];
|
||||
const alt = line.match(alternativeName);
|
||||
if (lastDevice && alt) lastDevice.alternativeName = alt[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;
|
||||
}
|
||||
deviceList.push(device);
|
||||
}
|
||||
});
|
||||
|
||||
audioDevices = audioDevices.filter(device => device.name !== undefined);
|
||||
const result = { videoDevices, audioDevices };
|
||||
if (callbackExists) return callback(result);
|
||||
return fulfill(result);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Unexpected error:', err);
|
||||
if (callbackExists) callback({ videoDevices: [], audioDevices: [] });
|
||||
else reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (callbackExists) execute();
|
||||
else return new Promise(execute);
|
||||
}
|
||||
|
||||
module.exports = parseAudioDevice;
|
||||
@@ -1,101 +0,0 @@
|
||||
const { logDebug, logError, logInfo, logWarn } = require('./console');
|
||||
const { serverConfig } = require('./server_config');
|
||||
const { Readable } = require('stream');
|
||||
const { finished } = require('stream/promises');
|
||||
const fs = require('fs/promises');
|
||||
const fs2 = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const ejs = require('ejs');
|
||||
const { spawn } = require('child_process');
|
||||
const readline = require('readline');
|
||||
|
||||
const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F_OK).then(() => resolve(true)).catch(() => resolve(false)));
|
||||
|
||||
async function download() {
|
||||
if (serverConfig.tunnel?.enabled === true) {
|
||||
const librariesDir = path.resolve(__dirname, '../libraries');
|
||||
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
|
||||
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
||||
if (!await fileExists(frpcPath)) {
|
||||
logInfo('frpc binary, required for tunnel is not available. Downloading now...');
|
||||
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
|
||||
|
||||
try {
|
||||
const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName);
|
||||
if (res.status === 404) throw new Error('404 error');
|
||||
const stream = fs2.createWriteStream(frpcPath);
|
||||
await finished(Readable.fromWeb(res.body).pipe(stream));
|
||||
} catch (err) {
|
||||
logError('Failed to download frpc, reason: ' + err);
|
||||
return;
|
||||
}
|
||||
logInfo('Downloading of frpc is completed.')
|
||||
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (serverConfig.tunnel?.enabled === true) {
|
||||
const librariesDir = path.resolve(__dirname, '../libraries');
|
||||
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
|
||||
|
||||
const cfg = ejs.render(frpcConfigTemplate, {
|
||||
cfg: serverConfig.tunnel,
|
||||
host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : (serverConfig.tunnel.region + ".fmtuner.org"),
|
||||
server: {
|
||||
port: serverConfig.webserver.webserverPort
|
||||
}
|
||||
});
|
||||
const cfgPath = path.resolve(librariesDir, 'frpc.toml');
|
||||
await fs.writeFile(cfgPath, cfg);
|
||||
const child = spawn(frpcPath, ['-c', cfgPath]);
|
||||
process.on('exit', () => child.kill());
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: child.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (line.includes('connect to server error')) logError('Failed to connect to tunnel, reason: ' + line.substring(line.indexOf(': ')+2));
|
||||
else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: invalid user or token');
|
||||
else if (line.includes('start proxy success')) logInfo('Tunnel established successfully');
|
||||
else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful');
|
||||
else logDebug('Tunnel log:', line);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
logError('Failed to start tunnel process:', err);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
logInfo(`Tunnel process exited with code ${code}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const frpcConfigTemplate = `
|
||||
serverAddr = "<%= host %>"
|
||||
serverPort = 7000
|
||||
loginFailExit = false
|
||||
log.disablePrintColor = true
|
||||
user = "<%= cfg.username %>"
|
||||
metadatas.token = "<%= cfg.token %>"
|
||||
<% if (cfg.lowLatencyMode) { %>
|
||||
transport.protocol = "kcp"
|
||||
<% } %>
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
localPort = <%= server.port %>
|
||||
subdomain = "<%= cfg.subdomain %>"
|
||||
<% if (cfg.httpName != "") { %>
|
||||
httpUser = "<%= cfg.httpName %>"
|
||||
httpPassword = "<%= cfg.httpPassword %>"
|
||||
<% } %>
|
||||
`;
|
||||
|
||||
module.exports = { connect, download };
|
||||
@@ -83,7 +83,6 @@ wss.on('connection', (ws, request) => {
|
||||
}
|
||||
|
||||
if(currentUsers >= MAX_USERS) {
|
||||
// TODO: give a nice fucking unwelcome message on the ui (we kindly ask you to FUCK OFF?)
|
||||
ws.send("a0"); // This means not authenticated in XDR
|
||||
ws.close(1008, 'Too many users using this server at the moment.');
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Template
|
||||
|
||||
```
|
||||
serverAddr = "<%= host %>"
|
||||
serverPort = 7000
|
||||
loginFailExit = false
|
||||
log.disablePrintColor = true
|
||||
user = "<%= cfg.username %>"
|
||||
metadatas.token = "<%= cfg.token %>"
|
||||
<% if (cfg.lowLatencyMode) { %>
|
||||
transport.protocol = "kcp"
|
||||
<% } %>
|
||||
|
||||
[[proxies]]
|
||||
name = "web"
|
||||
type = "http"
|
||||
localPort = <%= server.port %>
|
||||
subdomain = "<%= cfg.subdomain %>"
|
||||
<% if (cfg.httpName != "") { %>
|
||||
httpUser = "<%= cfg.httpName %>"
|
||||
httpPassword = "<%= cfg.httpPassword %>"
|
||||
<% } %>
|
||||
```
|
||||
+3
-2
@@ -20,10 +20,11 @@
|
||||
<div class="panel-100 p-10">
|
||||
<br>
|
||||
<i class="text-big fa-solid fa-exclamation-triangle color-4"></i>
|
||||
<p>Service refused, please try again later.</p>
|
||||
|
||||
<% if (reason) { %>
|
||||
<p><strong>Reason:</strong> <%= reason %></p>
|
||||
<p><%= reason %></p>
|
||||
<% } else { %>
|
||||
<p>Service refused, please try again later.</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
-10
@@ -6,6 +6,7 @@
|
||||
<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>
|
||||
@@ -21,18 +22,12 @@
|
||||
<meta property="og:image" content="https://flerken.zapto.org:5000/img/webserver_icon.png">
|
||||
<meta property="og:description" content="Server description: <%= tunerDescMeta %>.">
|
||||
|
||||
<script src="js/3las/util/3las.helpers.js"></script>
|
||||
<script src="js/3las/util/3las.logging.js"></script>
|
||||
<script src="js/3las/fallback/3las.liveaudioplayer.js"></script>
|
||||
<script src="js/3las/fallback/3las.formatreader.js"></script>
|
||||
<script src="js/3las/fallback/formats/3las.formatreader.mpeg.js"></script>
|
||||
<script src="js/3las/fallback/formats/3las.formatreader.wav.js"></script>
|
||||
<script src="js/3las/util/3las.websocketclient.js"></script>
|
||||
<script src="js/3las/fallback/3las.fallback.js"></script>
|
||||
<script src="js/3las/3las.js"></script>
|
||||
<script src="js/audio.js"></script>
|
||||
<!-- Audio streaming -->
|
||||
<script src="js/audio/mp3_player.js"></script>
|
||||
<script src="js/audio/main.js"></script>
|
||||
<script src="js/rtc-audio.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('load', Init, false);
|
||||
document.ontouchmove = function(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
var audioStreamRestartInterval;
|
||||
var elapsedTimeConnectionWatchdog;
|
||||
var _3LAS = /** @class */ (function () {
|
||||
function _3LAS(url, logger) {
|
||||
this.wsurl = url;
|
||||
this.Logger = logger;
|
||||
if (!this.Logger) this.Logger = new Logging(null, null);
|
||||
|
||||
try {
|
||||
this.Fallback_Settings = new Fallback_Settings();
|
||||
this.Fallback = new Fallback(this.Logger, this.Fallback_Settings);
|
||||
this.Fallback.ActivityCallback = this.OnActivity.bind(this);
|
||||
} catch (_b) {
|
||||
this.Fallback = null;
|
||||
}
|
||||
|
||||
if (this.Fallback == null) {
|
||||
this.Logger.Log('3LAS: Browser does not support either media handling methods.');
|
||||
throw new Error();
|
||||
}
|
||||
if (isAndroid) this.WakeLock = new WakeLock(this.Logger);
|
||||
}
|
||||
Object.defineProperty(_3LAS.prototype, "Volume", {
|
||||
get: function () {
|
||||
return this.Fallback.Volume;
|
||||
},
|
||||
set: function (value) {
|
||||
this.Fallback.Volume = value;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
_3LAS.prototype.CanChangeVolume = function () {
|
||||
return true;
|
||||
};
|
||||
_3LAS.prototype.Start = function () {
|
||||
this.ConnectivityFlag = false;
|
||||
this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug
|
||||
|
||||
// Restart audio stream if radio data connection was reestablished
|
||||
// to prevent stuttering audio in some cases
|
||||
if (audioStreamRestartInterval) clearInterval(audioStreamRestartInterval);
|
||||
audioStreamRestartInterval = setInterval(() => {
|
||||
if (requiresAudioStreamRestart) {
|
||||
requiresAudioStreamRestart = false;
|
||||
if (Stream) {
|
||||
this.Stop();
|
||||
this.Start();
|
||||
console.log("Audio stream restarted after radio data loss.");
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Stream connection watchdog monitors mp3 frames
|
||||
console.log("Stream connection watchdog active.");
|
||||
let intervalReconnectWatchdog = setInterval(() => {
|
||||
if (Stream) {
|
||||
let endTimeConnectionWatchdog = performance.now();
|
||||
elapsedTimeConnectionWatchdog = endTimeConnectionWatchdog - window.startTimeConnectionWatchdog;
|
||||
//console.log(`Stream frame elapsed time: ${parseInt(elapsedTimeConnectionWatchdog)} ms`);
|
||||
if (elapsedTimeConnectionWatchdog > 2000 && shouldReconnect) {
|
||||
clearInterval(intervalReconnectWatchdog);
|
||||
setTimeout(() => {
|
||||
clearInterval(intervalReconnectWatchdog);
|
||||
console.log("Unstable internet connection detected, reconnecting... (" + parseInt(elapsedTimeConnectionWatchdog) + " ms)");
|
||||
this.Stop();
|
||||
this.Start();
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
clearInterval(intervalReconnectWatchdog);
|
||||
this.Stop();
|
||||
console.log("Stream connection watchdog inactive.");
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// This is stupid, but required for Android.... thanks Google :(
|
||||
if (this.WakeLock) this.WakeLock.Begin();
|
||||
try {
|
||||
this.WebSocket = new WebSocketClient(this.Logger, this.wsurl , this.OnSocketError.bind(this), this.OnSocketConnect.bind(this), this.OnSocketDataReady.bind(this), this.OnSocketDisconnect.bind(this));
|
||||
this.Logger.Log("Init of WebSocketClient succeeded");
|
||||
this.Logger.Log("Trying to connect to server.");
|
||||
}
|
||||
catch (e) {
|
||||
this.Logger.Log("Init of WebSocketClient failed: " + e);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
_3LAS.prototype.Stop = function () {
|
||||
try {
|
||||
// Close WebSocket connection
|
||||
if (this.WebSocket) {
|
||||
this.WebSocket.Close();
|
||||
this.WebSocket.OnClose();
|
||||
this.WebSocket = null;
|
||||
this.Logger.Log("WebSocket connection closed.");
|
||||
}
|
||||
|
||||
// Stop WakeLock if it exists and is an Android device
|
||||
if (isAndroid && this.WakeLock) {
|
||||
this.WakeLock.End();
|
||||
this.Logger.Log("WakeLock stopped.");
|
||||
}
|
||||
|
||||
// Reset Fallback if it exists
|
||||
if (this.Fallback) {
|
||||
this.Fallback.OnSocketDisconnect();
|
||||
this.Fallback.Stop();
|
||||
this.Fallback.Reset();
|
||||
this.Logger.Log("Fallback reset.");
|
||||
}
|
||||
|
||||
// Reset connectivity flag
|
||||
if (this.ConnectivityFlag) {
|
||||
this.ConnectivityFlag = null;
|
||||
if (this.ConnectivityCallback) {
|
||||
this.ConnectivityCallback(null);
|
||||
}
|
||||
}
|
||||
|
||||
this.Logger.Log("3LAS stopped successfully.");
|
||||
} catch (e) {
|
||||
this.Logger.Log("Error while stopping 3LAS: " + e);
|
||||
}
|
||||
};
|
||||
_3LAS.prototype.OnActivity = function () {
|
||||
if (this.ActivityCallback)
|
||||
this.ActivityCallback();
|
||||
if (!this.ConnectivityFlag) {
|
||||
this.ConnectivityFlag = true;
|
||||
if (this.ConnectivityCallback)
|
||||
this.ConnectivityCallback(true);
|
||||
}
|
||||
};
|
||||
// Callback function from socket connection
|
||||
_3LAS.prototype.OnSocketError = function (message) {
|
||||
this.Logger.Log("Network error: " + message);
|
||||
this.Fallback.OnSocketError(message);
|
||||
};
|
||||
_3LAS.prototype.OnSocketConnect = function () {
|
||||
this.Logger.Log("Established connection with server.");
|
||||
this.Fallback.OnSocketConnect();
|
||||
this.Fallback.Init(this.WebSocket);
|
||||
};
|
||||
_3LAS.prototype.OnSocketDisconnect = function () {
|
||||
this.Logger.Log("Lost connection to server.");
|
||||
this.Fallback.OnSocketDisconnect();
|
||||
this.Fallback.Reset();
|
||||
if (this.ConnectivityFlag) {
|
||||
this.ConnectivityFlag = false;
|
||||
if (this.ConnectivityCallback)
|
||||
this.ConnectivityCallback(false);
|
||||
}
|
||||
};
|
||||
|
||||
_3LAS.prototype.OnSocketDataReady = function (data) {
|
||||
this.Fallback.OnSocketDataReady(data);
|
||||
};
|
||||
return _3LAS;
|
||||
}());
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
Socket fallback is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var Fallback_Settings = /** @class */ (function () {
|
||||
function Fallback_Settings() {
|
||||
this.Formats = [
|
||||
{ "Mime": "audio/mpeg", "Name": "mp3" },
|
||||
{ "Mime": "audio/wave", "Name": "wav" }
|
||||
];
|
||||
this.MaxVolume = 1.0;
|
||||
this.AutoCorrectSpeed = false;
|
||||
this.InitialBufferLength = 1.0 / 3.0;
|
||||
}
|
||||
return Fallback_Settings;
|
||||
}());
|
||||
var Fallback = /** @class */ (function () {
|
||||
function Fallback(logger, settings) {
|
||||
this.Logger = logger;
|
||||
if (!this.Logger) this.Logger = new Logging(null, null);
|
||||
// Create audio context
|
||||
if (typeof AudioContext !== "undefined") this.Audio = new AudioContext();
|
||||
else if (typeof webkitAudioContext !== "undefined") this.Audio = new webkitAudioContext();
|
||||
else if (typeof mozAudioContext !== "undefined") this.Audio = new mozAudioContext();
|
||||
else {
|
||||
this.Logger.Log('3LAS: Browser does not support "AudioContext".');
|
||||
throw new Error();
|
||||
}
|
||||
this.Settings = settings;
|
||||
this.Logger.Log("Detected: " +
|
||||
(OSName == "MacOSX" ? "Mac OSX" : (OSName == "Unknown" ? "Unknown OS" : OSName)) + ", " +
|
||||
(BrowserName == "IE" ? "Internet Explorer" : (BrowserName == "NativeChrome" ? "Chrome legacy" : (BrowserName == "Unknown" ? "Unknown Browser" : BrowserName))));
|
||||
this.SelectedFormatMime = "";
|
||||
this.SelectedFormatName = "";
|
||||
for (var i = 0; i < this.Settings.Formats.length; i++) {
|
||||
if (!AudioFormatReader.CanDecodeTypes([this.Settings.Formats[i].Mime]))
|
||||
continue;
|
||||
this.SelectedFormatMime = this.Settings.Formats[i].Mime;
|
||||
this.SelectedFormatName = this.Settings.Formats[i].Name;
|
||||
break;
|
||||
}
|
||||
if (this.SelectedFormatMime == "" || this.SelectedFormatName == "") {
|
||||
this.Logger.Log("None of the available MIME types are supported.");
|
||||
throw new Error();
|
||||
}
|
||||
this.Logger.Log("Using websocket fallback with MIME: " + this.SelectedFormatMime);
|
||||
try {
|
||||
this.Player = new LiveAudioPlayer(this.Audio, this.Logger, this.Settings.MaxVolume, this.Settings.InitialBufferLength, this.Settings.AutoCorrectSpeed);
|
||||
this.Logger.Log("Init of LiveAudioPlayer succeeded");
|
||||
}
|
||||
catch (e) {
|
||||
this.Logger.Log("Init of LiveAudioPlayer failed: " + e);
|
||||
throw new Error();
|
||||
}
|
||||
try {
|
||||
this.FormatReader = AudioFormatReader.Create(this.SelectedFormatMime, this.Audio, this.Logger, this.OnReaderError.bind(this), this.Player.CheckBeforeDecode, this.OnReaderDataReady.bind(this), AudioFormatReader.DefaultSettings());
|
||||
this.Logger.Log("Init of AudioFormatReader succeeded");
|
||||
}
|
||||
catch (e) {
|
||||
this.Logger.Log("Init of AudioFormatReader failed: " + e);
|
||||
throw new Error();
|
||||
}
|
||||
this.PacketModCounter = 0;
|
||||
this.LastCheckTime = 0;
|
||||
this.FocusChecker = 0;
|
||||
}
|
||||
Fallback.prototype.Init = function (webSocket) {
|
||||
this.MobileUnmute();
|
||||
this.WebSocket = webSocket;
|
||||
this.WebSocket?.Send(JSON.stringify({
|
||||
"type": "fallback",
|
||||
"data": this.SelectedFormatName
|
||||
}));
|
||||
this.StartFocusChecker();
|
||||
};
|
||||
Fallback.prototype.MobileUnmute = function () {
|
||||
var amplification = this.Audio.createGain();
|
||||
// Set volume to max
|
||||
amplification.gain.value = 1.0;
|
||||
// Connect gain node to context
|
||||
amplification.connect(this.Audio.destination);
|
||||
// Create one second buffer with silence
|
||||
var audioBuffer = this.Audio.createBuffer(2, this.Audio.sampleRate, this.Audio.sampleRate);
|
||||
// Create new audio source for the buffer
|
||||
var sourceNode = this.Audio.createBufferSource();
|
||||
// Make sure the node deletes itself after playback
|
||||
sourceNode.onended = function (_ev) {
|
||||
sourceNode.disconnect();
|
||||
amplification.disconnect();
|
||||
};
|
||||
// Pass audio data to source
|
||||
sourceNode.buffer = audioBuffer;
|
||||
// Connect the source to the gain node
|
||||
sourceNode.connect(amplification);
|
||||
// Play source
|
||||
sourceNode.start();
|
||||
};
|
||||
Object.defineProperty(Fallback.prototype, "Volume", {
|
||||
get: function () {
|
||||
return this.Player.Volume / this.Settings.MaxVolume;
|
||||
},
|
||||
set: function (value) {
|
||||
this.Player.Volume = value * this.Settings.MaxVolume;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
// Callback functions from format reader
|
||||
Fallback.prototype.OnReaderError = function () {
|
||||
this.Logger.Log("Reader error: Decoding failed.");
|
||||
};
|
||||
Fallback.prototype.OnReaderDataReady = function () {
|
||||
while (this.FormatReader.SamplesAvailable()) {
|
||||
this.Player.PushBuffer(this.FormatReader.PopSamples());
|
||||
}
|
||||
};
|
||||
// Callback function from socket connection
|
||||
Fallback.prototype.OnSocketError = function (message) {};
|
||||
Fallback.prototype.OnSocketConnect = function () {};
|
||||
Fallback.prototype.OnSocketDisconnect = function () {};
|
||||
Fallback.prototype.OnSocketDataReady = function (data) {
|
||||
this.PacketModCounter++;
|
||||
if (this.PacketModCounter > 100) {
|
||||
if (this.ActivityCallback) this.ActivityCallback();
|
||||
this.PacketModCounter = 0;
|
||||
}
|
||||
this.FormatReader.PushData(new Uint8Array(data));
|
||||
};
|
||||
Fallback.prototype.StartFocusChecker = function () {
|
||||
if (!this.FocusChecker) {
|
||||
this.LastCheckTime = Date.now();
|
||||
this.FocusChecker = window.setInterval(this.CheckFocus.bind(this), 2000);
|
||||
}
|
||||
};
|
||||
Fallback.prototype.StopFocusChecker = function () {
|
||||
if (this.FocusChecker) {
|
||||
window.clearInterval(this.FocusChecker);
|
||||
this.FocusChecker = 0;
|
||||
}
|
||||
};
|
||||
Fallback.prototype.CheckFocus = function () {
|
||||
var checkTime = Date.now();
|
||||
// Check if focus was lost
|
||||
if (checkTime - this.LastCheckTime > 10000) {
|
||||
// If so, drop all samples in the buffer
|
||||
this.Logger.Log("Focus lost, purging format reader.");
|
||||
this.FormatReader.PurgeData();
|
||||
}
|
||||
this.LastCheckTime = checkTime;
|
||||
};
|
||||
Fallback.prototype.Reset = function () {
|
||||
this.StopFocusChecker();
|
||||
this.FormatReader.Reset();
|
||||
this.Player.Reset();
|
||||
this.WebSocket = null;
|
||||
};
|
||||
return Fallback;
|
||||
}());
|
||||
@@ -1,193 +0,0 @@
|
||||
/*
|
||||
Audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var AudioFormatReader = /** @class */ (function () {
|
||||
function AudioFormatReader(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) {
|
||||
if (!audio)
|
||||
throw new Error('AudioFormatReader: audio must be specified');
|
||||
// Check callback argument
|
||||
if (typeof errorCallback !== 'function')
|
||||
throw new Error('AudioFormatReader: errorCallback must be specified');
|
||||
if (typeof beforeDecodeCheck !== 'function')
|
||||
throw new Error('AudioFormatReader: beforeDecodeCheck must be specified');
|
||||
if (typeof dataReadyCallback !== 'function')
|
||||
throw new Error('AudioFormatReader: dataReadyCallback must be specified');
|
||||
this.Audio = audio;
|
||||
this.Logger = logger;
|
||||
this.ErrorCallback = errorCallback;
|
||||
this.BeforeDecodeCheck = beforeDecodeCheck;
|
||||
this.DataReadyCallback = dataReadyCallback;
|
||||
this.Id = 0;
|
||||
this.LastPushedId = -1;
|
||||
this.Samples = new Array();
|
||||
this.BufferStore = {};
|
||||
this.DataBuffer = new Uint8Array(0);
|
||||
}
|
||||
// Pushes frame data into the buffer
|
||||
AudioFormatReader.prototype.PushData = function (data) {
|
||||
// Append data to framedata buffer
|
||||
this.DataBuffer = this.ConcatUint8Array(this.DataBuffer, data);
|
||||
// Try to extract frames
|
||||
this.ExtractAll();
|
||||
};
|
||||
// Check if samples are available
|
||||
AudioFormatReader.prototype.SamplesAvailable = function () {
|
||||
return (this.Samples.length > 0);
|
||||
};
|
||||
// Get a single bunch of sampels from the reader
|
||||
AudioFormatReader.prototype.PopSamples = function () {
|
||||
if (this.Samples.length > 0) {
|
||||
// Get first bunch of samples, remove said bunch from the array and hand it back to callee
|
||||
return this.Samples.shift();
|
||||
}
|
||||
else
|
||||
return null;
|
||||
};
|
||||
// Deletes all encoded and decoded data from the reader (does not effect headers, etc.)
|
||||
AudioFormatReader.prototype.PurgeData = function () {
|
||||
this.Id = 0;
|
||||
this.LastPushedId = -1;
|
||||
this.Samples = new Array();
|
||||
this.BufferStore = {};
|
||||
this.DataBuffer = new Uint8Array(0);
|
||||
};
|
||||
// Used to force frame extraction externaly
|
||||
AudioFormatReader.prototype.Poke = function () {
|
||||
this.ExtractAll();
|
||||
};
|
||||
// Deletes all data from the reader (does effect headers, etc.)
|
||||
AudioFormatReader.prototype.Reset = function () {
|
||||
this.PurgeData();
|
||||
};
|
||||
// Extracts and converts the raw data
|
||||
AudioFormatReader.prototype.ExtractAll = function () {
|
||||
};
|
||||
// Checks if a decode makes sense
|
||||
AudioFormatReader.prototype.OnBeforeDecode = function (id, duration) {
|
||||
return true;
|
||||
//TODO Fix this
|
||||
/*
|
||||
if(this.BeforeDecodeCheck(duration)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
this.OnDataReady(id, this.Audio.createBuffer(1, Math.ceil(duration * this.Audio.sampleRate), this.Audio.sampleRate));
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
};
|
||||
// Stores the converted bnuches of samples in right order
|
||||
AudioFormatReader.prototype.OnDataReady = function (id, audioBuffer) {
|
||||
if (this.LastPushedId + 1 == id) {
|
||||
// Push samples into array
|
||||
this.Samples.push(audioBuffer);
|
||||
this.LastPushedId++;
|
||||
while (this.BufferStore[this.LastPushedId + 1]) {
|
||||
// Push samples we decoded earlier in correct order
|
||||
this.Samples.push(this.BufferStore[this.LastPushedId + 1]);
|
||||
delete this.BufferStore[this.LastPushedId + 1];
|
||||
this.LastPushedId++;
|
||||
}
|
||||
// Callback to tell that data is ready
|
||||
this.DataReadyCallback();
|
||||
}
|
||||
else {
|
||||
// Is out of order, will be pushed later
|
||||
this.BufferStore[id] = audioBuffer;
|
||||
}
|
||||
};
|
||||
// Used to concatenate two Uint8Array (b comes BEHIND a)
|
||||
AudioFormatReader.prototype.ConcatUint8Array = function (a, b) {
|
||||
var tmp = new Uint8Array(a.length + b.length);
|
||||
tmp.set(a, 0);
|
||||
tmp.set(b, a.length);
|
||||
return tmp;
|
||||
};
|
||||
AudioFormatReader.CanDecodeTypes = function (mimeTypes) {
|
||||
var audioTag = new Audio();
|
||||
var result = false;
|
||||
for (var i = 0; i < mimeTypes.length; i++) {
|
||||
var mimeType = mimeTypes[i];
|
||||
var answer = audioTag.canPlayType(mimeType);
|
||||
if (answer != "probably" && answer != "maybe")
|
||||
continue;
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
audioTag = null;
|
||||
return result;
|
||||
};
|
||||
AudioFormatReader.DefaultSettings = function () {
|
||||
var settings = {};
|
||||
// WAV
|
||||
settings["wav"] = {};
|
||||
// Duration of wave samples to decode together
|
||||
settings["wav"]["BatchDuration"] = 1 / 10; // 0.1 seconds
|
||||
/*
|
||||
if (isAndroid && isNativeChrome)
|
||||
settings["wav"]["BatchDuration"] = 96 / 375;
|
||||
else if (isAndroid && isFirefox)
|
||||
settings["wav"]["BatchDuration"] = 96 / 375;
|
||||
else
|
||||
settings["wav"]["BatchDuration"] = 16 / 375;
|
||||
*/
|
||||
// Duration of addtional samples to decode to account for edge effects
|
||||
settings["wav"]["ExtraEdgeDuration"] = 1 / 300; // 0.00333... seconds
|
||||
/*
|
||||
if (isAndroid && isNativeChrome)
|
||||
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
|
||||
else if (isAndroid && isFirefox)
|
||||
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
|
||||
else
|
||||
settings["wav"]["ExtraEdgeDuration"] = 1 / 1000;
|
||||
*/
|
||||
// MPEG
|
||||
settings["mpeg"] = {};
|
||||
// Adds a minimal ID3v2 tag before decoding frames.
|
||||
settings["mpeg"]["AddID3Tag"] = false;
|
||||
// Minimum number of frames to decode together
|
||||
// Theoretical minimum is 2.
|
||||
// Recommended value is 3 or higher.
|
||||
if (isAndroid)
|
||||
settings["mpeg"]["MinDecodeFrames"] = 3;
|
||||
else
|
||||
settings["mpeg"]["MinDecodeFrames"] = 3;
|
||||
return settings;
|
||||
};
|
||||
AudioFormatReader.Create = function (mime, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings) {
|
||||
if (settings === void 0) { settings = null; }
|
||||
if (typeof mime !== "string")
|
||||
throw new Error('CreateAudioFormatReader: Invalid MIME-Type, must be string');
|
||||
if (!settings)
|
||||
settings = this.DefaultSettings();
|
||||
var fullMime = mime;
|
||||
if (mime.indexOf("audio/pcm") == 0)
|
||||
mime = "audio/pcm";
|
||||
// Load format handler according to MIME-Type
|
||||
switch (mime.replace(/\s/g, "")) {
|
||||
// MPEG Audio (mp3)
|
||||
case "audio/mpeg":
|
||||
case "audio/MPA":
|
||||
case "audio/mpa-robust":
|
||||
if (!AudioFormatReader.CanDecodeTypes(new Array("audio/mpeg", "audio/MPA", "audio/mpa-robust")))
|
||||
throw new Error('CreateAudioFormatReader: Browser can not decode specified MIME-Type (' + mime + ')');
|
||||
return new AudioFormatReader_MPEG(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings["mpeg"]["AddID3Tag"], settings["mpeg"]["MinDecodeFrames"]);
|
||||
break;
|
||||
// Waveform Audio File Format
|
||||
case "audio/vnd.wave":
|
||||
case "audio/wav":
|
||||
case "audio/wave":
|
||||
case "audio/x-wav":
|
||||
if (!AudioFormatReader.CanDecodeTypes(new Array("audio/wav", "audio/wave")))
|
||||
throw new Error('CreateAudioFormatReader: Browser can not decode specified MIME-Type (' + mime + ')');
|
||||
return new AudioFormatReader_WAV(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, settings["wav"]["BatchDuration"], settings["wav"]["ExtraEdgeDuration"]);
|
||||
break;
|
||||
// Unknown codec
|
||||
default:
|
||||
throw new Error('CreateAudioFormatReader: Specified MIME-Type (' + mime + ') not supported');
|
||||
break;
|
||||
}
|
||||
};
|
||||
return AudioFormatReader;
|
||||
}());
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
Live audio player is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var LiveAudioPlayer = /** @class */ (function () {
|
||||
function LiveAudioPlayer(audio, logger, maxVolume, startOffset, variableSpeed) {
|
||||
if (maxVolume === void 0) { maxVolume = 1.0; }
|
||||
if (startOffset === void 0) { startOffset = 0.33; }
|
||||
if (variableSpeed === void 0) { variableSpeed = false; }
|
||||
this.Audio = audio;
|
||||
this.Logger = logger;
|
||||
this.MaxVolume = maxVolume;
|
||||
this.StartOffset = startOffset;
|
||||
this.VariableSpeed = variableSpeed;
|
||||
this.OffsetMin = this.StartOffset - LiveAudioPlayer.OffsetVariance;
|
||||
this.OffsetMax = this.StartOffset + LiveAudioPlayer.OffsetVariance;
|
||||
// Set speed to default
|
||||
this.PlaybackSpeed = 1.0;
|
||||
// Reset variable for scheduling times
|
||||
this.NextScheduleTime = 0.0;
|
||||
// Create gain node for volume control
|
||||
this.Amplification = this.Audio.createGain();
|
||||
// Set volume to max
|
||||
this.Amplification.gain.value = 1.0;
|
||||
// Connect gain node to context
|
||||
this.Amplification.connect(this.Audio.destination);
|
||||
}
|
||||
Object.defineProperty(LiveAudioPlayer.prototype, "Volume", {
|
||||
get: function () {
|
||||
// Get volume from gain node
|
||||
return this.Amplification.gain.value;
|
||||
},
|
||||
set: function (value) {
|
||||
// Clamp value to [1e-20 ; MaxVolume]
|
||||
if (value > 1.0)
|
||||
value = this.MaxVolume;
|
||||
else if (value <= 0.0)
|
||||
value = 1e-20;
|
||||
// Cancel any scheduled ramps
|
||||
this.Amplification.gain.cancelScheduledValues(this.Audio.currentTime);
|
||||
// Change volume following a ramp (more userfriendly)
|
||||
this.Amplification.gain.exponentialRampToValueAtTime(value, this.Audio.currentTime + 0.5);
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
// Recieves an audiobuffer and schedules it for seamless playback
|
||||
LiveAudioPlayer.prototype.PushBuffer = function (buffer) {
|
||||
// Check if this is the first buffer we received
|
||||
if (this.NextScheduleTime == 0.0) {
|
||||
// Start playing [StartOffset] s from now
|
||||
this.NextScheduleTime = this.Audio.currentTime + this.StartOffset;
|
||||
}
|
||||
var duration;
|
||||
if (this.VariableSpeed)
|
||||
duration = buffer.duration / this.PlaybackSpeed; // Use regular duration
|
||||
else
|
||||
duration = buffer.duration; // Use duration adjusted for playback speed
|
||||
// Before creating a buffer and scheduling playback, check if playing this buffer makes sense at all
|
||||
// If a buffer should have been started so far in the past that it would have finished playing by now, we are better of skipping it.
|
||||
// But we still need to move the time forward to keep future timings right.
|
||||
if (this.NextScheduleTime + duration > this.Audio.currentTime) {
|
||||
var skipDurationTime = void 0;
|
||||
// If the playback start time is in the past but the playback end time is in the future, we need to partially play the buffer.
|
||||
if (this.Audio.currentTime >= this.NextScheduleTime) {
|
||||
// Calculate the time we need to skip
|
||||
skipDurationTime = this.Audio.currentTime - this.NextScheduleTime + 0.05;
|
||||
}
|
||||
else {
|
||||
// No skipping needed
|
||||
skipDurationTime = 0.0;
|
||||
}
|
||||
// Check if we'd skip the whole buffer anyway
|
||||
if (skipDurationTime < duration) {
|
||||
// Create new audio source for the buffer
|
||||
var sourceNode_1 = this.Audio.createBufferSource();
|
||||
// Make sure the node deletes itself after playback
|
||||
sourceNode_1.onended = function (_ev) {
|
||||
sourceNode_1.disconnect();
|
||||
};
|
||||
// Prevent looping (the standard says that it should be off by default)
|
||||
sourceNode_1.loop = false;
|
||||
// Pass audio data to source
|
||||
sourceNode_1.buffer = buffer;
|
||||
//Connect the source to the gain node
|
||||
sourceNode_1.connect(this.Amplification);
|
||||
if (this.VariableSpeed) {
|
||||
var scheduleOffset = this.NextScheduleTime - this.Audio.currentTime;
|
||||
// Check if we are to far or too close to target schedule time
|
||||
if (this.NextScheduleTime - this.Audio.currentTime > this.OffsetMax) {
|
||||
if (this.PlaybackSpeed < 1.0 + LiveAudioPlayer.SpeedCorrectionFactor) {
|
||||
// We are too slow, speed up playback (somewhat noticeable)
|
||||
this.Logger.Log("Buffer size too large, speeding up playback.");
|
||||
this.PlaybackSpeed = 1.0 + LiveAudioPlayer.SpeedCorrectionFactor;
|
||||
duration = buffer.duration / this.PlaybackSpeed;
|
||||
}
|
||||
}
|
||||
else if (this.NextScheduleTime - this.Audio.currentTime < this.OffsetMin) {
|
||||
if (this.PlaybackSpeed > 1.0 - LiveAudioPlayer.SpeedCorrectionFactor) {
|
||||
// We are too fast, slow down playback (somewhat noticeable)
|
||||
this.Logger.Log("Buffer size too small, slowing down playback.");
|
||||
this.PlaybackSpeed = 1.0 - LiveAudioPlayer.SpeedCorrectionFactor;
|
||||
duration = buffer.duration / this.PlaybackSpeed;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Check if we are in time
|
||||
if ((this.PlaybackSpeed > 1.0 && (this.NextScheduleTime - this.Audio.currentTime < this.StartOffset)) ||
|
||||
(this.PlaybackSpeed < 1.0 && (this.NextScheduleTime - this.Audio.currentTime > this.StartOffset))) {
|
||||
// We are within our min/max offset, set playpacks to default
|
||||
this.Logger.Log("Buffer size within limits, using normal playback speed.");
|
||||
this.PlaybackSpeed = 1.0;
|
||||
duration = buffer.duration;
|
||||
}
|
||||
}
|
||||
// Set playback speed
|
||||
sourceNode_1.playbackRate.value = this.PlaybackSpeed;
|
||||
}
|
||||
// Schedule playback
|
||||
sourceNode_1.start(this.NextScheduleTime + skipDurationTime, skipDurationTime);
|
||||
}
|
||||
else {
|
||||
this.Logger.Log("Skipped buffer because it became too old.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Logger.Log("Skipped buffer because it was too old.");
|
||||
}
|
||||
// Move time forward
|
||||
this.NextScheduleTime += duration;
|
||||
};
|
||||
LiveAudioPlayer.prototype.Reset = function () {
|
||||
this.NextScheduleTime = 0.0;
|
||||
};
|
||||
LiveAudioPlayer.prototype.CheckBeforeDecode = function (playbackLength) {
|
||||
if (this.NextScheduleTime == 0)
|
||||
return true;
|
||||
return this.NextScheduleTime + playbackLength > this.Audio.currentTime;
|
||||
};
|
||||
// Crystal oscillator have a variance of about +/- 20ppm
|
||||
// So worst case would be a difference of 40ppm between two oscillators.
|
||||
LiveAudioPlayer.SpeedCorrectionFactor = 40 / 1.0e6;
|
||||
// Hystersis value for speed up/down trigger
|
||||
LiveAudioPlayer.OffsetVariance = 0.2;
|
||||
return LiveAudioPlayer;
|
||||
}());
|
||||
@@ -1,310 +0,0 @@
|
||||
/*
|
||||
MPEG audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
window.startTimeConnectionWatchdog = performance.now();
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
if (typeof b !== "function" && b !== null)
|
||||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var MPEGFrameInfo = /** @class */ (function () {
|
||||
function MPEGFrameInfo(data, sampleCount, sampleRate) {
|
||||
this.Data = data;
|
||||
this.SampleCount = sampleCount;
|
||||
this.SampleRate = sampleRate;
|
||||
}
|
||||
return MPEGFrameInfo;
|
||||
}());
|
||||
var AudioFormatReader_MPEG = /** @class */ (function (_super) {
|
||||
__extends(AudioFormatReader_MPEG, _super);
|
||||
function AudioFormatReader_MPEG(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, addId3Tag, minDecodeFrames) {
|
||||
var _this = _super.call(this, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) || this;
|
||||
_this._OnDecodeSuccess = _this.OnDecodeSuccess.bind(_this);
|
||||
_this._OnDecodeError = _this.OnDecodeError.bind(_this);
|
||||
_this.AddId3Tag = addId3Tag;
|
||||
_this.MinDecodeFrames = minDecodeFrames;
|
||||
_this.Frames = new Array();
|
||||
_this.FrameStartIdx = -1;
|
||||
_this.FrameEndIdx = -1;
|
||||
_this.FrameSamples = 0;
|
||||
_this.FrameSampleRate = 0;
|
||||
_this.TimeBudget = 0;
|
||||
return _this;
|
||||
}
|
||||
// Deletes all frames from the databuffer and framearray and all samples from the samplearray
|
||||
AudioFormatReader_MPEG.prototype.PurgeData = function () {
|
||||
_super.prototype.PurgeData.call(this);
|
||||
this.Frames = new Array();
|
||||
this.FrameStartIdx = -1;
|
||||
this.FrameEndIdx = -1;
|
||||
this.FrameSamples = 0;
|
||||
this.FrameSampleRate = 0;
|
||||
this.TimeBudget = 0;
|
||||
};
|
||||
// Extracts all currently possible frames
|
||||
AudioFormatReader_MPEG.prototype.ExtractAll = function () {
|
||||
// Look for frames
|
||||
this.FindFrame();
|
||||
// Repeat as long as we can extract frames
|
||||
while (this.CanExtractFrame()) {
|
||||
// Extract frame and push into array
|
||||
this.Frames.push(this.ExtractFrame());
|
||||
// Look for frames
|
||||
this.FindFrame();
|
||||
}
|
||||
// Check if we have enough frames to decode
|
||||
if (this.Frames.length >= this.MinDecodeFrames) {
|
||||
// Note:
|
||||
// =====
|
||||
// mp3 frames have an overlap of [granule size] so we can't use the first or last [granule size] samples.
|
||||
// [granule size] is equal to half of a [frame size] in samples (using the mp3's sample rate).
|
||||
// Sum up the playback time of each decoded frame and data buffer lengths
|
||||
// Note: Since mp3-Frames overlap by half of their sample-length we expect the
|
||||
// first and last frame to be only half as long. Some decoders will still output
|
||||
// the full frame length by adding zeros.
|
||||
var bufferLength = 0;
|
||||
var expectedTotalPlayTime_1 = 0;
|
||||
var firstGranulePlayTime_1 = 0;
|
||||
var lastGranulePlayTime_1 = 0;
|
||||
firstGranulePlayTime_1 = this.Frames[0].SampleCount / this.Frames[0].SampleRate / 2.0; // Only half of data is usable due to overlap
|
||||
expectedTotalPlayTime_1 += firstGranulePlayTime_1;
|
||||
bufferLength += this.Frames[0].Data.length;
|
||||
for (var i = 1; i < this.Frames.length - 1; i++) {
|
||||
expectedTotalPlayTime_1 += this.Frames[i].SampleCount / this.Frames[i].SampleRate;
|
||||
bufferLength += this.Frames[i].Data.length;
|
||||
}
|
||||
lastGranulePlayTime_1 = this.Frames[this.Frames.length - 1].SampleCount / this.Frames[this.Frames.length - 1].SampleRate / 2.0; // Only half of data is usable due to overlap
|
||||
expectedTotalPlayTime_1 += lastGranulePlayTime_1;
|
||||
bufferLength += this.Frames[this.Frames.length - 1].Data.length;
|
||||
// If needed, add some space for the ID3v2 tag
|
||||
if (this.AddId3Tag) bufferLength += AudioFormatReader_MPEG.Id3v2Tag.length;
|
||||
// Create a buffer long enough to hold everything
|
||||
var decodeBuffer = new Uint8Array(bufferLength);
|
||||
var offset = 0;
|
||||
// If needed, add ID3v2 tag to beginning of buffer
|
||||
if (this.AddId3Tag) {
|
||||
decodeBuffer.set(AudioFormatReader_MPEG.Id3v2Tag, offset);
|
||||
offset += AudioFormatReader_MPEG.Id3v2Tag.length;
|
||||
}
|
||||
// Add the frames to the window
|
||||
for (var i = 0; i < this.Frames.length; i++) {
|
||||
decodeBuffer.set(this.Frames[i].Data, offset);
|
||||
offset += this.Frames[i].Data.length;
|
||||
}
|
||||
// Remove the used frames from the array
|
||||
this.Frames.splice(0, this.Frames.length - 1);
|
||||
// Increment Id
|
||||
var id_1 = this.Id++;
|
||||
// Check if decoded frames might be too far back in the past
|
||||
if (!this.OnBeforeDecode(id_1, expectedTotalPlayTime_1)) return;
|
||||
// Push window to the decoder
|
||||
this.Audio.decodeAudioData(decodeBuffer.buffer, (function (decodedData) {
|
||||
var _id = id_1;
|
||||
var _expectedTotalPlayTime = expectedTotalPlayTime_1;
|
||||
var _firstGranulePlayTime = firstGranulePlayTime_1;
|
||||
var _lastGranulePlayTime = lastGranulePlayTime_1;
|
||||
this._OnDecodeSuccess(decodedData, _id, _expectedTotalPlayTime, _firstGranulePlayTime, _lastGranulePlayTime);
|
||||
}).bind(this), this._OnDecodeError.bind(this));
|
||||
}
|
||||
};
|
||||
// Finds frame boundries within the data buffer
|
||||
AudioFormatReader_MPEG.prototype.FindFrame = function () {
|
||||
// Find frame start
|
||||
if (this.FrameStartIdx < 0) {
|
||||
var i = 0;
|
||||
// Make sure we don't exceed array bounds
|
||||
while ((i + 1) < this.DataBuffer.length) {
|
||||
// Look for MPEG sync word
|
||||
if (this.DataBuffer[i] == 0xFF && (this.DataBuffer[i + 1] & 0xE0) == 0xE0) {
|
||||
// Sync found, set frame start
|
||||
this.FrameStartIdx = i;
|
||||
break;
|
||||
} i++;
|
||||
}
|
||||
}
|
||||
// Find frame end
|
||||
if (this.FrameStartIdx >= 0 && this.FrameEndIdx < 0) {
|
||||
// Check if we have enough data to process the header
|
||||
if ((this.FrameStartIdx + 2) < this.DataBuffer.length) {
|
||||
// Get header data
|
||||
// Version index
|
||||
var ver = (this.DataBuffer[this.FrameStartIdx + 1] & 0x18) >>> 3;
|
||||
// Layer index
|
||||
var lyr = (this.DataBuffer[this.FrameStartIdx + 1] & 0x06) >>> 1;
|
||||
// Padding? 0/1
|
||||
var pad = (this.DataBuffer[this.FrameStartIdx + 2] & 0x02) >>> 1;
|
||||
// Bitrate index
|
||||
var brx = (this.DataBuffer[this.FrameStartIdx + 2] & 0xF0) >>> 4;
|
||||
// SampRate index
|
||||
var srx = (this.DataBuffer[this.FrameStartIdx + 2] & 0x0C) >>> 2;
|
||||
// Resolve flags to real values
|
||||
var bitrate = AudioFormatReader_MPEG.MPEG_bitrates[ver][lyr][brx] * 1000;
|
||||
var samprate = AudioFormatReader_MPEG.MPEG_srates[ver][srx];
|
||||
var samples = AudioFormatReader_MPEG.MPEG_frame_samples[ver][lyr];
|
||||
var slot_size = AudioFormatReader_MPEG.MPEG_slot_size[lyr];
|
||||
// In-between calculations
|
||||
var bps = samples / 8.0;
|
||||
var fsize = ((bps * bitrate) / samprate) + ((pad == 1) ? slot_size : 0);
|
||||
// Truncate to integer
|
||||
var frameSize = Math.floor(fsize);
|
||||
// Store number of samples and samplerate for frame
|
||||
this.FrameSamples = samples;
|
||||
this.FrameSampleRate = samprate;
|
||||
// Set end frame boundry
|
||||
this.FrameEndIdx = this.FrameStartIdx + frameSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Checks if there is a frame ready to be extracted
|
||||
AudioFormatReader_MPEG.prototype.CanExtractFrame = function () {
|
||||
if (this.FrameStartIdx < 0 || this.FrameEndIdx < 0) return false;
|
||||
else if (this.FrameEndIdx <= this.DataBuffer.length) return true;
|
||||
else return false;
|
||||
};
|
||||
// Extract a single frame from the buffer
|
||||
AudioFormatReader_MPEG.prototype.ExtractFrame = function () {
|
||||
// Extract frame data from buffer
|
||||
var frameArray = this.DataBuffer.buffer.slice(this.FrameStartIdx, this.FrameEndIdx);
|
||||
// Remove frame from buffer
|
||||
if ((this.FrameEndIdx + 1) < this.DataBuffer.length) this.DataBuffer = new Uint8Array(this.DataBuffer.buffer.slice(this.FrameEndIdx));
|
||||
else this.DataBuffer = new Uint8Array(0);
|
||||
// Reset Start/End indices
|
||||
this.FrameStartIdx = 0;
|
||||
this.FrameEndIdx = -1;
|
||||
return new MPEGFrameInfo(new Uint8Array(frameArray), this.FrameSamples, this.FrameSampleRate);
|
||||
};
|
||||
// Is called if the decoding of the window succeeded
|
||||
AudioFormatReader_MPEG.prototype.OnDecodeSuccess = function (decodedData, id, expectedTotalPlayTime, firstGranulePlayTime, lastGranulePlayTime) {
|
||||
window.startTimeConnectionWatchdog = performance.now();
|
||||
var extractSampleCount;
|
||||
var extractSampleOffset;
|
||||
var delta = 0.001;
|
||||
// Check if we got the expected number of samples
|
||||
if (expectedTotalPlayTime > decodedData.duration) {
|
||||
// We got less samples than expect, we suspect that they were truncated equally at start and end.
|
||||
// This can happen in case of sample rate conversions.
|
||||
extractSampleCount = decodedData.length;
|
||||
extractSampleOffset = 0;
|
||||
this.TimeBudget += (expectedTotalPlayTime - decodedData.duration);
|
||||
}
|
||||
else if (expectedTotalPlayTime < decodedData.duration) {
|
||||
// We got more samples than expect, we suspect that zeros were added equally at start and end.
|
||||
// This can happen in case of sample rate conversions or edge frame handling.
|
||||
extractSampleCount = Math.ceil(expectedTotalPlayTime * decodedData.sampleRate);
|
||||
var budgetSamples = this.TimeBudget * decodedData.sampleRate;
|
||||
if (budgetSamples > 1.0) {
|
||||
if (budgetSamples > decodedData.length - extractSampleCount) {
|
||||
budgetSamples = decodedData.length - extractSampleCount;
|
||||
}
|
||||
extractSampleCount += budgetSamples;
|
||||
this.TimeBudget -= (budgetSamples / decodedData.sampleRate);
|
||||
}
|
||||
var diff = decodedData.duration - expectedTotalPlayTime;
|
||||
if ((diff - firstGranulePlayTime - lastGranulePlayTime) >= -delta) {
|
||||
// Both first and last granule are present. Cut out middle section.
|
||||
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
|
||||
}
|
||||
if (Math.abs(firstGranulePlayTime - lastGranulePlayTime) <= delta) {
|
||||
// First and last granule are equal. We need to make an educated guess which one is present.
|
||||
if (isIOS || isIPadOS || isMacOSX) {
|
||||
// I don't know why, but Apple does things differently.
|
||||
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
|
||||
}
|
||||
else {
|
||||
// Assume last granule is present.
|
||||
extractSampleOffset = decodedData.length - extractSampleCount;
|
||||
}
|
||||
}
|
||||
else if (Math.abs(diff - firstGranulePlayTime) <= delta) {
|
||||
// Last granule is present.
|
||||
extractSampleOffset = decodedData.length - extractSampleCount;
|
||||
}
|
||||
else if (Math.abs(diff - lastGranulePlayTime) <= delta) {
|
||||
// First granule is present.
|
||||
extractSampleOffset = 0;
|
||||
}
|
||||
else {
|
||||
// The difference is not equal to neither the first, the last nor the sum of both granule. So we just use the middle.
|
||||
extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We got the expected number of samples, no adaption needed
|
||||
extractSampleCount = decodedData.length;
|
||||
extractSampleOffset = 0;
|
||||
}
|
||||
// Create a buffer that can hold the frame to extract
|
||||
var audioBuffer = this.Audio.createBuffer(decodedData.numberOfChannels, extractSampleCount, decodedData.sampleRate);
|
||||
// Fill buffer with the last part of the decoded frame leave out last granule
|
||||
for (var i = 0; i < decodedData.numberOfChannels; i++)
|
||||
audioBuffer.getChannelData(i).set(decodedData.getChannelData(i).subarray(extractSampleOffset, extractSampleOffset + extractSampleCount));
|
||||
this.OnDataReady(id, audioBuffer);
|
||||
};
|
||||
// Is called in case the decoding of the window fails
|
||||
AudioFormatReader_MPEG.prototype.OnDecodeError = function (_error) {
|
||||
this.ErrorCallback();
|
||||
};
|
||||
// MPEG versions - use [version]
|
||||
AudioFormatReader_MPEG.MPEG_versions = new Array(25, 0, 2, 1);
|
||||
// Layers - use [layer]
|
||||
AudioFormatReader_MPEG.MPEG_layers = new Array(0, 3, 2, 1);
|
||||
// Bitrates - use [version][layer][bitrate]
|
||||
AudioFormatReader_MPEG.MPEG_bitrates = new Array(new Array(// Version 2.5
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
|
||||
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3
|
||||
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2
|
||||
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1
|
||||
), new Array(// Reserved
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // Invalid
|
||||
), new Array(// Version 2
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
|
||||
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3
|
||||
new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2
|
||||
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1
|
||||
), new Array(// Version 1
|
||||
new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved
|
||||
new Array(0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0), // Layer 3
|
||||
new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0), // Layer 2
|
||||
new Array(0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0) // Layer 1
|
||||
));
|
||||
// Sample rates - use [version][srate]
|
||||
AudioFormatReader_MPEG.MPEG_srates = new Array(new Array(11025, 12000, 8000, 0), // MPEG 2.5
|
||||
new Array(0, 0, 0, 0), // Reserved
|
||||
new Array(22050, 24000, 16000, 0), // MPEG 2
|
||||
new Array(44100, 48000, 32000, 0) // MPEG 1
|
||||
);
|
||||
// Samples per frame - use [version][layer]
|
||||
AudioFormatReader_MPEG.MPEG_frame_samples = new Array(
|
||||
// Rsvd 3 2 1 < Layer v Version
|
||||
new Array(0, 576, 1152, 384), // 2.5
|
||||
new Array(0, 0, 0, 0), // Reserved
|
||||
new Array(0, 576, 1152, 384), // 2
|
||||
new Array(0, 1152, 1152, 384) // 1
|
||||
);
|
||||
AudioFormatReader_MPEG.Id3v2Tag = new Uint8Array(new Array(0x49, 0x44, 0x33, // File identifier: "ID3"
|
||||
0x03, 0x00, // Version 2.3
|
||||
0x00, // Flags: no unsynchronisation, no extended header, no experimental indicator
|
||||
0x00, 0x00, 0x00, 0x0D, // Size of the (tag-)frames, extended header and padding
|
||||
0x54, 0x49, 0x54, 0x32, // Title frame: "TIT2"
|
||||
0x00, 0x00, 0x00, 0x02, // Size of the frame data
|
||||
0x00, 0x00, // Frame Flags
|
||||
0x00, 0x20, 0x00 // Frame data (space character) and padding
|
||||
));
|
||||
// Slot size (MPEG unit of measurement) - use [layer]
|
||||
AudioFormatReader_MPEG.MPEG_slot_size = new Array(0, 1, 1, 4); // Rsvd, 3, 2, 1
|
||||
return AudioFormatReader_MPEG;
|
||||
}(AudioFormatReader));
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
WAV audio format reader is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
if (typeof b !== "function" && b !== null)
|
||||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var AudioFormatReader_WAV = /** @class */ (function (_super) {
|
||||
__extends(AudioFormatReader_WAV, _super);
|
||||
function AudioFormatReader_WAV(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, batchDuration, extraEdgeDuration) {
|
||||
var _this = _super.call(this, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) || this;
|
||||
_this._OnDecodeSuccess = _this.OnDecodeSuccess.bind(_this);
|
||||
_this._OnDecodeError = _this.OnDecodeError.bind(_this);
|
||||
_this.BatchDuration = batchDuration;
|
||||
_this.ExtraEdgeDuration = extraEdgeDuration;
|
||||
_this.GotHeader = false;
|
||||
_this.RiffHeader = null;
|
||||
_this.WaveSampleRate = 0;
|
||||
_this.WaveBitsPerSample = 0;
|
||||
_this.WaveBytesPerSample = 0;
|
||||
_this.WaveBlockAlign = 0;
|
||||
_this.WaveChannels = 0;
|
||||
_this.BatchSamples = 0;
|
||||
_this.BatchBytes = 0;
|
||||
_this.ExtraEdgeSamples = 0;
|
||||
_this.TotalBatchSampleSize = 0;
|
||||
_this.TotalBatchByteSize = 0;
|
||||
_this.SampleBudget = 0;
|
||||
return _this;
|
||||
}
|
||||
// Deletes all samples from the databuffer and the samplearray
|
||||
AudioFormatReader_WAV.prototype.PurgeData = function () {
|
||||
_super.prototype.PurgeData.call(this);
|
||||
this.SampleBudget = 0;
|
||||
};
|
||||
// Deletes all data from the reader (deos effect headers, etc.)
|
||||
AudioFormatReader_WAV.prototype.Reset = function () {
|
||||
_super.prototype.Reset.call(this);
|
||||
this.GotHeader = false;
|
||||
this.RiffHeader = null;
|
||||
this.WaveSampleRate = 0;
|
||||
this.WaveBitsPerSample = 0;
|
||||
this.WaveBytesPerSample = 0;
|
||||
this.WaveBlockAlign = 0;
|
||||
this.WaveChannels = 0;
|
||||
this.BatchSamples = 0;
|
||||
this.BatchBytes = 0;
|
||||
this.ExtraEdgeSamples = 0;
|
||||
this.TotalBatchSampleSize = 0;
|
||||
this.TotalBatchByteSize = 0;
|
||||
this.SampleBudget = 0;
|
||||
};
|
||||
AudioFormatReader_WAV.prototype.ExtractAll = function () {
|
||||
if (!this.GotHeader)
|
||||
this.FindAndExtractHeader();
|
||||
else {
|
||||
var _loop_1 = function () {
|
||||
// Extract samples
|
||||
var tmpSamples = this_1.ExtractIntSamples();
|
||||
// Increment Id
|
||||
var id = this_1.Id++;
|
||||
if (!this_1.OnBeforeDecode(id, this_1.BatchDuration))
|
||||
return "continue";
|
||||
// Note:
|
||||
// =====
|
||||
// When audio data is resampled we get edge-effects at beginnging and end.
|
||||
// We should be able to compensate for that by keeping the last sample of the
|
||||
// previous batch and adding it to the beginning of the current one, but then
|
||||
// cutting it out AFTER the resampling (since the same effects apply to it)
|
||||
// The effects at the end can be compensated by cutting the resampled samples shorter
|
||||
// This is not trivial for non-natural ratios (e.g. 16kHz -> 44.1kHz). Because we would have
|
||||
// to cut out a non-natural number of samples at beginning and end.
|
||||
// TODO: All of the above...
|
||||
// Create a buffer long enough to hold everything
|
||||
var samplesBuffer = new Uint8Array(this_1.RiffHeader.length + tmpSamples.length);
|
||||
var offset = 0;
|
||||
// Add header
|
||||
samplesBuffer.set(this_1.RiffHeader, offset);
|
||||
offset += this_1.RiffHeader.length;
|
||||
// Add samples
|
||||
samplesBuffer.set(tmpSamples, offset);
|
||||
// Push pages to the decoder
|
||||
this_1.Audio.decodeAudioData(samplesBuffer.buffer, (function (decodedData) {
|
||||
var _id = id;
|
||||
this._OnDecodeSuccess(decodedData, _id);
|
||||
}).bind(this_1), this_1._OnDecodeError);
|
||||
};
|
||||
var this_1 = this;
|
||||
while (this.CanExtractSamples()) {
|
||||
_loop_1();
|
||||
}
|
||||
}
|
||||
};
|
||||
// Finds riff header within the data buffer and extracts it
|
||||
AudioFormatReader_WAV.prototype.FindAndExtractHeader = function () {
|
||||
var curpos = 0;
|
||||
// Make sure a whole header can fit
|
||||
if (!((curpos + 4) < this.DataBuffer.length))
|
||||
return;
|
||||
// Check chunkID, should be "RIFF"
|
||||
if (!(this.DataBuffer[curpos] == 0x52 && this.DataBuffer[curpos + 1] == 0x49 && this.DataBuffer[curpos + 2] == 0x46 && this.DataBuffer[curpos + 3] == 0x46))
|
||||
return;
|
||||
curpos += 8;
|
||||
if (!((curpos + 4) < this.DataBuffer.length))
|
||||
return;
|
||||
// Check riffType, should be "WAVE"
|
||||
if (!(this.DataBuffer[curpos] == 0x57 && this.DataBuffer[curpos + 1] == 0x41 && this.DataBuffer[curpos + 2] == 0x56 && this.DataBuffer[curpos + 3] == 0x45))
|
||||
return;
|
||||
curpos += 4;
|
||||
if (!((curpos + 4) < this.DataBuffer.length))
|
||||
return;
|
||||
// Check for format subchunk, should be "fmt "
|
||||
if (!(this.DataBuffer[curpos] == 0x66 && this.DataBuffer[curpos + 1] == 0x6d && this.DataBuffer[curpos + 2] == 0x74 && this.DataBuffer[curpos + 3] == 0x20))
|
||||
return;
|
||||
curpos += 4;
|
||||
if (!((curpos + 4) < this.DataBuffer.length))
|
||||
return;
|
||||
var subChunkSize = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8 | this.DataBuffer[curpos + 2] << 16 | this.DataBuffer[curpos + 3] << 24;
|
||||
if (!((curpos + 4 + subChunkSize) < this.DataBuffer.length))
|
||||
return;
|
||||
curpos += 6;
|
||||
this.WaveChannels = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
|
||||
curpos += 2;
|
||||
this.WaveSampleRate = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8 | this.DataBuffer[curpos + 2] << 16 | this.DataBuffer[curpos + 3] << 24;
|
||||
curpos += 8;
|
||||
this.WaveBlockAlign = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
|
||||
curpos += 2;
|
||||
this.WaveBitsPerSample = this.DataBuffer[curpos] | this.DataBuffer[curpos + 1] << 8;
|
||||
this.WaveBytesPerSample = this.WaveBitsPerSample / 8;
|
||||
curpos += subChunkSize - 14;
|
||||
while (true) {
|
||||
if ((curpos + 8) < this.DataBuffer.length) {
|
||||
subChunkSize = this.DataBuffer[curpos + 4] | this.DataBuffer[curpos + 5] << 8 | this.DataBuffer[curpos + 6] << 16 | this.DataBuffer[curpos + 7] << 24;
|
||||
// Check for data subchunk, should be "data"
|
||||
if (this.DataBuffer[curpos] == 0x64 && this.DataBuffer[curpos + 1] == 0x61 && this.DataBuffer[curpos + 2] == 0x74 && this.DataBuffer[curpos + 3] == 0x61) // Data chunk found
|
||||
break;
|
||||
else
|
||||
curpos += 8 + subChunkSize;
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
curpos += 8;
|
||||
this.RiffHeader = new Uint8Array(this.DataBuffer.buffer.slice(0, curpos));
|
||||
this.BatchSamples = Math.ceil(this.BatchDuration * this.WaveSampleRate);
|
||||
this.ExtraEdgeSamples = Math.ceil(this.ExtraEdgeDuration * this.WaveSampleRate);
|
||||
this.BatchBytes = this.BatchSamples * this.WaveBlockAlign;
|
||||
this.TotalBatchSampleSize = (this.BatchSamples + this.ExtraEdgeSamples);
|
||||
this.TotalBatchByteSize = this.TotalBatchSampleSize * this.WaveBlockAlign;
|
||||
var chunkSize = this.RiffHeader.length + this.TotalBatchByteSize - 8;
|
||||
// Fix header chunksizes
|
||||
this.RiffHeader[4] = chunkSize & 0xFF;
|
||||
this.RiffHeader[5] = (chunkSize & 0xFF00) >>> 8;
|
||||
this.RiffHeader[6] = (chunkSize & 0xFF0000) >>> 16;
|
||||
this.RiffHeader[7] = (chunkSize & 0xFF000000) >>> 24;
|
||||
this.RiffHeader[this.RiffHeader.length - 4] = (this.TotalBatchByteSize & 0xFF);
|
||||
this.RiffHeader[this.RiffHeader.length - 3] = (this.TotalBatchByteSize & 0xFF00) >>> 8;
|
||||
this.RiffHeader[this.RiffHeader.length - 2] = (this.TotalBatchByteSize & 0xFF0000) >>> 16;
|
||||
this.RiffHeader[this.RiffHeader.length - 1] = (this.TotalBatchByteSize & 0xFF000000) >>> 24;
|
||||
this.GotHeader = true;
|
||||
};
|
||||
// Checks if there is a samples ready to be extracted
|
||||
AudioFormatReader_WAV.prototype.CanExtractSamples = function () {
|
||||
if (this.DataBuffer.length >= this.TotalBatchByteSize)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
};
|
||||
// Extract a single batch of samples from the buffer
|
||||
AudioFormatReader_WAV.prototype.ExtractIntSamples = function () {
|
||||
// Extract sample data from buffer
|
||||
var intSampleArray = new Uint8Array(this.DataBuffer.buffer.slice(0, this.TotalBatchByteSize));
|
||||
// Remove samples from buffer
|
||||
this.DataBuffer = new Uint8Array(this.DataBuffer.buffer.slice(this.BatchBytes));
|
||||
return intSampleArray;
|
||||
};
|
||||
// Is called if the decoding of the samples succeeded
|
||||
AudioFormatReader_WAV.prototype.OnDecodeSuccess = function (decodedData, id) {
|
||||
// Calculate the length of the parts
|
||||
var pickSize = this.BatchDuration * decodedData.sampleRate;
|
||||
this.SampleBudget += (pickSize - Math.ceil(pickSize));
|
||||
pickSize = Math.ceil(pickSize);
|
||||
var pickOffset = (decodedData.length - pickSize) / 2.0;
|
||||
if (pickOffset < 0)
|
||||
pickOffset = 0; // This should never happen!
|
||||
else
|
||||
pickOffset = Math.floor(pickOffset);
|
||||
if (this.SampleBudget < -1.0) {
|
||||
var correction = -1.0 * Math.floor(Math.abs(this.SampleBudget));
|
||||
this.SampleBudget -= correction;
|
||||
pickSize += correction;
|
||||
}
|
||||
else if (this.SampleBudget > 1.0) {
|
||||
var correction = Math.floor(this.SampleBudget);
|
||||
this.SampleBudget -= correction;
|
||||
pickSize += correction;
|
||||
}
|
||||
// Create a buffer that can hold a single part
|
||||
var audioBuffer = this.Audio.createBuffer(decodedData.numberOfChannels, pickSize, decodedData.sampleRate);
|
||||
// Fill buffer with the last part of the decoded frame
|
||||
for (var i = 0; i < decodedData.numberOfChannels; i++)
|
||||
audioBuffer.getChannelData(i).set(decodedData.getChannelData(i).slice(pickOffset, -pickOffset));
|
||||
this.OnDataReady(id, audioBuffer);
|
||||
};
|
||||
// Is called in case the decoding of the window fails
|
||||
AudioFormatReader_WAV.prototype.OnDecodeError = function (_error) {
|
||||
this.ErrorCallback();
|
||||
};
|
||||
return AudioFormatReader_WAV;
|
||||
}(AudioFormatReader));
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
Helpers is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var isAndroid;
|
||||
var isIOS;
|
||||
var isIPadOS;
|
||||
var isWindows;
|
||||
var isLinux;
|
||||
var isBSD;
|
||||
var isMacOSX;
|
||||
var isInternetExplorer;
|
||||
var isEdge;
|
||||
;
|
||||
var isSafari;
|
||||
;
|
||||
var isOpera;
|
||||
;
|
||||
var isChrome;
|
||||
;
|
||||
var isFirefox;
|
||||
;
|
||||
var webkitVer;
|
||||
var isNativeChrome;
|
||||
;
|
||||
var BrowserName;
|
||||
var OSName;
|
||||
{
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
isAndroid = (ua.match('android') ? true : false);
|
||||
isIOS = (ua.match(/(iphone|ipod)/g) ? true : false);
|
||||
isIPadOS = ((ua.match('ipad') || (navigator.platform == 'MacIntel' && navigator.maxTouchPoints > 1)) ? true : false);
|
||||
isWindows = (ua.match('windows') ? true : false);
|
||||
isLinux = (ua.match('android') ? false : (ua.match('linux') ? true : false));
|
||||
isBSD = (ua.match('bsd') ? true : false);
|
||||
isMacOSX = !isIOS && !isIPadOS && (ua.match('mac os x') ? true : false);
|
||||
isInternetExplorer = (ua.match('msie') ? true : false);
|
||||
isEdge = (ua.match('edg') ? true : false);
|
||||
isSafari = (ua.match(/(chromium|chrome|crios)/g) ? false : (ua.match('safari') ? true : false));
|
||||
isOpera = (ua.match('opera') ? true : false);
|
||||
isChrome = !isSafari && (ua.match(/(chromium|chrome|crios)/g) ? true : false);
|
||||
isFirefox = (ua.match('like gecko') ? false : (ua.match(/(gecko|fennec|firefox)/g) ? true : false));
|
||||
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";
|
||||
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 () {
|
||||
function WakeLock(logger) {
|
||||
this.Logger = logger;
|
||||
this.Logger.Log("Preparing WakeLock");
|
||||
if (typeof navigator.wakeLock == "undefined") {
|
||||
this.Logger.Log("Using video loop method.");
|
||||
var video = document.createElement('video');
|
||||
video.setAttribute('loop', '');
|
||||
video.setAttribute('style', 'position: fixed; opacity: 0.1; pointer-events: none;');
|
||||
WakeLock.AddSourceToVideo(video, 'webm', 'data:video/webm;base64,' + WakeLock.VideoWebm);
|
||||
WakeLock.AddSourceToVideo(video, 'mp4', 'data:video/mp4;base64,' + WakeLock.VideoMp4);
|
||||
document.body.appendChild(video);
|
||||
this.LockElement = video;
|
||||
}
|
||||
else {
|
||||
this.Logger.Log("Using WakeLock API.");
|
||||
this.LockElement = null;
|
||||
}
|
||||
}
|
||||
WakeLock.prototype.Begin = function () {
|
||||
var _this = this;
|
||||
if (this.LockElement == null) {
|
||||
try {
|
||||
navigator.wakeLock.request("screen").then(function (obj) {
|
||||
_this.Logger.Log("WakeLock request successful. Lock acquired.");
|
||||
_this.LockElement = obj; // Not an audio/video element
|
||||
console.log("WakeLock request successful.");
|
||||
}, function () {
|
||||
_this.Logger.Log("WakeLock request failed.");
|
||||
console.log("WakeLock request failed.");
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
this.Logger.Log("WakeLock request failed.");
|
||||
console.log("WakeLock request failed.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.Logger.Log("WakeLock video loop started.");
|
||||
|
||||
// Ensure it's an audio/video element before calling play()
|
||||
if (_this.LockElement instanceof HTMLMediaElement) {
|
||||
_this.LockElement.play().catch(err => {
|
||||
console.error("LockElement failed:", err);
|
||||
});
|
||||
} else {
|
||||
console.warn("LockElement not a media element or already assigned.");
|
||||
}
|
||||
}
|
||||
};
|
||||
WakeLock.AddSourceToVideo = function (element, type, dataURI) {
|
||||
var source = document.createElement('source');
|
||||
source.src = dataURI;
|
||||
source.type = 'video/' + type;
|
||||
element.appendChild(source);
|
||||
};
|
||||
WakeLock.VideoWebm = 'GkXfo0AgQoaBAUL3gQFC8oEEQvOBCEKCQAR3ZWJtQoeBAkKFgQIYU4BnQI0VSalmQCgq17FAAw9CQE2AQAZ3aGFtbXlXQUAGd2hhbW15RIlACECPQAAAAAAAFlSua0AxrkAu14EBY8WBAZyBACK1nEADdW5khkAFVl9WUDglhohAA1ZQOIOBAeBABrCBCLqBCB9DtnVAIueBAKNAHIEAAIAwAQCdASoIAAgAAUAmJaQAA3AA/vz0AAA=';
|
||||
WakeLock.VideoMp4 = 'AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAAAAhmcmVlAAAAG21kYXQAAAGzABAHAAABthADAowdbb9/AAAC6W1vb3YAAABsbXZoZAAAAAB8JbCAfCWwgAAAA+gAAAAAAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIVdHJhawAAAFx0a2hkAAAAD3wlsIB8JbCAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAIAAAACAAAAAABsW1kaWEAAAAgbWRoZAAAAAB8JbCAfCWwgAAAA+gAAAAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAVxtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAEcc3RibAAAALhzdHNkAAAAAAAAAAEAAACobXA0dgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAIAAgASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAFJlc2RzAAAAAANEAAEABDwgEQAAAAADDUAAAAAABS0AAAGwAQAAAbWJEwAAAQAAAAEgAMSNiB9FAEQBFGMAAAGyTGF2YzUyLjg3LjQGAQIAAAAYc3R0cwAAAAAAAAABAAAAAQAAAAAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAAAEwAAAAEAAAAUc3RjbwAAAAAAAAABAAAALAAAAGB1ZHRhAAAAWG1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAK2lsc3QAAAAjqXRvbwAAABtkYXRhAAAAAQAAAABMYXZmNTIuNzguMw==';
|
||||
return WakeLock;
|
||||
}());
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
Logging is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var Logging = /** @class */ (function () {
|
||||
function Logging(parentElement, childElementType) {
|
||||
this.ParentElement = parentElement;
|
||||
this.ChildElementType = childElementType;
|
||||
}
|
||||
Logging.prototype.Log = function (message) {
|
||||
var dateTime = new Date();
|
||||
var lineText = "[" + (dateTime.getHours() > 9 ? dateTime.getHours() : "0" + dateTime.getHours()) + ":" +
|
||||
(dateTime.getMinutes() > 9 ? dateTime.getMinutes() : "0" + dateTime.getMinutes()) + ":" +
|
||||
(dateTime.getSeconds() > 9 ? dateTime.getSeconds() : "0" + dateTime.getSeconds()) +
|
||||
"] " + message;
|
||||
if (this.ParentElement && this.ChildElementType) {
|
||||
var line = document.createElement(this.ChildElementType);
|
||||
line.innerText = lineText;
|
||||
this.ParentElement.appendChild(line);
|
||||
}
|
||||
else {
|
||||
//console.log(lineText);
|
||||
}
|
||||
};
|
||||
return Logging;
|
||||
}());
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
WebSocket client is part of 3LAS (Low Latency Live Audio Streaming)
|
||||
https://github.com/JoJoBond/3LAS
|
||||
*/
|
||||
var WebSocketClient = /** @class */ (function () {
|
||||
function WebSocketClient(logger, uri, errorCallback, connectCallback, dataReadyCallback, disconnectCallback) {
|
||||
this.Logger = logger;
|
||||
this.Uri = uri;
|
||||
// Check callback argument
|
||||
if (typeof errorCallback !== 'function')
|
||||
throw new Error('WebSocketClient: ErrorCallback must be specified');
|
||||
if (typeof connectCallback !== 'function')
|
||||
throw new Error('WebSocketClient: ConnectCallback must be specified');
|
||||
if (typeof dataReadyCallback !== 'function')
|
||||
throw new Error('WebSocketClient: DataReadyCallback must be specified');
|
||||
if (typeof disconnectCallback !== 'function')
|
||||
throw new Error('WebSocketClient: DisconnectCallback must be specified');
|
||||
this.ErrorCallback = errorCallback;
|
||||
this.ConnectCallback = connectCallback;
|
||||
this.DataReadyCallback = dataReadyCallback;
|
||||
this.DisconnectCallback = disconnectCallback;
|
||||
// Client is not yet connected
|
||||
this.IsConnected = false;
|
||||
// Create socket, connect to URI
|
||||
if (typeof WebSocket !== "undefined")
|
||||
this.Socket = new WebSocket(this.Uri);
|
||||
else if (typeof webkitWebSocket !== "undefined")
|
||||
this.Socket = new webkitWebSocket(this.Uri);
|
||||
else if (typeof mozWebSocket !== "undefined")
|
||||
this.Socket = new mozWebSocket(this.Uri);
|
||||
else
|
||||
throw new Error('WebSocketClient: Browser does not support "WebSocket".');
|
||||
this.Socket.binaryType = 'arraybuffer';
|
||||
this.Socket.addEventListener("open", this.OnOpen.bind(this));
|
||||
this.Socket.addEventListener("error", this.OnError.bind(this));
|
||||
this.Socket.addEventListener("close", this.OnClose.bind(this));
|
||||
this.Socket.addEventListener("message", this.OnMessage.bind(this));
|
||||
}
|
||||
Object.defineProperty(WebSocketClient.prototype, "Connected", {
|
||||
get: function () {
|
||||
return this.IsConnected;
|
||||
},
|
||||
enumerable: false,
|
||||
configurable: true
|
||||
});
|
||||
WebSocketClient.prototype.Send = function (message) {
|
||||
if (!this.IsConnected)
|
||||
return;
|
||||
this.Socket.send(message);
|
||||
};
|
||||
// Handle errors
|
||||
WebSocketClient.prototype.OnError = function (_ev) {
|
||||
if (this.IsConnected == true)
|
||||
this.ErrorCallback("Socket fault.");
|
||||
else
|
||||
this.ErrorCallback("Could not connect to server.");
|
||||
};
|
||||
// Change connetion status once connected
|
||||
WebSocketClient.prototype.OnOpen = function (_ev) {
|
||||
if (this.Socket.readyState == 1) {
|
||||
this.IsConnected = true;
|
||||
this.ConnectCallback();
|
||||
}
|
||||
};
|
||||
// Change connetion status on disconnect
|
||||
WebSocketClient.prototype.OnClose = function (_ev) {
|
||||
if (this.IsConnected == true && (this.Socket.readyState == 2 || this.Socket.readyState == 3)) {
|
||||
this.IsConnected = false;
|
||||
this.DisconnectCallback();
|
||||
}
|
||||
};
|
||||
WebSocketClient.prototype.Close = function () {
|
||||
if (this.Socket) {
|
||||
this.Socket.close();
|
||||
}
|
||||
};
|
||||
// Handle incomping data
|
||||
WebSocketClient.prototype.OnMessage = function (ev) {
|
||||
// Trigger callback
|
||||
this.DataReadyCallback(ev.data);
|
||||
};
|
||||
return WebSocketClient;
|
||||
}());
|
||||
+28
-26
@@ -1,41 +1,43 @@
|
||||
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) {
|
||||
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);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + addVal));
|
||||
}
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
getCurrentFreq();
|
||||
let addVal = 0;
|
||||
if (currentFreq < 0.52) addVal = 9 - (Math.round(currentFreq*1000) % 9);
|
||||
else if (currentFreq < 1.71) {
|
||||
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);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + addVal));
|
||||
}
|
||||
|
||||
function tuneDown() {
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
getCurrentFreq();
|
||||
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) {
|
||||
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);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - subVal));
|
||||
}
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
getCurrentFreq();
|
||||
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) {
|
||||
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);
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - subVal));
|
||||
}
|
||||
|
||||
function tuneTo(freq) {
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
previousFreq = getCurrentFreq();
|
||||
socket.send("T" + ((parseFloat(freq)) * 1000).toFixed(0));
|
||||
}
|
||||
|
||||
function resetRDS() {
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
socket.send("T0");
|
||||
}
|
||||
|
||||
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
class WebSocketAudioPlayer {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.audio = null;
|
||||
this.mediaSource = null;
|
||||
this.sourceBuffer = null;
|
||||
this.queue = [];
|
||||
this.started = false;
|
||||
this._volume = 1;
|
||||
}
|
||||
|
||||
supported() {
|
||||
return MediaSource.isTypeSupported("audio/mpeg")
|
||||
}
|
||||
|
||||
_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 > 1) this.audio.currentTime = liveEdge - 0.25; // snap to near live edge
|
||||
}
|
||||
|
||||
Start() {
|
||||
this.audio = new Audio();
|
||||
this.mediaSource = new MediaSource();
|
||||
this.audio.src = URL.createObjectURL(this.mediaSource);
|
||||
|
||||
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.onmessage = (event) => {
|
||||
this.queue.push(event.data);
|
||||
this._appendNext();
|
||||
if (!this.started && this.queue.length === 0) { this.audio.play(); this.started = true; }
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
if (this.mediaSource.readyState === 'open') this.mediaSource.endOfStream();
|
||||
};
|
||||
});
|
||||
|
||||
this.audio.addEventListener('canplay', () => {
|
||||
if (!this.started) { this.audio.play(); this.started = true; }
|
||||
});
|
||||
}
|
||||
|
||||
Stop() { // capital to match 3las
|
||||
this.ws?.close();
|
||||
this.audio?.pause();
|
||||
this.queue = [];
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
get Volume() {
|
||||
return this._volume;
|
||||
}
|
||||
set Volume(v) {
|
||||
const actual = Math.max(0, Math.min(1, v))
|
||||
if (this.audio) this.audio.volume = actual;
|
||||
this._volume = actual;
|
||||
}
|
||||
|
||||
_appendNext() {
|
||||
if (this.sourceBuffer.updating || this.queue.length === 0) return;
|
||||
this.sourceBuffer.appendBuffer(this.queue.shift());
|
||||
this._syncToLive();
|
||||
}
|
||||
}
|
||||
|
||||
let Stream = null;
|
||||
let shouldReconnect = true;
|
||||
let firefox = false;
|
||||
|
||||
function Init(_ev) {
|
||||
$(".playbutton").off('click').on('click', OnPlayButtonClick);
|
||||
$("#volumeSlider").off("input").on("input", updateVolume);
|
||||
}
|
||||
|
||||
function createStream() {
|
||||
try {
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${wsProtocol}//${location.hostname}/audio`;
|
||||
|
||||
if (MediaSource.isTypeSupported("audio/mpeg")) {
|
||||
firefox = false;
|
||||
Stream = new WebSocketAudioPlayer(url);
|
||||
} else {
|
||||
firefox = true;
|
||||
alert("Your browser does not support MP3 over MSE, meaning we have to use 3LAS which is terrible and gives terrible audio quality. If you want better sound, switch to anything chromium based")
|
||||
Stream = new _3LAS(url, null);
|
||||
}
|
||||
Stream.Volume = $('#volumeSlider').val();
|
||||
} catch (error) {
|
||||
console.error("Initialization Error: ", error);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyStream() {
|
||||
if (Stream) {
|
||||
Stream.Stop();
|
||||
Stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
function OnPlayButtonClick(_ev) {
|
||||
const $playbutton = $('.playbutton');
|
||||
const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
if (Stream) {
|
||||
console.log("Stopping stream...");
|
||||
shouldReconnect = false;
|
||||
destroyStream();
|
||||
$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');
|
||||
if (isAppleiOS && 'audioSession' in navigator) navigator.audioSession.type = "playback";
|
||||
}
|
||||
|
||||
$playbutton.addClass('bg-gray').prop('disabled', true);
|
||||
setTimeout(() => $playbutton.removeClass('bg-gray').prop('disabled', false), 2000);
|
||||
}
|
||||
|
||||
function updateVolume() {
|
||||
const newVolume = $(this).val();
|
||||
if(!Stream) {
|
||||
console.warn("Stream is not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
Stream.Volume = newVolume;
|
||||
}
|
||||
|
||||
$(document).ready(Init);
|
||||
window.addEventListener('load', Init, false);
|
||||
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
let Stream;
|
||||
let shouldReconnect = true;
|
||||
let audioStreamRestartInterval;
|
||||
|
||||
function Init() {
|
||||
$('.playbutton').off('click').on('click', OnPlayButtonClick);
|
||||
$('#volumeSlider').off('input').on('input', updateVolume);
|
||||
}
|
||||
|
||||
function createStream() {
|
||||
try {
|
||||
Stream = new AudioWsMp3Player();
|
||||
Stream.Volume = $('#volumeSlider').val();
|
||||
} catch (e) {
|
||||
console.error('Audio init error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyStream() {
|
||||
if (Stream) {
|
||||
Stream.Reset();
|
||||
Stream = null;
|
||||
}
|
||||
if (audioStreamRestartInterval) {
|
||||
clearInterval(audioStreamRestartInterval);
|
||||
audioStreamRestartInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function OnPlayButtonClick() {
|
||||
const $btn = $('.playbutton');
|
||||
const isAppleiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
||||
|
||||
if (Stream) {
|
||||
shouldReconnect = false;
|
||||
destroyStream();
|
||||
$btn.find('.fa-solid').toggleClass('fa-stop fa-play');
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = 'none';
|
||||
}
|
||||
} else {
|
||||
shouldReconnect = true;
|
||||
createStream();
|
||||
Stream.Start();
|
||||
$btn.find('.fa-solid').toggleClass('fa-play fa-stop');
|
||||
if (isAppleiOS && 'audioSession' in navigator) {
|
||||
navigator.audioSession.type = 'playback';
|
||||
}
|
||||
if (audioStreamRestartInterval) clearInterval(audioStreamRestartInterval);
|
||||
audioStreamRestartInterval = setInterval(() => {
|
||||
if (typeof requiresAudioStreamRestart !== 'undefined' && requiresAudioStreamRestart && Stream) {
|
||||
requiresAudioStreamRestart = false;
|
||||
Stream.Reset();
|
||||
Stream.Start();
|
||||
console.log('Audio stream restarted after radio data loss.');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
$btn.addClass('bg-gray').prop('disabled', true);
|
||||
setTimeout(() => $btn.removeClass('bg-gray').prop('disabled', false), 3000);
|
||||
}
|
||||
|
||||
function updateVolume() {
|
||||
if (Stream) {
|
||||
Stream.Volume = $(this).val();
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(Init);
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Parts of this file are derived from 3LAS (Low Latency Live Audio Streaming)
|
||||
* by JoJoBond — https://github.com/JoJoBond/3LAS
|
||||
* Licensed under GPL-2.0 (https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
class AudioWsMp3Player {
|
||||
constructor() {
|
||||
this._audioCtx = null;
|
||||
this._gainNode = null;
|
||||
this._ws = null;
|
||||
this._dataBuffer = new Uint8Array(0);
|
||||
this._frames = [];
|
||||
this._frameStartIdx = -1;
|
||||
this._frameEndIdx = -1;
|
||||
this._frameSamples = 0;
|
||||
this._frameSampleRate = 0;
|
||||
this._timeBudget = 0;
|
||||
this._nextPlayTime = 0;
|
||||
this._volume = 1.0;
|
||||
this._watchdogTimer = null;
|
||||
this._shouldReconnect = false;
|
||||
this._decoding = false;
|
||||
this._lastDecodeTime = 0;
|
||||
}
|
||||
|
||||
get Volume() {
|
||||
return this._volume;
|
||||
}
|
||||
|
||||
set Volume(value) {
|
||||
this._volume = Number(value);
|
||||
if (this._gainNode && this._audioCtx) {
|
||||
const clamped = Math.max(1e-20, Math.min(1.0, this._volume));
|
||||
this._gainNode.gain.cancelScheduledValues(this._audioCtx.currentTime);
|
||||
this._gainNode.gain.exponentialRampToValueAtTime(clamped, this._audioCtx.currentTime + 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
Start() {
|
||||
this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this._gainNode = this._audioCtx.createGain();
|
||||
this._gainNode.gain.value = Math.max(1e-20, Math.min(1.0, this._volume));
|
||||
this._gainNode.connect(this._audioCtx.destination);
|
||||
this._nextPlayTime = 0;
|
||||
this._dataBuffer = new Uint8Array(0);
|
||||
this._frames = [];
|
||||
this._frameStartIdx = -1;
|
||||
this._frameEndIdx = -1;
|
||||
this._timeBudget = 0;
|
||||
this._shouldReconnect = true;
|
||||
this._lastDecodeTime = performance.now();
|
||||
this._mobileUnmute();
|
||||
this._connect();
|
||||
}
|
||||
|
||||
Reset() {
|
||||
this._shouldReconnect = false;
|
||||
this._stopWatchdog();
|
||||
if (this._ws) {
|
||||
const ws = this._ws;
|
||||
this._ws = null;
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
ws.close();
|
||||
}
|
||||
if (this._audioCtx) {
|
||||
this._audioCtx.close();
|
||||
this._audioCtx = null;
|
||||
}
|
||||
this._gainNode = null;
|
||||
this._dataBuffer = new Uint8Array(0);
|
||||
this._frames = [];
|
||||
this._nextPlayTime = 0;
|
||||
this._timeBudget = 0;
|
||||
this._decoding = false;
|
||||
}
|
||||
|
||||
// Mobile workaround - play silent stereo buffer to unblock AudioContext.
|
||||
_mobileUnmute() {
|
||||
if (!this._audioCtx) return;
|
||||
const gain = this._audioCtx.createGain();
|
||||
gain.gain.value = 1.0;
|
||||
gain.connect(this._audioCtx.destination);
|
||||
const silentBuf = this._audioCtx.createBuffer(2, this._audioCtx.sampleRate, this._audioCtx.sampleRate);
|
||||
const src = this._audioCtx.createBufferSource();
|
||||
src.onended = () => { src.disconnect(); gain.disconnect(); };
|
||||
src.buffer = silentBuf;
|
||||
src.connect(gain);
|
||||
src.start();
|
||||
}
|
||||
|
||||
// Open the websocket for audio and set up handlers.
|
||||
_connect() {
|
||||
if (!this._shouldReconnect) return;
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
this._ws = new WebSocket(`${protocol}//${location.host}${location.pathname}audio`);
|
||||
this._ws.binaryType = 'arraybuffer';
|
||||
|
||||
this._ws.onopen = () => {
|
||||
this._lastDecodeTime = performance.now();
|
||||
this._startWatchdog();
|
||||
if (this._audioCtx && this._audioCtx.state === 'suspended') {
|
||||
this._audioCtx.resume();
|
||||
}
|
||||
this._mobileUnmute();
|
||||
};
|
||||
|
||||
this._ws.onmessage = (event) => {
|
||||
const chunk = new Uint8Array(event.data);
|
||||
const merged = new Uint8Array(this._dataBuffer.length + chunk.length);
|
||||
merged.set(this._dataBuffer);
|
||||
merged.set(chunk, this._dataBuffer.length);
|
||||
this._dataBuffer = merged;
|
||||
this._extractAndSchedule();
|
||||
};
|
||||
|
||||
this._ws.onclose = () => {
|
||||
this._stopWatchdog();
|
||||
this._ws = null;
|
||||
if (this._shouldReconnect) {
|
||||
this._dataBuffer = new Uint8Array(0);
|
||||
this._frames = [];
|
||||
this._frameStartIdx = -1;
|
||||
this._frameEndIdx = -1;
|
||||
this._nextPlayTime = 0;
|
||||
this._decoding = false;
|
||||
setTimeout(() => this._connect(), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
this._ws.onerror = () => {
|
||||
if (this._ws) this._ws.close();
|
||||
};
|
||||
}
|
||||
|
||||
// Check every 3 seconds if the connection has died, and if so schedule a re-connect.
|
||||
_startWatchdog() {
|
||||
this._stopWatchdog();
|
||||
this._watchdogTimer = setInterval(() => {
|
||||
if (!this._shouldReconnect) { this._stopWatchdog(); return; }
|
||||
if (performance.now() - this._lastDecodeTime > 2000) {
|
||||
this._stopWatchdog();
|
||||
if (this._ws) {
|
||||
const ws = this._ws;
|
||||
this._ws = null;
|
||||
ws.onopen = null;
|
||||
ws.onmessage = null;
|
||||
ws.onclose = null;
|
||||
ws.onerror = null;
|
||||
ws.close();
|
||||
}
|
||||
this._dataBuffer = new Uint8Array(0);
|
||||
this._frames = [];
|
||||
this._frameStartIdx = -1;
|
||||
this._frameEndIdx = -1;
|
||||
this._nextPlayTime = 0;
|
||||
this._decoding = false;
|
||||
setTimeout(() => this._connect(), 1000);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Clear any pre-existing re-connect check
|
||||
_stopWatchdog() {
|
||||
if (this._watchdogTimer !== null) {
|
||||
clearInterval(this._watchdogTimer);
|
||||
this._watchdogTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Find an MP3 frame start and its length in the data buffer, by searching byte-by-byte for the MPEG sync word if the start byte is not known.
|
||||
_findFrame() {
|
||||
if (this._frameStartIdx < 0) {
|
||||
for (let i = 0; i + 1 < this._dataBuffer.length; i++) {
|
||||
if (this._dataBuffer[i] === 0xFF && (this._dataBuffer[i + 1] & 0xE0) === 0xE0) {
|
||||
this._frameStartIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this._frameStartIdx >= 0 && this._frameEndIdx < 0) {
|
||||
if (this._frameStartIdx + 3 < this._dataBuffer.length) {
|
||||
const b1 = this._dataBuffer[this._frameStartIdx + 1];
|
||||
const b2 = this._dataBuffer[this._frameStartIdx + 2];
|
||||
const ver = (b1 & 0x18) >>> 3;
|
||||
const lyr = (b1 & 0x06) >>> 1;
|
||||
const pad = (b2 & 0x02) >>> 1;
|
||||
const brx = (b2 & 0xF0) >>> 4;
|
||||
const srx = (b2 & 0x0C) >>> 2;
|
||||
const bitrate = AudioWsMp3Player.MPEG_bitrates[ver][lyr][brx] * 1000;
|
||||
const samprate = AudioWsMp3Player.MPEG_srates[ver][srx];
|
||||
const samples = AudioWsMp3Player.MPEG_frame_samples[ver][lyr];
|
||||
const slotSize = AudioWsMp3Player.MPEG_slot_size[lyr];
|
||||
if (bitrate > 0 && samprate > 0) {
|
||||
this._frameSamples = samples;
|
||||
this._frameSampleRate = samprate;
|
||||
this._frameEndIdx = this._frameStartIdx + Math.floor((samples / 8.0 * bitrate) / samprate + (pad ? slotSize : 0));
|
||||
} else {
|
||||
this._frameStartIdx = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a complete frame boundary has been found and enough bytes have arrived in the buffer to extract.
|
||||
_canExtractFrame() {
|
||||
return this._frameStartIdx >= 0 && this._frameEndIdx >= 0 && this._frameEndIdx <= this._dataBuffer.length;
|
||||
}
|
||||
|
||||
// Extract the MPEG audio frame from the buffer, and reset the data buffer indexes ready for the next frame.
|
||||
_extractFrame() {
|
||||
const frameData = new Uint8Array(this._dataBuffer.buffer.slice(this._frameStartIdx, this._frameEndIdx));
|
||||
this._dataBuffer = this._frameEndIdx + 1 < this._dataBuffer.length
|
||||
? new Uint8Array(this._dataBuffer.buffer.slice(this._frameEndIdx))
|
||||
: new Uint8Array(0);
|
||||
this._frameStartIdx = 0;
|
||||
this._frameEndIdx = -1;
|
||||
return { data: frameData, sampleCount: this._frameSamples, sampleRate: this._frameSampleRate };
|
||||
}
|
||||
|
||||
// Central processing loop to find all frames within the incoming buffer data each time a websocket message is received.
|
||||
_extractAndSchedule() {
|
||||
if (!this._audioCtx) return;
|
||||
this._findFrame();
|
||||
while (this._canExtractFrame()) {
|
||||
this._frames.push(this._extractFrame());
|
||||
this._findFrame();
|
||||
}
|
||||
if (this._decoding || this._frames.length < 3) return;
|
||||
|
||||
const first = this._frames[0];
|
||||
const last = this._frames[this._frames.length - 1];
|
||||
const firstGranule = first.sampleCount / first.sampleRate / 2.0;
|
||||
const lastGranule = last.sampleCount / last.sampleRate / 2.0;
|
||||
let expectedTime = firstGranule + lastGranule;
|
||||
let bufLen = first.data.length + last.data.length;
|
||||
for (let i = 1; i < this._frames.length - 1; i++) {
|
||||
expectedTime += this._frames[i].sampleCount / this._frames[i].sampleRate;
|
||||
bufLen += this._frames[i].data.length;
|
||||
}
|
||||
if (this._nextPlayTime !== 0 && this._nextPlayTime + expectedTime <= this._audioCtx.currentTime) return;
|
||||
|
||||
const decodeBuffer = new Uint8Array(bufLen);
|
||||
let offset = 0;
|
||||
for (const frame of this._frames) {
|
||||
decodeBuffer.set(frame.data, offset);
|
||||
offset += frame.data.length;
|
||||
}
|
||||
|
||||
this._frames.splice(0, this._frames.length - 1);
|
||||
|
||||
this._decoding = true;
|
||||
this._audioCtx.decodeAudioData(
|
||||
decodeBuffer.buffer,
|
||||
(decoded) => {
|
||||
this._decoding = false;
|
||||
this._onDecoded(decoded, expectedTime, firstGranule, lastGranule);
|
||||
this._extractAndSchedule();
|
||||
},
|
||||
() => {
|
||||
this._decoding = false;
|
||||
this._extractAndSchedule();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Each time we call to decode, trim artefacts created by decoder cold start to stop audio glitches.
|
||||
_onDecoded(decoded, expectedTime, firstGranule, lastGranule) {
|
||||
if (!this._audioCtx || !this._gainNode) return;
|
||||
this._lastDecodeTime = performance.now();
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isApple = /iphone|ipod/.test(ua) ||
|
||||
/ipad/.test(ua) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
|
||||
/mac os x/.test(ua);
|
||||
|
||||
const delta = 0.001;
|
||||
let sampleCount, sampleOffset = 0;
|
||||
|
||||
if (expectedTime > decoded.duration) {
|
||||
sampleCount = decoded.length;
|
||||
sampleOffset = 0;
|
||||
this._timeBudget += expectedTime - decoded.duration;
|
||||
} else if (expectedTime < decoded.duration) {
|
||||
sampleCount = Math.ceil(expectedTime * decoded.sampleRate);
|
||||
const budgetSamples = this._timeBudget * decoded.sampleRate;
|
||||
if (budgetSamples > 1.0) {
|
||||
const extra = Math.min(budgetSamples, decoded.length - sampleCount);
|
||||
sampleCount += extra;
|
||||
this._timeBudget -= extra / decoded.sampleRate;
|
||||
}
|
||||
const diff = decoded.duration - expectedTime;
|
||||
if ((diff - firstGranule - lastGranule) >= -delta) {
|
||||
sampleOffset = Math.floor((decoded.length - sampleCount) / 2);
|
||||
}
|
||||
if (Math.abs(firstGranule - lastGranule) <= delta) {
|
||||
sampleOffset = isApple
|
||||
? Math.floor((decoded.length - sampleCount) / 2)
|
||||
: decoded.length - sampleCount;
|
||||
} else if (Math.abs(diff - firstGranule) <= delta) {
|
||||
sampleOffset = decoded.length - sampleCount;
|
||||
} else if (Math.abs(diff - lastGranule) <= delta) {
|
||||
sampleOffset = 0;
|
||||
} else {
|
||||
sampleOffset = Math.floor((decoded.length - sampleCount) / 2);
|
||||
}
|
||||
} else {
|
||||
sampleCount = decoded.length;
|
||||
sampleOffset = 0;
|
||||
}
|
||||
|
||||
const buf = this._audioCtx.createBuffer(decoded.numberOfChannels, sampleCount, decoded.sampleRate);
|
||||
for (let ch = 0; ch < decoded.numberOfChannels; ch++) {
|
||||
buf.getChannelData(ch).set(decoded.getChannelData(ch).subarray(sampleOffset, sampleOffset + sampleCount));
|
||||
}
|
||||
this._scheduleBuffer(buf);
|
||||
}
|
||||
|
||||
// Schedule a decoded buffer for playback at the correct AudioContext time. Reset if it's falled behind (on reconnect etc.).
|
||||
_scheduleBuffer(buffer) {
|
||||
if (!this._audioCtx || !this._gainNode) return;
|
||||
if (this._audioCtx.state === 'suspended') {
|
||||
this._audioCtx.resume();
|
||||
}
|
||||
const INITIAL_OFFSET = 0.33;
|
||||
|
||||
if (this._nextPlayTime === 0) {
|
||||
this._nextPlayTime = this._audioCtx.currentTime + INITIAL_OFFSET;
|
||||
}
|
||||
|
||||
const duration = buffer.duration;
|
||||
if (this._nextPlayTime + duration <= this._audioCtx.currentTime) {
|
||||
this._nextPlayTime = this._audioCtx.currentTime + INITIAL_OFFSET;
|
||||
}
|
||||
|
||||
let skipDuration = 0;
|
||||
if (this._audioCtx.currentTime >= this._nextPlayTime) {
|
||||
skipDuration = this._audioCtx.currentTime - this._nextPlayTime + 0.05;
|
||||
}
|
||||
|
||||
if (skipDuration < duration) {
|
||||
const src = this._audioCtx.createBufferSource();
|
||||
src.loop = false;
|
||||
src.buffer = buffer;
|
||||
src.onended = () => src.disconnect();
|
||||
src.connect(this._gainNode);
|
||||
src.start(this._nextPlayTime + skipDuration, skipDuration);
|
||||
}
|
||||
this._nextPlayTime += duration;
|
||||
}
|
||||
}
|
||||
|
||||
// MPEG bitrate table [version][layer][index] in kbps
|
||||
AudioWsMp3Player.MPEG_bitrates = [
|
||||
[ // MPEG 2.5
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0],
|
||||
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0],
|
||||
[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,0]
|
||||
],
|
||||
[ // Reserved
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
||||
],
|
||||
[ // MPEG 2
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0],
|
||||
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0],
|
||||
[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,0]
|
||||
],
|
||||
[ // MPEG 1
|
||||
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320,0],
|
||||
[0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,0],
|
||||
[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,0]
|
||||
]
|
||||
];
|
||||
|
||||
// Sample rates [version][index] in Hz
|
||||
AudioWsMp3Player.MPEG_srates = [
|
||||
[11025,12000,8000,0],
|
||||
[0,0,0,0],
|
||||
[22050,24000,16000,0],
|
||||
[44100,48000,32000,0]
|
||||
];
|
||||
|
||||
// Samples per frame [version][layer]
|
||||
AudioWsMp3Player.MPEG_frame_samples = [
|
||||
[0,576,1152,384],
|
||||
[0,0,0,0],
|
||||
[0,576,1152,384],
|
||||
[0,1152,1152,384]
|
||||
];
|
||||
|
||||
// Slot size [layer]: Reserved, Layer3, Layer2, Layer1
|
||||
AudioWsMp3Player.MPEG_slot_size = [0,1,1,4];
|
||||
+3
-3
@@ -24,7 +24,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
// Load nickname from localStorage on page load
|
||||
let savedNickname = localStorage.getItem('nickname') || `User ${generateRandomString(5)}`;
|
||||
let savedNickname = localStorage.getItem('nickname') || `User ${generateRandomString(6)}`;
|
||||
chatNicknameInput.val(savedNickname);
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
|
||||
@@ -60,7 +60,7 @@ $(document).ready(function() {
|
||||
|
||||
$('.chat-send-message-btn').click(sendMessage);
|
||||
chatNicknameSave.click(function() {
|
||||
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(5)}`;
|
||||
const currentNickname = chatNicknameInput.val().trim() || `Anonymous User ${generateRandomString(6)}`;
|
||||
localStorage.setItem('nickname', currentNickname);
|
||||
savedNickname = currentNickname;
|
||||
chatIdentityNickname.text(savedNickname);
|
||||
@@ -87,7 +87,7 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const nickname = savedNickname || `Anonymous User ${generateRandomString(5)}`;
|
||||
const nickname = savedNickname || `Anonymous User ${generateRandomString(6)}`;
|
||||
const message = chatSendInput.val().trim();
|
||||
|
||||
if (message) {
|
||||
|
||||
+92
-129
@@ -1,3 +1,6 @@
|
||||
const isMobileDevice = /android|iphone|ipod|ipad/i.test(navigator.userAgent) ||
|
||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
|
||||
var parsedData, signalChart, previousFreq;
|
||||
var data = [];
|
||||
var signalData = [];
|
||||
@@ -110,7 +113,7 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
if (socket.readyState === WebSocket.OPEN) tuneTo(textInput.val());
|
||||
tuneTo(textInput.val());
|
||||
textInput.val('');
|
||||
}
|
||||
});
|
||||
@@ -126,10 +129,8 @@ $(document).ready(function () {
|
||||
e.preventDefault();
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastExecutionTime < throttleDelay) {
|
||||
// Ignore this event as it's within the throttle delay
|
||||
return;
|
||||
}
|
||||
// Ignore this event as it's within the throttle delay
|
||||
if (now - lastExecutionTime < throttleDelay) return;
|
||||
|
||||
lastExecutionTime = now; // Update the last execution time
|
||||
|
||||
@@ -362,9 +363,7 @@ function sendPingRequest() {
|
||||
socket.onopen = () => {
|
||||
sendToast('info', 'Connected', 'Reconnected successfully!', false, false);
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
handleWebSocketMessage(event);
|
||||
};
|
||||
socket.onmessage = handleWebSocketMessage;
|
||||
socket.onerror = (error) => {
|
||||
console.error("Main/UI WebSocket error during reconnection:", error);
|
||||
};
|
||||
@@ -390,12 +389,18 @@ function handleWebSocketMessage(event) {
|
||||
if (event.data == 'KICK') {
|
||||
console.log('Kick initiated.')
|
||||
setTimeout(() => {
|
||||
window.location.href = '/403';
|
||||
window.location.href = `/403?reason=${encodeURIComponent("You have been kicked")}`;
|
||||
}, 100);
|
||||
return;
|
||||
} else if (event.data.startsWith("!")) {
|
||||
sendToast('info', 'Info from server', event.data.slice(1), false, false)
|
||||
return;
|
||||
} else if( event.data == "a0") {
|
||||
console.log('Too many users');
|
||||
setTimeout(() => {
|
||||
window.location.href = `/403?reason=${encodeURIComponent("Too many users")}`;
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
parsedData = JSON.parse(event.data);
|
||||
@@ -404,11 +409,8 @@ function handleWebSocketMessage(event) {
|
||||
updatePanels(parsedData);
|
||||
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
data.push(averageSignal);
|
||||
data.push(sum / signalData.length);
|
||||
}
|
||||
|
||||
// Attach the message handler
|
||||
socket.onmessage = handleWebSocketMessage;
|
||||
|
||||
const signalBuffer = [];
|
||||
@@ -453,12 +455,12 @@ function initCanvas() {
|
||||
grid: { display: false, borderWidth: 0, borderColor: "transparent" },
|
||||
realtime: {
|
||||
duration: 30000,
|
||||
refresh: 75,
|
||||
refresh: 100,
|
||||
delay: 150,
|
||||
frameRate: 30, // default is 30
|
||||
frameRate: 24,
|
||||
onRefresh: (chart) => {
|
||||
if (!chart?.data?.datasets || parsedData?.sig === undefined) return;
|
||||
if ((isAndroid || isIOS || isIPadOS) && (document.hidden || !document.hasFocus())) return;
|
||||
if (isMobileDevice && (document.hidden || !document.hasFocus())) return;
|
||||
|
||||
const sig = parsedData.sig;
|
||||
signalBuffer.push(sig);
|
||||
@@ -572,18 +574,16 @@ function initCanvas() {
|
||||
}
|
||||
|
||||
function setRefreshRate(rate) {
|
||||
const rt = signalChart.options.scales.x.realtime;
|
||||
rt.refresh = rate;
|
||||
signalChart.options.scales.x.realtime.refresh = rate;
|
||||
signalChart.update('none');
|
||||
console.log(`Graph refresh rate set to ${rate} ms`);
|
||||
}
|
||||
|
||||
window.addEventListener("focus", () => {
|
||||
if (isAndroid || isIOS || isIPadOS) setRefreshRate(75);
|
||||
if(isMobileDevice) setRefreshRate(100);
|
||||
});
|
||||
|
||||
window.addEventListener("blur", () => {
|
||||
if (isAndroid || isIOS || isIPadOS) setRefreshRate(3000);
|
||||
if(isMobileDevice) setRefreshRate(3000);
|
||||
});
|
||||
|
||||
let reconnectTimer = null;
|
||||
@@ -603,34 +603,10 @@ const resetDataTimeout = () => {
|
||||
}, TIMEOUT_DURATION);
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
if (event.data === 'KICK') {
|
||||
console.log('Kick initiated.');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/403';
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
parsedData = JSON.parse(event.data);
|
||||
|
||||
resetDataTimeout();
|
||||
updatePanels(parsedData);
|
||||
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
data.push(averageSignal);
|
||||
};
|
||||
|
||||
function compareNumbers(a, b) {
|
||||
// Really?
|
||||
return a - b;
|
||||
}
|
||||
|
||||
function escapeHTML(unsafeText) {
|
||||
let div = document.createElement('div');
|
||||
div.innerText = unsafeText;
|
||||
return div.innerHTML.replace(' ', ' ');
|
||||
function escapeHTML(text) {
|
||||
const div = document.createElement("div");
|
||||
div.innerText = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function processString(string, errors) {
|
||||
@@ -650,90 +626,84 @@ function processString(string, errors) {
|
||||
}
|
||||
|
||||
function checkKey(e) {
|
||||
if (socket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
e = e || window.event;
|
||||
|
||||
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
|
||||
|
||||
if ($('#password:focus').length > 0
|
||||
|| $('#chat-send-message:focus').length > 0
|
||||
|| $('#volumeSlider:focus').length > 0
|
||||
|| $('#chat-nickname:focus').length > 0
|
||||
|| $('.option:focus').length > 0) {
|
||||
return;
|
||||
}
|
||||
|| $('.option:focus').length > 0) return;
|
||||
|
||||
getCurrentFreq();
|
||||
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
switch (e.keyCode) {
|
||||
case 66: // Back to previous frequency
|
||||
tuneTo(previousFreq);
|
||||
break;
|
||||
case 82: // RDS Reset (R key)
|
||||
tuneTo(Number(currentFreq));
|
||||
break;
|
||||
case 83: // Screenshot (S key)
|
||||
break;
|
||||
case 38:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + ((currentFreq > 30) ? 10 : 1)));
|
||||
break;
|
||||
case 40:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - ((currentFreq > 30) ? 10 : 1)));
|
||||
break;
|
||||
case 37:
|
||||
tuneDown();
|
||||
break;
|
||||
case 39:
|
||||
tuneUp();
|
||||
break;
|
||||
case 46:
|
||||
let $dropdown = $(".data-ant");
|
||||
let $input = $dropdown.find("input");
|
||||
let $options = $dropdown.find("ul.options .option");
|
||||
switch (e.keyCode) {
|
||||
case 66: // Back to previous frequency
|
||||
tuneTo(previousFreq);
|
||||
break;
|
||||
case 82: // RDS Reset (R key)
|
||||
tuneTo(Number(currentFreq));
|
||||
break;
|
||||
case 83: // Screenshot (S key)
|
||||
break;
|
||||
case 38:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) + ((currentFreq > 30) ? 10 : 1)));
|
||||
break;
|
||||
case 40:
|
||||
socket.send("T" + (Math.round(currentFreq*1000) - ((currentFreq > 30) ? 10 : 1)));
|
||||
break;
|
||||
case 37:
|
||||
tuneDown();
|
||||
break;
|
||||
case 39:
|
||||
tuneUp();
|
||||
break;
|
||||
case 46:
|
||||
let $dropdown = $(".data-ant");
|
||||
let $input = $dropdown.find("input");
|
||||
let $options = $dropdown.find("ul.options .option");
|
||||
|
||||
if ($options.length === 0) return; // No antennas available
|
||||
if ($options.length === 0) return; // No antennas available
|
||||
|
||||
// Find the currently selected antenna
|
||||
let currentText = $input.val().trim();
|
||||
let currentIndex = $options.index($options.filter(function () {
|
||||
return $(this).text().trim() === currentText;
|
||||
}));
|
||||
// Find the currently selected antenna
|
||||
let currentText = $input.val().trim();
|
||||
let currentIndex = $options.index($options.filter(function () {
|
||||
return $(this).text().trim() === currentText;
|
||||
}));
|
||||
|
||||
console.log(currentIndex, currentText);
|
||||
console.log(currentIndex, currentText);
|
||||
|
||||
// Cycle to the next option
|
||||
let nextIndex = (currentIndex + 1) % $options.length;
|
||||
let $nextOption = $options.eq(nextIndex);
|
||||
// Cycle to the next option
|
||||
let nextIndex = (currentIndex + 1) % $options.length;
|
||||
let $nextOption = $options.eq(nextIndex);
|
||||
|
||||
// Update UI
|
||||
$input.attr("placeholder", $nextOption.text());
|
||||
$input.data("value", $nextOption.data("value"));
|
||||
// Update UI
|
||||
$input.attr("placeholder", $nextOption.text());
|
||||
$input.data("value", $nextOption.data("value"));
|
||||
|
||||
let socketMessage = "Z" + $nextOption.data("value");
|
||||
socket.send(socketMessage);
|
||||
break;
|
||||
case 112: // F1
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset1')));
|
||||
break;
|
||||
case 113: // F2
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset2')));
|
||||
break;
|
||||
case 114: // F3
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset3')));
|
||||
break;
|
||||
case 115: // F4
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset4')));
|
||||
break;
|
||||
default:
|
||||
// Handle default case if needed
|
||||
let socketMessage = "Z" + $nextOption.data("value");
|
||||
socket.send(socketMessage);
|
||||
break;
|
||||
case 112: // F1
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset1')));
|
||||
break;
|
||||
case 113: // F2
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset2')));
|
||||
break;
|
||||
case 114: // F3
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset3')));
|
||||
break;
|
||||
case 115: // F4
|
||||
e.preventDefault();
|
||||
tuneTo(Number(localStorage.getItem('preset4')));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,11 +739,8 @@ async function copyTx() {
|
||||
}
|
||||
|
||||
async function copyRt() {
|
||||
var rt0 = $('#data-rt0 span').text();
|
||||
var rt1 = $('#data-rt1 span').text();
|
||||
|
||||
try {
|
||||
await copyToClipboard("[0] RT: " + rt0 + "\n[1] RT: " + rt1);
|
||||
await copyToClipboard("[0] RT: " + $('#data-rt0 span').text() + "\n[1] RT: " + $('#data-rt1 span').text());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -809,13 +776,10 @@ function copyToClipboard(textToCopy) {
|
||||
|
||||
function findOnMaps() {
|
||||
var frequency = parseFloat($('#data-frequency').text());
|
||||
var pi = $('#data-pi').text();
|
||||
var latitude = localStorage.getItem('qthLongitude');
|
||||
var longitude = localStorage.getItem('qthLatitude');
|
||||
|
||||
frequency > 74 ? frequency = frequency.toFixed(1) : null;
|
||||
|
||||
window.open(`https://maps.fmdx.org/#qth=${longitude},${latitude}&freq=${frequency}&findPi=${pi}`, "_blank");
|
||||
window.open(`https://maps.fmdx.org/#qth=${localStorage.getItem('qthLatitude')},${localStorage.getItem('qthLongitude')}&freq=${frequency}&findPi=${$('#data-pi').text()}`, "_blank");
|
||||
}
|
||||
|
||||
function updateSignalUnits(parsedData, averageSignal) {
|
||||
@@ -832,7 +796,7 @@ function updateSignalUnits(parsedData, averageSignal) {
|
||||
signalValue = currentSignal - 11.25;
|
||||
highestSignal = highestSignal - 11.25;
|
||||
signalText.text('dBµV');
|
||||
break;
|
||||
break;
|
||||
case 'dbm':
|
||||
signalValue = currentSignal - 120;
|
||||
highestSignal = highestSignal - 120;
|
||||
@@ -1024,7 +988,7 @@ function updatePanels(parsedData) {
|
||||
const sum = signalData.reduce((acc, strNum) => acc + parseFloat(strNum), 0);
|
||||
const averageSignal = sum / signalData.length;
|
||||
|
||||
const sortedAf = parsedData.af.sort(compareNumbers);
|
||||
const sortedAf = parsedData.af.sort(function (a, b){return a-b});
|
||||
const scaledArray = sortedAf.map(element => element / 1000);
|
||||
|
||||
const listContainer = $('#af-list');
|
||||
@@ -1048,8 +1012,7 @@ function updatePanels(parsedData) {
|
||||
// Add the event listener only once
|
||||
if (!isEventListenerAdded) {
|
||||
ul.on('click', 'a', function () {
|
||||
const frequency = parseFloat($(this).text());
|
||||
tuneTo(frequency);
|
||||
tuneTo(parseFloat($(this).text()));
|
||||
});
|
||||
isEventListenerAdded = true;
|
||||
}
|
||||
|
||||
+42
-120
@@ -22,10 +22,10 @@ window.addEventListener('load', (() => {
|
||||
const RECONNECT_DELAY = 3000;
|
||||
const FIRST_DISCONNECT_RECONNECT_DELAY = 500;
|
||||
const MAX_RECONNECT_ATTEMPTS = 50;
|
||||
const UI_ATTACH_INITIAL_DELAY = 250;
|
||||
const UI_ATTACH_INITIAL_DELAY = 100;
|
||||
const ONDEMAND_STARTUP_404_GRACE_ATTEMPTS = 5;
|
||||
const ONDEMAND_STARTUP_RECONNECT_MS_FAST = 500;
|
||||
const ONDEMAND_STARTUP_RECONNECT_MS_SLOW = 1000;
|
||||
const ONDEMAND_STARTUP_RECONNECT_MS_SLOW = 750;
|
||||
const ONDEMAND_STARTUP_FAST_RETRY_COUNT = 2;
|
||||
|
||||
const webrtcCss = `
|
||||
@@ -193,9 +193,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function logWebRTCDebug(message, details = undefined) {
|
||||
if (!DEBUG_WEBRTC_AUDIO) {
|
||||
return;
|
||||
}
|
||||
if (!DEBUG_WEBRTC_AUDIO) return;
|
||||
|
||||
const elapsedMs = currentDebugSessionStartedAt
|
||||
? Math.round(performance.now() - currentDebugSessionStartedAt)
|
||||
@@ -284,9 +282,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function persistSelectedSource(value) {
|
||||
if (value) {
|
||||
setPersistentCookie(LAST_SOURCE_COOKIE_NAME, value);
|
||||
}
|
||||
if (value) setPersistentCookie(LAST_SOURCE_COOKIE_NAME, value);
|
||||
}
|
||||
|
||||
function parseWhepProfiles(rawConfig) {
|
||||
@@ -329,7 +325,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function getLegacyOptionLabel() {
|
||||
return '3LAS (TCP)';
|
||||
return 'Websocket';
|
||||
}
|
||||
|
||||
function getSelectedBitrateLabel() {
|
||||
@@ -337,9 +333,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function parseSelectedBitrate(value) {
|
||||
if (typeof value !== 'string' || !value.startsWith('webrtc:')) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string' || !value.startsWith('webrtc:')) return null;
|
||||
|
||||
const bitrate = Number(value.slice('webrtc:'.length));
|
||||
return Number.isFinite(bitrate) && bitrate > 0 ? bitrate : null;
|
||||
@@ -366,9 +360,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function ensureAudioElement() {
|
||||
if (audioElement) {
|
||||
return audioElement;
|
||||
}
|
||||
if (audioElement) return audioElement;
|
||||
|
||||
audioElement = new Audio();
|
||||
audioElement.autoplay = true;
|
||||
@@ -393,9 +385,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function maybeMarkAudioPlaybackStarted(source) {
|
||||
if (!audioElement || hasAudioPlaybackStarted) {
|
||||
return;
|
||||
}
|
||||
if (!audioElement || hasAudioPlaybackStarted) return;
|
||||
|
||||
const currentTime = Number(audioElement.currentTime || 0);
|
||||
if (currentTime <= 0.05) {
|
||||
@@ -441,9 +431,7 @@ window.addEventListener('load', (() => {
|
||||
isConnecting,
|
||||
audio: getAudioElementDebugState()
|
||||
});
|
||||
if (!peerConnection || !isPeerConnectionReady || !hasAudioPlaybackStarted || isActive) {
|
||||
return;
|
||||
}
|
||||
if (!peerConnection || !isPeerConnectionReady || !hasAudioPlaybackStarted || isActive) return;
|
||||
|
||||
isConnecting = false;
|
||||
isActive = true;
|
||||
@@ -482,22 +470,14 @@ window.addEventListener('load', (() => {
|
||||
const $managedButton = getManagedPlayButtons()
|
||||
.filter((_, element) => $(element).closest('#mobileTray').length > 0)
|
||||
.first();
|
||||
if ($managedButton.length > 0) {
|
||||
return $managedButton;
|
||||
}
|
||||
|
||||
if ($managedButton.length > 0) return $managedButton;
|
||||
return $('#mobileTray .playbutton').first();
|
||||
}
|
||||
|
||||
function ensureManagedDesktopPlayButton() {
|
||||
let $desktopButton = getDesktopPlayButton();
|
||||
if ($desktopButton.length === 0) {
|
||||
return $desktopButton;
|
||||
}
|
||||
|
||||
if ($desktopButton.hasClass(MANAGED_PLAYBUTTON_CLASS)) {
|
||||
return $desktopButton;
|
||||
}
|
||||
if ($desktopButton.length === 0) return $desktopButton;
|
||||
if ($desktopButton.hasClass(MANAGED_PLAYBUTTON_CLASS)) return $desktopButton;
|
||||
|
||||
const $proxyButton = $desktopButton;
|
||||
const $managedButton = $proxyButton.clone(false);
|
||||
@@ -516,13 +496,8 @@ window.addEventListener('load', (() => {
|
||||
|
||||
function ensureManagedMobilePlayButton() {
|
||||
let $mobileButton = getMobilePlayButton();
|
||||
if ($mobileButton.length === 0) {
|
||||
return $mobileButton;
|
||||
}
|
||||
|
||||
if ($mobileButton.hasClass(MANAGED_PLAYBUTTON_CLASS)) {
|
||||
return $mobileButton;
|
||||
}
|
||||
if ($mobileButton.length === 0) return $mobileButton;
|
||||
if ($mobileButton.hasClass(MANAGED_PLAYBUTTON_CLASS)) return $mobileButton;
|
||||
|
||||
$mobileButton.removeClass('playbutton').addClass(MANAGED_PLAYBUTTON_CLASS);
|
||||
$mobileButton.off();
|
||||
@@ -536,9 +511,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
const $icon = $proxyButton.find('i');
|
||||
if ($icon.hasClass('fa-stop')) {
|
||||
return 'connected';
|
||||
}
|
||||
if ($icon.hasClass('fa-stop')) return 'connected';
|
||||
return 'disconnected';
|
||||
}
|
||||
|
||||
@@ -546,9 +519,7 @@ window.addEventListener('load', (() => {
|
||||
const legacyState = getLegacyProxyState();
|
||||
const $buttons = getManagedPlayButtons();
|
||||
$buttons.removeClass('rtc-active rtc-connecting');
|
||||
if (legacyState === 'connected') {
|
||||
$buttons.addClass('rtc-active');
|
||||
}
|
||||
if (legacyState === 'connected') $buttons.addClass('rtc-active');
|
||||
updatePlayButtonIcon(legacyState === 'connected' ? 'connected' : 'disconnected');
|
||||
updatePlayButtonTitle(legacyState === 'connected' ? 'connected' : 'disconnected');
|
||||
}
|
||||
@@ -602,9 +573,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function normalizeStoredSourceValue(value) {
|
||||
if (value === LEGACY_SOURCE_VALUE) {
|
||||
return value;
|
||||
}
|
||||
if (value === LEGACY_SOURCE_VALUE) return value;
|
||||
|
||||
const storedBitrate = parseSelectedBitrate(value);
|
||||
if (storedBitrate) {
|
||||
@@ -617,14 +586,10 @@ window.addEventListener('load', (() => {
|
||||
|
||||
function getDefaultSourceValue() {
|
||||
const storedValue = normalizeStoredSourceValue(getStoredSource());
|
||||
if (storedValue === LEGACY_SOURCE_VALUE && !disablews) {
|
||||
return LEGACY_SOURCE_VALUE;
|
||||
}
|
||||
if (storedValue === LEGACY_SOURCE_VALUE && !disablews) return LEGACY_SOURCE_VALUE;
|
||||
|
||||
const storedBitrate = parseSelectedBitrate(storedValue);
|
||||
if (profiles.length === 0) {
|
||||
return storedValue || SELECT_LOADING_VALUE;
|
||||
}
|
||||
if (profiles.length === 0) return storedValue || SELECT_LOADING_VALUE;
|
||||
|
||||
const matchingProfile = Number.isFinite(storedBitrate)
|
||||
? profiles.find((profile) => profile.bitrate === storedBitrate)
|
||||
@@ -638,20 +603,14 @@ window.addEventListener('load', (() => {
|
||||
const buttonState = getActivePlaybackBackend() ? 'connected' : 'disconnected';
|
||||
|
||||
if (value === LEGACY_SOURCE_VALUE && !disablews) {
|
||||
if (syncSelect) {
|
||||
$selects.val(LEGACY_SOURCE_VALUE);
|
||||
}
|
||||
if (persist) {
|
||||
persistSelectedSource(LEGACY_SOURCE_VALUE);
|
||||
}
|
||||
if (syncSelect) $selects.val(LEGACY_SOURCE_VALUE);
|
||||
if (persist) persistSelectedSource(LEGACY_SOURCE_VALUE);
|
||||
updatePlayButtonTitle(buttonState);
|
||||
return LEGACY_SOURCE_VALUE;
|
||||
}
|
||||
|
||||
if (profiles.length === 0) {
|
||||
if (syncSelect) {
|
||||
$selects.val(SELECT_LOADING_VALUE);
|
||||
}
|
||||
if (syncSelect) $selects.val(SELECT_LOADING_VALUE);
|
||||
updatePlayButtonTitle(buttonState);
|
||||
return SELECT_LOADING_VALUE;
|
||||
}
|
||||
@@ -665,12 +624,8 @@ window.addEventListener('load', (() => {
|
||||
persistSelectedBitrate();
|
||||
|
||||
const appliedValue = `webrtc:${currentBitrate}`;
|
||||
if (syncSelect) {
|
||||
$selects.val(appliedValue);
|
||||
}
|
||||
if (persist) {
|
||||
persistSelectedSource(appliedValue);
|
||||
}
|
||||
if (syncSelect) $selects.val(appliedValue);
|
||||
if (persist) persistSelectedSource(appliedValue);
|
||||
|
||||
updatePlayButtonTitle(buttonState);
|
||||
return appliedValue;
|
||||
@@ -680,18 +635,14 @@ window.addEventListener('load', (() => {
|
||||
const $selects = $(`.fmdx-webrtc-mode-select`);
|
||||
for (let i = 0; i < $selects.length; i++) {
|
||||
const value = $selects[i].value;
|
||||
if (value && value !== SELECT_LOADING_VALUE) {
|
||||
return value;
|
||||
}
|
||||
if (value && value !== SELECT_LOADING_VALUE) return value;
|
||||
}
|
||||
return getDefaultSourceValue();
|
||||
}
|
||||
|
||||
function getSelectedSourceLabel() {
|
||||
const selectedValue = getSelectedSourceValue();
|
||||
if (selectedValue === LEGACY_SOURCE_VALUE) {
|
||||
return getLegacyOptionLabel();
|
||||
}
|
||||
if (selectedValue === LEGACY_SOURCE_VALUE) return getLegacyOptionLabel();
|
||||
|
||||
const selectedBitrate = parseSelectedBitrate(selectedValue) ?? currentBitrate;
|
||||
return selectedBitrate ? formatWebRtcOptionLabel(selectedBitrate) : 'WebRTC';
|
||||
@@ -702,12 +653,8 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function getActivePlaybackBackend() {
|
||||
if (isConnecting || isActive || shouldReconnect) {
|
||||
return 'webrtc';
|
||||
}
|
||||
if (isLegacyPlaybackActive()) {
|
||||
return LEGACY_SOURCE_VALUE;
|
||||
}
|
||||
if (isConnecting || isActive || shouldReconnect) return 'webrtc';
|
||||
if (isLegacyPlaybackActive()) return LEGACY_SOURCE_VALUE;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -761,9 +708,7 @@ window.addEventListener('load', (() => {
|
||||
}
|
||||
|
||||
function attachSelectToButton($button, selectId) {
|
||||
if ($button.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if ($button.length === 0) return null;
|
||||
|
||||
const $host = $button.closest('.panel-10').length > 0
|
||||
? $button.closest('.panel-10')
|
||||
@@ -803,13 +748,9 @@ window.addEventListener('load', (() => {
|
||||
const $desktopButton = ensureManagedDesktopPlayButton();
|
||||
|
||||
let $mobileButton = $();
|
||||
if (!disableMobileSelect) {
|
||||
$mobileButton = ensureManagedMobilePlayButton();
|
||||
}
|
||||
if (!disableMobileSelect) $mobileButton = ensureManagedMobilePlayButton();
|
||||
|
||||
if ($desktopButton.length === 0 && $mobileButton.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if ($desktopButton.length === 0 && $mobileButton.length === 0) return false;
|
||||
|
||||
const optionsHtml = buildOptionsHtml();
|
||||
|
||||
@@ -820,9 +761,7 @@ window.addEventListener('load', (() => {
|
||||
|
||||
if (!disableMobileSelect) {
|
||||
const $mobileSelect = attachSelectToButton($mobileButton, SELECT_ID + '-mobile');
|
||||
if ($mobileSelect) {
|
||||
$mobileSelect.html(optionsHtml);
|
||||
}
|
||||
if ($mobileSelect) $mobileSelect.html(optionsHtml);
|
||||
}
|
||||
|
||||
applySelectedSourceValue(getDefaultSourceValue(), { persist: false, syncSelect: true });
|
||||
@@ -831,9 +770,7 @@ window.addEventListener('load', (() => {
|
||||
|
||||
function bindPlayButtons() {
|
||||
const $buttons = getManagedPlayButtons();
|
||||
if ($buttons.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if ($buttons.length === 0) return false;
|
||||
|
||||
$buttons
|
||||
.off('click')
|
||||
@@ -850,13 +787,9 @@ window.addEventListener('load', (() => {
|
||||
bindPlayButtons();
|
||||
|
||||
const activeBackend = getActivePlaybackBackend();
|
||||
if (activeBackend === 'webrtc') {
|
||||
updateButtonState(isConnecting ? 'connecting' : 'connected');
|
||||
} else if (!activeBackend) {
|
||||
updateButtonState('disconnected');
|
||||
} else {
|
||||
syncManagedButtonWithLegacyState();
|
||||
}
|
||||
if (activeBackend === 'webrtc') updateButtonState(isConnecting ? 'connecting' : 'connected');
|
||||
else if (!activeBackend) updateButtonState('disconnected');
|
||||
else syncManagedButtonWithLegacyState();
|
||||
}
|
||||
|
||||
async function fetchAvailableBitrates({ preserveSelection = true } = {}) {
|
||||
@@ -955,21 +888,15 @@ window.addEventListener('load', (() => {
|
||||
if (audioElement) {
|
||||
audioElement.pause();
|
||||
audioElement.srcObject = null;
|
||||
if (audioElement.parentNode) {
|
||||
audioElement.parentNode.removeChild(audioElement);
|
||||
}
|
||||
if (audioElement.parentNode) audioElement.parentNode.removeChild(audioElement);
|
||||
audioElement = null;
|
||||
}
|
||||
|
||||
if (deleteSession) {
|
||||
await deleteWhepSession();
|
||||
}
|
||||
if (deleteSession) await deleteWhepSession();
|
||||
}
|
||||
|
||||
function scheduleReconnect({ countAttempt = true, delayMs = RECONNECT_DELAY } = {}) {
|
||||
if (!shouldReconnect) {
|
||||
return;
|
||||
}
|
||||
if (!shouldReconnect) return;
|
||||
|
||||
if (countAttempt && reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
sendToast('error', 'WebRTC Audio', 'Reconnect limit reached', false, false);
|
||||
@@ -1015,11 +942,8 @@ window.addEventListener('load', (() => {
|
||||
isActive = false;
|
||||
await cleanupConnection();
|
||||
|
||||
if (wasRequested) {
|
||||
scheduleReconnect();
|
||||
} else {
|
||||
updateButtonState('disconnected');
|
||||
}
|
||||
if (wasRequested) scheduleReconnect();
|
||||
else updateButtonState('disconnected');
|
||||
}
|
||||
|
||||
async function startWebRTC({ refreshProfiles = false } = {}) {
|
||||
@@ -1103,9 +1027,7 @@ window.addEventListener('load', (() => {
|
||||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc !== peerConnection) {
|
||||
return;
|
||||
}
|
||||
if (pc !== peerConnection) return;
|
||||
|
||||
logWebRTCDebug('pc.connectionstatechange', {
|
||||
connectionState: pc.connectionState,
|
||||
|
||||
+20
-23
@@ -25,12 +25,11 @@ const signalUnits = {
|
||||
};
|
||||
|
||||
$(document).ready(() => {
|
||||
|
||||
getInitialSettings();
|
||||
|
||||
|
||||
$('#login-form').submit(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: './login',
|
||||
@@ -45,11 +44,11 @@ $(document).ready(() => {
|
||||
if (xhr.status === 403) sendToast('error', 'Login failed!', xhr.responseJSON.message, false, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$('.logout-link').click(function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
|
||||
$.ajax({
|
||||
type: 'GET', // Assuming the logout is a GET request, adjust accordingly
|
||||
url: './logout',
|
||||
@@ -60,9 +59,7 @@ $(document).ready(() => {
|
||||
}, 500);
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
if (xhr.status === 403) {
|
||||
sendToast('error', 'Logout failed!', xhr.responseJSON.message, false, true);
|
||||
}
|
||||
if (xhr.status === 403) sendToast('error', 'Logout failed!', xhr.responseJSON.message, false, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -132,7 +129,7 @@ function getInitialSettings() {
|
||||
['qthLatitude', 'qthLongitude', 'defaultTheme', 'bgImage', 'rdsMode', 'rdsTimeout'].forEach(key => {
|
||||
if (data[key] !== undefined) localStorage.setItem(key, data[key]);
|
||||
});
|
||||
|
||||
|
||||
data.presets.forEach((preset, index) => localStorage.setItem(`preset${index + 1}`, preset));
|
||||
|
||||
loadInitialSettings();
|
||||
@@ -150,7 +147,7 @@ function loadInitialSettings() {
|
||||
const savedUnit = localStorage.getItem('signalUnit');
|
||||
|
||||
themeSelector.find('input').val(themeSelector.find('.option[data-value="' + defaultTheme + '"]').text());
|
||||
|
||||
|
||||
if(defaultTheme && themes[defaultTheme]) {
|
||||
setTheme(defaultTheme);
|
||||
}
|
||||
@@ -160,12 +157,12 @@ function loadInitialSettings() {
|
||||
setTheme(themeParameter);
|
||||
themeSelector.find('input').val(themeSelector.find('.option[data-value="' + themeParameter + '"]').text());
|
||||
}
|
||||
|
||||
|
||||
if (savedTheme && themes[savedTheme]) {
|
||||
setTheme(savedTheme);
|
||||
themeSelector.find('input').val(themeSelector.find('.option[data-value="' + savedTheme + '"]').text());
|
||||
}
|
||||
|
||||
|
||||
themeSelector.on('click', '.option', (event) => {
|
||||
const selectedTheme = $(event.target).data('value');
|
||||
setTheme(selectedTheme);
|
||||
@@ -173,15 +170,15 @@ function loadInitialSettings() {
|
||||
localStorage.setItem('theme', selectedTheme);
|
||||
setBg();
|
||||
});
|
||||
|
||||
|
||||
const signalSelector = $('#signal-selector');
|
||||
|
||||
|
||||
const signalParameter = getQueryParameter('signalUnits');
|
||||
if(signalParameter && !localStorage.getItem('signalUnit')) {
|
||||
signalSelector.find('input').val(signalSelector.find('.option[data-value="' + signalParameter + '"]').text());
|
||||
localStorage.setItem('signalUnit', signalParameter);
|
||||
} else signalSelector.find('input').val(signalSelector.find('.option[data-value="' + savedUnit + '"]').text());
|
||||
|
||||
|
||||
signalSelector.on('click', '.option', (event) => {
|
||||
const selectedSignalUnit = $(event.target).data('value');
|
||||
signalSelector.find('input').val($(event.target).text()); // Set the text of the clicked option to the input
|
||||
@@ -192,23 +189,23 @@ function loadInitialSettings() {
|
||||
if (extendedFreqRange === "true") {
|
||||
$("#extended-frequency-range").prop("checked", true);
|
||||
}
|
||||
|
||||
|
||||
$("#extended-frequency-range").change(function() {
|
||||
var isChecked = $(this).is(":checked");
|
||||
localStorage.setItem("extendedFreqRange", isChecked);
|
||||
});
|
||||
|
||||
|
||||
const psUnderscoreParameter = getQueryParameter('psUnderscores');
|
||||
if(psUnderscoreParameter) {
|
||||
$("#ps-underscores").prop("checked", JSON.parse(psUnderscoreParameter));
|
||||
}
|
||||
|
||||
|
||||
var psUnderscores = localStorage.getItem("psUnderscores");
|
||||
if (psUnderscores) {
|
||||
$("#ps-underscores").prop("checked", JSON.parse(psUnderscores));
|
||||
localStorage.setItem("psUnderscores", psUnderscores);
|
||||
}
|
||||
|
||||
|
||||
$("#ps-underscores").change(function() {
|
||||
var isChecked = $(this).is(":checked");
|
||||
localStorage.setItem("psUnderscores", isChecked);
|
||||
@@ -219,13 +216,13 @@ function loadInitialSettings() {
|
||||
$("#imperial-units").prop("checked", JSON.parse(imperialUnits));
|
||||
localStorage.setItem("imperialUnits", imperialUnits);
|
||||
}
|
||||
|
||||
|
||||
$("#imperial-units").change(function() {
|
||||
var isChecked = $(this).is(":checked");
|
||||
localStorage.setItem("imperialUnits", isChecked);
|
||||
});
|
||||
|
||||
|
||||
$('.version-string').text(currentVersion);
|
||||
|
||||
|
||||
setBg();
|
||||
}
|
||||
+2
-3
@@ -17,11 +17,10 @@ async function loadConsoleLogs() {
|
||||
html = stripAnsi(html);
|
||||
|
||||
const logColors = {
|
||||
INFO: "lime",
|
||||
DEBUG: "cyan",
|
||||
CHAT: "cyan",
|
||||
ERROR: "red",
|
||||
INFO: "lime",
|
||||
WARN: "yellow",
|
||||
ERROR: "red"
|
||||
};
|
||||
|
||||
let firstBracketProcessed = false;
|
||||
|
||||
+3
-4
@@ -43,11 +43,10 @@ function sendToast(type, title, message, persistent, important) {
|
||||
|
||||
function closeToast($toast) {
|
||||
$toast.removeClass('show');
|
||||
setTimeout(function () {
|
||||
$toast.remove();
|
||||
}, 300);
|
||||
setTimeout(() => $toast.remove(), 300);
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
// JS doesn't have a native capitilzation function? What the actual fuck in the toy language?
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
const versionDate = new Date('Apr 11, 2026 13:30:00');
|
||||
const versionDate = new Date('Jul 10, 2026 21:20:00');
|
||||
const currentVersion = `v1.4.0b [${versionDate.getDate()}/${versionDate.getMonth() + 1}/${versionDate.getFullYear()}]`;
|
||||
@@ -1,30 +0,0 @@
|
||||
$(document).ready(function() {
|
||||
$('.btn-prev').toggle($('.step:visible').index() !== 0);
|
||||
$('.btn-next').click(() => navigateStep(true));
|
||||
$('.btn-prev').click(() => navigateStep(false));
|
||||
});
|
||||
|
||||
function updateProgressBar(currentStep) {
|
||||
var stepIndex = $('.step').index(currentStep) + 1;
|
||||
$('.btn-rounded-cube').removeClass('activated');
|
||||
$('.btn-rounded-cube:lt(' + stepIndex + ')').addClass('activated');
|
||||
}
|
||||
|
||||
function updateWizardContent() {
|
||||
var visibleStepIndex = $('.step:visible').index();
|
||||
|
||||
$('.btn-prev').toggle(visibleStepIndex !== 0);
|
||||
$('.btn-next').text(visibleStepIndex === 4 ? 'Save' : 'Next');
|
||||
}
|
||||
|
||||
function navigateStep(isNext) {
|
||||
var currentStep = $('.step:visible');
|
||||
var targetStep = isNext ? currentStep.next('.step') : currentStep.prev('.step');
|
||||
|
||||
if (targetStep.length !== 0) {
|
||||
currentStep.hide();
|
||||
targetStep.show();
|
||||
updateProgressBar(targetStep);
|
||||
} else if (isNext) submitConfig();
|
||||
updateWizardContent();
|
||||
}
|
||||
Reference in New Issue
Block a user