diff --git a/TODO.org b/TODO.org
index 83a37c7..e795b02 100644
--- a/TODO.org
+++ b/TODO.org
@@ -1,4 +1,4 @@
-* Rundown to Launch. Still to do:
+** [#C] Rundown to Launch. Still to do:
* Setup asteroid.radio server at Hetzner [7/7]
- [X] Provision a VPS
@@ -25,23 +25,23 @@
1) [X] Liquidsoap is exposing its management console via telnet on the exterior network interface of b612.asteroid.radio
2) [X] icecast is also binding the external interface on b612, which it
should not be. HAproxy is there to mediate this flow.
-3) [ ] We're still on the built in i-lambdalite database
+3) [X] We're still on the built in i-lambdalite database
4) [X] The templates still advertise the default administrator password,
which is no bueno.
-5) [ ] We need to work out the TLS situation with letsencrypt, and
+5) [X] We need to work out the TLS situation with letsencrypt, and
integrate it into HAproxy.
6) [ ] The administrative interface should be beefed up.
- 6.1) [ ] Deactivate users
- 6.2) [ ] Change user access permissions
+ 6.1) [X] Deactivate users
+ 6.2) [X] Change user access permissions
6.3) [ ] Listener statistics, breakdown by day/hour, new users, % changed &c
7) [ ] When the player is paused, there are pretty serious stream sync issues in the form of stuttering for some time after the stream is unpaused.
8) [ ] User profile pages should probably be fleshed out.
9) [ ] the stream management features aren't there for Admins or DJs.
-10) [ ] The "Scan Library" feature is not working in the main branch
-11) [ ] The player widget should be styled so it fits the site theme on systems running 'light' thmes.
-12) [ ] ensure each info field 'Listeners: ..' &c has only one instance per page.
+10) [X] The "Scan Library" feature is not working in the main branch
+11) [X] The player widget should be styled so it fits the site theme on systems running 'light' thmes.
+12) [X] ensure each info field 'Listeners: ..' &c has only one instance per page.
* Server runtime configuration [0/1]
- [ ] parameterize all configuration for runtime loading [0/2]
diff --git a/docker/icecast.xml b/docker/icecast.xml
index 1ec1f94..5d2113f 100644
--- a/docker/icecast.xml
+++ b/docker/icecast.xml
@@ -10,7 +10,8 @@
15
10
1
- 65535
+
+ 8192
diff --git a/spectrum-analyzer.lisp b/spectrum-analyzer.lisp
index 66c3977..c284ff6 100644
--- a/spectrum-analyzer.lisp
+++ b/spectrum-analyzer.lisp
@@ -12,6 +12,18 @@
(defvar *canvas* nil)
(defvar *canvas-ctx* nil)
(defvar *animation-id* nil)
+ (defvar *media-source* nil)
+ (defvar *current-audio-element* nil)
+
+ (defun reset-spectrum-analyzer ()
+ "Reset the spectrum analyzer to allow reconnection after audio element reload"
+ (when *animation-id*
+ (cancel-animation-frame *animation-id*)
+ (setf *animation-id* nil))
+ (setf *audio-context* nil)
+ (setf *analyser* nil)
+ (setf *media-source* nil)
+ (ps:chain console (log "Spectrum analyzer reset for reconnection")))
(defun init-spectrum-analyzer ()
"Initialize the spectrum analyzer"
@@ -37,27 +49,35 @@
(:catch (e)
(ps:chain console (log "Cross-frame access error:" e)))))
- (when (and audio-element canvas-element (not *audio-context*))
- ;; Create Audio Context
- (setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
- (ps:@ window |webkitAudioContext|))))
+ (when (and audio-element canvas-element)
+ ;; Store current audio element
+ (setf *current-audio-element* audio-element)
- ;; Create Analyser Node
- (setf *analyser* (ps:chain *audio-context* (create-analyser)))
- (setf (ps:@ *analyser* |fftSize|) 256)
- (setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
-
- ;; Connect audio source to analyser
- (let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
- (ps:chain source (connect *analyser*))
- (ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
+ ;; Only create audio context and media source once
+ (when (not *audio-context*)
+ ;; Create Audio Context
+ (setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
+ (ps:@ window |webkitAudioContext|))))
+
+ ;; Create Analyser Node
+ (setf *analyser* (ps:chain *audio-context* (create-analyser)))
+ (setf (ps:@ *analyser* |fftSize|) 256)
+ (setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
+
+ ;; Connect audio source to analyser (can only be done once per element)
+ (setf *media-source* (ps:chain *audio-context* (create-media-element-source audio-element)))
+ (ps:chain *media-source* (connect *analyser*))
+ (ps:chain *analyser* (connect (ps:@ *audio-context* destination)))
+
+ (ps:chain console (log "Spectrum analyzer audio context created")))
;; Setup canvas
(setf *canvas* canvas-element)
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
- ;; Start visualization
- (draw-spectrum))))
+ ;; Start visualization if not already running
+ (when (not *animation-id*)
+ (draw-spectrum)))))
(defun draw-spectrum ()
"Draw the spectrum analyzer visualization"
diff --git a/static/js/admin.js b/static/js/admin.js
index a69f9ca..86fb4e2 100644
--- a/static/js/admin.js
+++ b/static/js/admin.js
@@ -635,6 +635,12 @@ function displayQueueSearchResults(results) {
// Live stream info update
async function updateLiveStreamInfo() {
+ // Don't update if stream is paused
+ const audioElement = document.getElementById('live-stream-audio');
+ if (audioElement && audioElement.paused) {
+ return;
+ }
+
try {
const response = await fetch('/api/asteroid/partial/now-playing-inline');
const contentType = response.headers.get("content-type");
diff --git a/static/js/front-page.js b/static/js/front-page.js
index 6f41ec1..af2fb68 100644
--- a/static/js/front-page.js
+++ b/static/js/front-page.js
@@ -55,6 +55,12 @@ function changeStreamQuality() {
// Update now playing info from Icecast
async function updateNowPlaying() {
+ // Don't update if stream is paused
+ const audioElement = document.getElementById('live-audio');
+ if (audioElement && audioElement.paused) {
+ return;
+ }
+
try {
const response = await fetch('/api/asteroid/partial/now-playing')
const contentType = response.headers.get("content-type")
@@ -102,9 +108,42 @@ window.addEventListener('DOMContentLoaded', function() {
// Update playing information right after load
updateNowPlaying();
- // Auto-reconnect on stream errors
+ // Auto-reconnect on stream errors and after long pauses
const audioElement = document.getElementById('live-audio');
if (audioElement) {
+ // Track pause timestamp to detect long pauses and reconnect
+ let pauseTimestamp = null;
+ const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
+
+ audioElement.addEventListener('pause', function() {
+ pauseTimestamp = Date.now();
+ console.log('Stream paused at:', pauseTimestamp);
+ });
+
+ audioElement.addEventListener('play', function() {
+ if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
+ console.log('Reconnecting stream after long pause to clear stale buffers...');
+
+ // Reset spectrum analyzer before reconnect
+ if (typeof resetSpectrumAnalyzer === 'function') {
+ resetSpectrumAnalyzer();
+ }
+
+ audioElement.load(); // Force reconnect to clear accumulated buffer
+
+ // Start playing the fresh stream and reinitialize spectrum analyzer
+ setTimeout(function() {
+ audioElement.play().catch(err => console.log('Reconnect play failed:', err));
+
+ if (typeof initSpectrumAnalyzer === 'function') {
+ initSpectrumAnalyzer();
+ console.log('Spectrum analyzer reinitialized after reconnect');
+ }
+ }, 500);
+ }
+ pauseTimestamp = null;
+ });
+
audioElement.addEventListener('error', function(e) {
console.log('Stream error, attempting reconnect in 3 seconds...');
setTimeout(function() {
diff --git a/static/js/player.js b/static/js/player.js
index 51783f2..cb794eb 100644
--- a/static/js/player.js
+++ b/static/js/player.js
@@ -26,6 +26,39 @@ document.addEventListener('DOMContentLoaded', function() {
if (liveAudio) {
// Reduce buffer to minimize delay
liveAudio.preload = 'none';
+
+ // Track pause timestamp to detect long pauses and reconnect
+ let pauseTimestamp = null;
+ const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
+
+ liveAudio.addEventListener('pause', function() {
+ pauseTimestamp = Date.now();
+ console.log('Live stream paused at:', pauseTimestamp);
+ });
+
+ liveAudio.addEventListener('play', function() {
+ if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
+ console.log('Reconnecting live stream after long pause to clear stale buffers...');
+
+ // Reset spectrum analyzer before reconnect
+ if (typeof resetSpectrumAnalyzer === 'function') {
+ resetSpectrumAnalyzer();
+ }
+
+ liveAudio.load(); // Force reconnect to clear accumulated buffer
+
+ // Start playing the fresh stream and reinitialize spectrum analyzer
+ setTimeout(function() {
+ liveAudio.play().catch(err => console.log('Reconnect play failed:', err));
+
+ if (typeof initSpectrumAnalyzer === 'function') {
+ initSpectrumAnalyzer();
+ console.log('Spectrum analyzer reinitialized after reconnect');
+ }
+ }, 500);
+ }
+ pauseTimestamp = null;
+ });
}
// Restore user quality preference
const selector = document.getElementById('live-stream-quality');
@@ -598,6 +631,12 @@ function changeLiveStreamQuality() {
// Live stream informatio update
async function updateNowPlaying() {
+ // Don't update if stream is paused
+ const liveAudio = document.getElementById('live-stream-audio');
+ if (liveAudio && liveAudio.paused) {
+ return;
+ }
+
try {
const response = await fetch('/api/asteroid/partial/now-playing')
const contentType = response.headers.get("content-type")
diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml
index 88777b1..b9398c9 100644
--- a/template/audio-player-frame.ctml
+++ b/template/audio-player-frame.ctml
@@ -47,6 +47,15 @@
});
}
+ // Track pause timestamp to detect long pauses and reconnect
+ let pauseTimestamp = null;
+ const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
+
+ audioElement.addEventListener('pause', function() {
+ pauseTimestamp = Date.now();
+ console.log('Frame player stream paused at:', pauseTimestamp);
+ });
+
// Add event listeners for debugging
audioElement.addEventListener('waiting', function() {
console.log('Audio buffering...');
@@ -60,6 +69,30 @@
console.error('Audio error:', e);
});
+ audioElement.addEventListener('play', function() {
+ if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
+ console.log('Reconnecting frame player stream after long pause to clear stale buffers...');
+
+ // Reset spectrum analyzer before reconnect
+ if (typeof resetSpectrumAnalyzer === 'function') {
+ resetSpectrumAnalyzer();
+ }
+
+ audioElement.load(); // Force reconnect to clear accumulated buffer
+
+ // Start playing the fresh stream and reinitialize spectrum analyzer
+ setTimeout(function() {
+ audioElement.play().catch(err => console.log('Reconnect play failed:', err));
+
+ if (typeof initSpectrumAnalyzer === 'function') {
+ initSpectrumAnalyzer();
+ console.log('Spectrum analyzer reinitialized after reconnect');
+ }
+ }, 500);
+ }
+ pauseTimestamp = null;
+ });
+
const selector = document.getElementById('stream-quality');
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
if (selector && selector.value !== streamQuality) {
@@ -112,6 +145,12 @@
// Update mini now playing display
async function updateMiniNowPlaying() {
+ // Don't update if stream is paused
+ const audioElement = document.getElementById('persistent-audio');
+ if (audioElement && audioElement.paused) {
+ return;
+ }
+
try {
const response = await fetch('/api/asteroid/partial/now-playing-inline');
if (response.ok) {
diff --git a/template/popout-player.ctml b/template/popout-player.ctml
index 569182f..7582eaa 100644
--- a/template/popout-player.ctml
+++ b/template/popout-player.ctml
@@ -97,6 +97,12 @@
// Update now playing info for popout
async function updatePopoutNowPlaying() {
+ // Don't update if stream is paused
+ const audioElement = document.getElementById('live-audio');
+ if (audioElement && audioElement.paused) {
+ return;
+ }
+
try {
const response = await fetch('/api/asteroid/partial/now-playing-inline');
const html = await response.text();
@@ -125,8 +131,42 @@
// Initial update
updatePopoutNowPlaying();
- // Auto-reconnect on stream errors
+ // Auto-reconnect on stream errors and after long pauses
const audioElement = document.getElementById('live-audio');
+
+ // Track pause timestamp to detect long pauses and reconnect
+ let pauseTimestamp = null;
+ const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
+
+ audioElement.addEventListener('pause', function() {
+ pauseTimestamp = Date.now();
+ console.log('Popout stream paused at:', pauseTimestamp);
+ });
+
+ audioElement.addEventListener('play', function() {
+ if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
+ console.log('Reconnecting popout stream after long pause to clear stale buffers...');
+
+ // Reset spectrum analyzer before reconnect
+ if (typeof resetSpectrumAnalyzer === 'function') {
+ resetSpectrumAnalyzer();
+ }
+
+ audioElement.load(); // Force reconnect to clear accumulated buffer
+
+ // Start playing the fresh stream and reinitialize spectrum analyzer
+ setTimeout(function() {
+ audioElement.play().catch(err => console.log('Reconnect play failed:', err));
+
+ if (typeof initSpectrumAnalyzer === 'function') {
+ initSpectrumAnalyzer();
+ console.log('Spectrum analyzer reinitialized after reconnect');
+ }
+ }, 500);
+ }
+ pauseTimestamp = null;
+ });
+
audioElement.addEventListener('error', function(e) {
console.log('Stream error, attempting reconnect in 3 seconds...');
setTimeout(function() {