diff --git a/static/player.js b/static/player.js index 5fe7d03..fe7cf9c 100644 --- a/static/player.js +++ b/static/player.js @@ -22,6 +22,8 @@ elms.forEach(function(elm) { var Player = function(playlist) { this.playlist = playlist; this.index = 0; + this.isStream = false; // Track if current audio is a stream + this.durationCheckInterval = null; // For periodic duration validation // Display the title of the first track. track.innerHTML = '1. ' + playlist[0].title; @@ -54,15 +56,23 @@ Player.prototype = { if (data.howl) { sound = data.howl; } else { + // Detect if this is a streaming URL + self.isStream = (data.file === 'stream' || data.file.indexOf('stream') !== -1); + sound = data.howl = new Howl({ src: [ './' + data.file ], html5: true, // Force to HTML5 so that the audio can stream in (best for large files). onplay: function() { - // Display the duration. - duration.innerHTML = self.formatTime(Math.round(sound.duration())); + // Display the duration using our validation logic + duration.innerHTML = self.formatDuration(sound.duration()); // Start updating the progress of the track. requestAnimationFrame(self.step.bind(self)); + + // Start periodic duration checking for streams + if (self.isStream) { + self.startDurationCheck(); + } // Start the wave animation if we have already loaded wave.container.style.display = 'block'; @@ -76,17 +86,26 @@ Player.prototype = { loading.style.display = 'none'; }, onend: function() { + // Stop duration checking + self.stopDurationCheck(); + // Stop the wave animation. wave.container.style.display = 'none'; bar.style.display = 'block'; self.skip('next'); }, onpause: function() { + // Stop duration checking when paused + self.stopDurationCheck(); + // Stop the wave animation. wave.container.style.display = 'none'; bar.style.display = 'block'; }, onstop: function() { + // Stop duration checking + self.stopDurationCheck(); + // Stop the wave animation. wave.container.style.display = 'none'; bar.style.display = 'block'; @@ -222,7 +241,17 @@ Player.prototype = { // Determine our current seek position. var seek = sound.seek() || 0; timer.innerHTML = self.formatTime(Math.round(seek)); - progress.style.width = (((seek / sound.duration()) * 100) || 0) + '%'; + + // Calculate progress, handling infinite duration + var currentDuration = sound.duration(); + var correctedDuration = self.validateDuration(currentDuration); + + if (correctedDuration === Infinity || !isFinite(correctedDuration)) { + // For streams, don't show progress bar + progress.style.width = '0%'; + } else { + progress.style.width = (((seek / correctedDuration) * 100) || 0) + '%'; + } // If the sound is still playing, continue stepping. if (sound.playing()) { @@ -266,6 +295,83 @@ Player.prototype = { var seconds = (secs - minutes * 60) || 0; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; + }, + + /** + * Validate and correct duration for streaming audio + * @param {Number} duration Raw duration from Howler + * @return {Number} Corrected duration + */ + validateDuration: function(duration) { + // If duration is Infinity, keep it as is + if (duration === Infinity || !isFinite(duration)) { + return Infinity; + } + + // If duration is suspiciously large (> 24 hours), treat as stream + if (duration > 86400) { + return Infinity; + } + + // For streaming URLs, always return Infinity + if (this.isStream) { + return Infinity; + } + + return duration; + }, + + /** + * Format duration for display, handling streaming audio + * @param {Number} duration Duration in seconds + * @return {String} Formatted duration string + */ + formatDuration: function(duration) { + var correctedDuration = this.validateDuration(duration); + + if (correctedDuration === Infinity || !isFinite(correctedDuration)) { + // Add live class for styling + duration.classList.add('live'); + return 'LIVE'; + } + + // Remove live class for non-streaming content + duration.classList.remove('live'); + return this.formatTime(Math.round(correctedDuration)); + }, + + /** + * Start periodic duration checking for streams + */ + startDurationCheck: function() { + var self = this; + + if (self.durationCheckInterval) { + clearInterval(self.durationCheckInterval); + } + + self.durationCheckInterval = setInterval(function() { + var sound = self.playlist[self.index].howl; + if (sound && sound.playing()) { + var currentDuration = sound.duration(); + var correctedDuration = self.validateDuration(currentDuration); + + // If duration changed from finite to infinite or vice versa, update display + if ((correctedDuration === Infinity) !== (duration.innerHTML === 'LIVE')) { + duration.innerHTML = self.formatDuration(currentDuration); + } + } + }, 5000); // Check every 5 seconds + }, + + /** + * Stop periodic duration checking + */ + stopDurationCheck: function() { + if (this.durationCheckInterval) { + clearInterval(this.durationCheckInterval); + this.durationCheckInterval = null; + } } }; diff --git a/static/styles.css b/static/styles.css index 6383747..0cefcaf 100644 --- a/static/styles.css +++ b/static/styles.css @@ -62,6 +62,22 @@ body { text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33); } +/* Stream indicator styling for LIVE duration */ +#duration.live { + background: rgba(255, 0, 0, 0.8); + padding: 4px 8px; + border-radius: 4px; + font-weight: 500; + opacity: 0.9; + animation: livePulse 2s infinite; +} + +@keyframes livePulse { + 0% { opacity: 0.9; } + 50% { opacity: 0.6; } + 100% { opacity: 0.9; } +} + /* Controls */ .controlsOuter { position: absolute;