From e510ce61e674413f512efb3346b099930259b8bb Mon Sep 17 00:00:00 2001 From: AmateurAudioDude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Sun, 14 Jul 2024 19:52:12 +1000 Subject: [PATCH] Several fixes * Fixed rare unprompted auto-restart stream bug (bkram) [/web/js/3las/main.js] * Fixed multiple tooltip bug while RDS PS is tentatively loaded [/web/js/main.js] * Changed copying of plugin files to symlinks (junction for Windows) [/server/plugins.js] * Auto-reconnect audio stream on restored/changed internet connection [/web/js/3las/3las.js] * Main WebSocket connection can be shared with plugins [/web/js/websocket.js] [/web/index.ejs] --- server/plugins.js | 55 +++++++++++++++++++++++++++++++---- web/index.ejs | 1 + web/js/3las/3las.js | 46 +++++++++++++++++++++++++++-- web/js/3las/main.js | 70 +++++++++++++++++++++++++++++++-------------- web/js/main.js | 13 +++++---- web/js/websocket.js | 30 +++++++++++++++++++ 6 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 web/js/websocket.js diff --git a/server/plugins.js b/server/plugins.js index 3b87e6b..7446d94 100644 --- a/server/plugins.js +++ b/server/plugins.js @@ -21,22 +21,39 @@ function parsePluginConfig(filePath) { // Check if pluginConfig has frontEndPath defined if (pluginConfig.frontEndPath) { const sourcePath = path.join(path.dirname(filePath), pluginConfig.frontEndPath); - const destinationDir = path.join(path.dirname(filePath), '../web/js/plugins', pluginConfig.frontEndPath, '..'); + const destinationDir = path.join(__dirname, '../web/js/plugins', path.dirname(pluginConfig.frontEndPath)); + + // Check if the source path exists + if (!fs.existsSync(sourcePath)) { + console.error(`Error: source path ${sourcePath} does not exist.`); + return pluginConfig; + } // Check if the destination directory exists, if not, create it if (!fs.existsSync(destinationDir)) { fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively } - // Copy the file to the destination directory const destinationFile = path.join(destinationDir, path.basename(sourcePath)); - fs.copyFileSync(sourcePath, destinationFile); - console.log(`File copied from ${sourcePath} to ${destinationFile}`); + + // Platform-specific handling for symlinks/junctions + if (process.platform !== 'win32') { + // On Linux, create a symlink + try { + if (fs.existsSync(destinationFile)) { + fs.unlinkSync(destinationFile); // Remove existing file/symlink + } + fs.symlinkSync(sourcePath, destinationFile); + console.log(`Symlink created from ${sourcePath} to ${destinationFile}`); + } catch (err) { + console.error(`Error creating symlink at ${destinationFile}: ${err.message}`); + } + } } else { console.error(`Error: frontEndPath is not defined in ${filePath}`); } } catch (err) { - console.error(`Error parsing plugin config from ${filePath}: ${err}`); + console.error(`Error parsing plugin config from ${filePath}: ${err.message}`); } return pluginConfig; @@ -59,9 +76,35 @@ function collectPluginConfigs() { return pluginConfigs; } +// Ensure the web/js/plugins directory exists +const webJsPluginsDir = path.join(__dirname, '../web/js/plugins'); +if (!fs.existsSync(webJsPluginsDir)) { + fs.mkdirSync(webJsPluginsDir, { recursive: true }); +} + +// Main function to create symlinks/junctions for plugins +function createLinks() { + const pluginsDir = path.join(__dirname, '../plugins'); + const destinationPluginsDir = path.join(__dirname, '../web/js/plugins'); + + if (process.platform === 'win32') { + // On Windows, create a junction + try { + if (fs.existsSync(destinationPluginsDir)) { + fs.rmSync(destinationPluginsDir, { recursive: true }); + } + fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction'); + console.log(`Junction created from ${pluginsDir} to ${destinationPluginsDir}`); + } catch (err) { + console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`); + } + } +} + // Usage example const allPluginConfigs = collectPluginConfigs(); +createLinks(); module.exports = { allPluginConfigs -} \ No newline at end of file +}; diff --git a/web/index.ejs b/web/index.ejs index b8ef67b..8f65420 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -490,6 +490,7 @@ + <% plugins?.forEach(function(plugin) { %> diff --git a/web/js/3las/3las.js b/web/js/3las/3las.js index 3ec297f..bfc2f6c 100644 --- a/web/js/3las/3las.js +++ b/web/js/3las/3las.js @@ -45,6 +45,7 @@ var _3LAS = /** @class */ (function () { }; _3LAS.prototype.Start = function () { this.ConnectivityFlag = false; + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug // This is stupid, but required for Android.... thanks Google :( if (this.WakeLock) @@ -128,11 +129,50 @@ var _3LAS = /** @class */ (function () { this.ConnectivityFlag = false; if (this.ConnectivityCallback) this.ConnectivityCallback(false); + } + + if (shouldReconnect) { + if (!this.ConnectivityFlag) { + console.log("Initial reconnect attempt..."); + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug + this.Start(); } - this.Start(); - }; + + // Delay launch of subsequent reconnect attempts by 3 seconds + setTimeout(() => { + + let streamReconnecting = false; + + let intervalReconnect = setInterval(() => { + if (this.ConnectivityFlag || typeof Stream === 'undefined' || Stream === null) { + console.log("Reconnect attempts aborted."); + clearInterval(intervalReconnect); + } else if (!streamReconnecting) { + streamReconnecting = true; + console.log("Attempting to restart stream..."); + this.Stop(); // Attempt to mitigate the 0.5x speed/multiple stream bug + this.Start(); + // Wait for reconnect attempt + setTimeout(() => { + streamReconnecting = false; + }, 3000); + } + // Restore user set volume + if (Stream && typeof newVolumeGlobal !== 'undefined' && newVolumeGlobal !== null) { + Stream.Volume = newVolumeGlobal; + console.log(`User volume restored: ${Math.round(newVolumeGlobal * 100)}%`); + } + }, 3000); + + }, 3000); + + } else { + this.Logger.Log("Reconnection is disabled."); + } +}; + _3LAS.prototype.OnSocketDataReady = function (data) { this.Fallback.OnSocketDataReady(data); }; return _3LAS; -}()); \ No newline at end of file +}()); diff --git a/web/js/3las/main.js b/web/js/3las/main.js index 4812bed..9e716cb 100644 --- a/web/js/3las/main.js +++ b/web/js/3las/main.js @@ -1,43 +1,69 @@ const DefaultVolume = 0.5; let Stream; +let shouldReconnect = true; +let newVolumeGlobal = 1; function Init(_ev) { - try { - const settings = new _3LAS_Settings(); - if (!Stream) { // Ensure Stream is not re-initialized - Stream = new _3LAS(null, settings); - } - } catch (error) { - console.log(error); - return; - } - - Stream.ConnectivityCallback = OnConnectivityCallback; $(".playbutton").off('click').on('click', OnPlayButtonClick); // Ensure only one event handler is attached $("#volumeSlider").off("input").on("input", updateVolume); // Ensure only one event handler is attached } +function createStream() { + try { + const settings = new _3LAS_Settings(); + Stream = new _3LAS(null, settings); + Stream.ConnectivityCallback = OnConnectivityCallback; + } catch (error) { + console.error("Initialization Error: ", error); + } +} + +function destroyStream() { + if (Stream) { + Stream.Stop(); + Stream = null; + } +} + function OnConnectivityCallback(isConnected) { - Stream.Volume = isConnected ? 1.0 : DefaultVolume; + console.log("Connectivity changed:", isConnected); + if (Stream) { + Stream.Volume = isConnected ? 1.0 : DefaultVolume; + } else { + console.warn("Stream is not initialized."); + } } function OnPlayButtonClick(_ev) { const $playbutton = $('.playbutton'); - $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); - - if (Stream.ConnectivityFlag) { - Stream.Stop(); + if (Stream) { + console.log("Stopping stream..."); + shouldReconnect = false; + destroyStream(); + $playbutton.find('.fa-solid').toggleClass('fa-stop fa-play'); } else { + console.log("Starting stream..."); + shouldReconnect = true; + createStream(); Stream.Start(); - $playbutton.addClass('bg-gray').prop('disabled', true); - setTimeout(() => { - $playbutton.removeClass('bg-gray').prop('disabled', false); - }, 3000); + $playbutton.find('.fa-solid').toggleClass('fa-play fa-stop'); } + $playbutton.addClass('bg-gray').prop('disabled', true); + setTimeout(() => { + $playbutton.removeClass('bg-gray').prop('disabled', false); + }, 3000); } function updateVolume() { - Stream.Volume = $(this).val(); + if (Stream) { + const newVolume = $(this).val(); + newVolumeGlobal = newVolume; + console.log("Volume updated to:", newVolume); + Stream.Volume = newVolume; + } else { + console.warn("Stream is not initialized."); + } } -$(document).ready(Init); \ No newline at end of file +$(document).ready(Init); + diff --git a/web/js/main.js b/web/js/main.js index 8ce773a..aa5ea70 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -1,7 +1,7 @@ -var url = new URL('text', window.location.href); -url.protocol = url.protocol.replace('http', 'ws'); -var socketAddress = url.href; -var socket = new WebSocket(socketAddress); +// WebSocket connection located in ./websocket.js + + + var parsedData, signalChart, previousFreq; var signalData = []; var data = []; @@ -10,7 +10,7 @@ let updateCounter = 0; const europe_programmes = [ "No PTY", "News", "Current Affairs", "Info", "Sport", "Education", "Drama", "Culture", "Science", "Varied", - "Pop M", "Rock M", "Easy Listening", "Light Classical", + "Pop Music", "Rock Music", "Easy Listening", "Light Classical", "Serious Classical", "Other Music", "Weather", "Finance", "Children's Programmes", "Social Affairs", "Religion", "Phone-in", "Travel", "Leisure", "Jazz Music", "Country Music", "National Music", @@ -881,7 +881,7 @@ function initTooltips() { // Add a delay of 500 milliseconds before creating and appending the tooltip $(this).data('timeout', setTimeout(() => { var tooltip = $('
').html(tooltipText); - $('body').append(tooltip); + if ($('.tooltiptext').length === 0) { $('body').append(tooltip); } // Don't allow more than one tooltip var posX = e.pageX; var posY = e.pageY; @@ -896,6 +896,7 @@ function initTooltips() { // Clear the timeout if the mouse leaves before the delay completes clearTimeout($(this).data('timeout')); $('.tooltiptext').remove(); + setTimeout(() => { $('.tooltiptext').remove(); }, 500); // Ensure no tooltips remain stuck }).mousemove(function(e){ var tooltipWidth = $('.tooltiptext').outerWidth(); var tooltipHeight = $('.tooltiptext').outerHeight(); diff --git a/web/js/websocket.js b/web/js/websocket.js new file mode 100644 index 0000000..d965d33 --- /dev/null +++ b/web/js/websocket.js @@ -0,0 +1,30 @@ +var url = new URL('text', window.location.href); +url.protocol = url.protocol.replace('http', 'ws'); +var socketAddress = url.href; +var socket = new WebSocket(socketAddress); + +const socketPromise = new Promise((resolve, reject) => { + // Event listener for when the WebSocket connection is open + socket.addEventListener('open', () => { + console.log('WebSocket connection open'); + resolve(socket); // Resolve the promise with the WebSocket instance + }); + + // Event listener for WebSocket errors + socket.addEventListener('error', (error) => { + console.error('WebSocket error', error); + reject(error); // Reject the promise on error + }); + + // Event listener for WebSocket connection closure + socket.addEventListener('close', () => { + console.warn('WebSocket connection closed'); + reject(new Error('WebSocket connection closed')); // Reject with closure warning + }); +}); + +// Assign the socketPromise to window.socketPromise for global access +window.socketPromise = socketPromise; + +// Assign the socket instance to window.socket for global access +window.socket = socket;