Add robust auto-reconnect to all audio players

- Implement isReconnecting flag to prevent duplicate reconnect attempts
- Add exponential backoff for error retries (3s, 6s, 12s, max 30s)
- Retry indefinitely until stream returns
- Handle error, stalled, and ended events consistently
- Reset state on successful playback
- Apply same logic to frame player, popout player, and front-page player
This commit is contained in:
Glenn Thompson 2025-12-12 18:11:09 +03:00 committed by Brian O'Reilly
parent 8b0f7e7705
commit 2cd128260c
3 changed files with 117 additions and 38 deletions

View File

@ -311,38 +311,50 @@
(defun attach-audio-event-listeners (audio-element) (defun attach-audio-event-listeners (audio-element)
"Attach all necessary event listeners to an audio element" "Attach all necessary event listeners to an audio element"
;; Error handler ;; Error handler - retry indefinitely with exponential backoff
(ps:chain audio-element (ps:chain audio-element
(add-event-listener "error" (add-event-listener "error"
(lambda (err) (lambda (err)
(incf *stream-error-count*)
(ps:chain console (log "Stream error:" err)) (ps:chain console (log "Stream error:" err))
(when *is-reconnecting*
(if (< *stream-error-count* 3) (return))
;; Auto-retry for first few errors (incf *stream-error-count*)
(progn ;; Calculate delay with exponential backoff (3s, 6s, 12s, max 30s)
(show-stream-status (+ "⚠️ Stream error. Reconnecting... (attempt " *stream-error-count* ")") "warning") (let ((delay (ps:chain |Math| (min (* 3000 (ps:chain |Math| (pow 2 (- *stream-error-count* 1)))) 30000))))
(setf *reconnect-timeout* (show-stream-status (+ "⚠️ Stream error. Reconnecting in " (/ delay 1000) "s... (attempt " *stream-error-count* ")") "warning")
(set-timeout reconnect-stream 3000))) (setf *is-reconnecting* t)
;; Too many errors, show manual reconnect (setf *reconnect-timeout*
(progn (set-timeout reconnect-stream delay))))))
(show-stream-status "❌ Stream connection lost. Click Reconnect to try again." "error")
(show-reconnect-button))))))
;; Stalled handler ;; Stalled handler
(ps:chain audio-element (ps:chain audio-element
(add-event-listener "stalled" (add-event-listener "stalled"
(lambda () (lambda ()
(ps:chain console (log "Stream stalled")) (when *is-reconnecting*
(show-stream-status "⚠️ Stream stalled. Attempting to recover..." "warning") (return))
(ps:chain console (log "Stream stalled, will auto-reconnect in 5 seconds..."))
(show-stream-status "⚠️ Stream stalled - reconnecting..." "warning")
(setf *is-reconnecting* t)
(setf *reconnect-timeout* (setf *reconnect-timeout*
(set-timeout (set-timeout
(lambda () (lambda ()
;; Only reconnect if still stalled ;; Only reconnect if still stalled
(when (ps:@ audio-element paused) (if (< (ps:@ audio-element ready-state) 3)
(reconnect-stream))) (reconnect-stream)
(setf *is-reconnecting* false)))
5000))))) 5000)))))
;; Ended handler - stream shouldn't end, so reconnect
(ps:chain audio-element
(add-event-listener "ended"
(lambda ()
(when *is-reconnecting*
(return))
(ps:chain console (log "Stream ended unexpectedly, reconnecting..."))
(show-stream-status "⚠️ Stream ended - reconnecting..." "warning")
(setf *is-reconnecting* t)
(set-timeout reconnect-stream 2000))))
;; Waiting handler (buffering) ;; Waiting handler (buffering)
(ps:chain audio-element (ps:chain audio-element
(add-event-listener "waiting" (add-event-listener "waiting"
@ -355,6 +367,7 @@
(add-event-listener "playing" (add-event-listener "playing"
(lambda () (lambda ()
(setf *stream-error-count* 0) (setf *stream-error-count* 0)
(setf *is-reconnecting* false)
(hide-stream-status) (hide-stream-status)
(hide-reconnect-button) (hide-reconnect-button)
(when *reconnect-timeout* (when *reconnect-timeout*

View File

@ -54,18 +54,8 @@
}); });
} }
// Add event listeners for debugging // Note: Main event listeners are attached via attachAudioListeners()
audioElement.addEventListener('waiting', function() { // which is called at the bottom of this script
console.log('Audio buffering...');
});
audioElement.addEventListener('playing', function() {
console.log('Audio playing');
});
audioElement.addEventListener('error', function(e) {
console.error('Audio error:', e);
});
const selector = document.getElementById('stream-quality'); const selector = document.getElementById('stream-quality');
const streamQuality = localStorage.getItem('stream-quality') || 'aac'; const streamQuality = localStorage.getItem('stream-quality') || 'aac';
@ -251,6 +241,11 @@
}, 300); }, 300);
} }
// Error retry counter and reconnect state
var streamErrorCount = 0;
var reconnectTimeout = null;
var isReconnecting = false;
// Attach event listeners to audio element // Attach event listeners to audio element
function attachAudioListeners(audioElement) { function attachAudioListeners(audioElement) {
audioElement.addEventListener('waiting', function() { audioElement.addEventListener('waiting', function() {
@ -260,16 +255,50 @@
audioElement.addEventListener('playing', function() { audioElement.addEventListener('playing', function() {
console.log('Audio playing'); console.log('Audio playing');
hideStatus(); hideStatus();
streamErrorCount = 0; // Reset error count on successful play
isReconnecting = false; // Reset reconnecting flag
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
}); });
audioElement.addEventListener('error', function(e) { audioElement.addEventListener('error', function(e) {
console.error('Audio error:', e); console.error('Audio error:', e);
showStatus('⚠️ Stream error - click 🔄 to reconnect', true); if (isReconnecting) return; // Already reconnecting, skip
streamErrorCount++;
// Calculate delay with exponential backoff (3s, 6s, 12s, max 30s)
var delay = Math.min(3000 * Math.pow(2, streamErrorCount - 1), 30000);
showStatus('⚠️ Stream error. Reconnecting in ' + (delay/1000) + 's... (attempt ' + streamErrorCount + ')', true);
isReconnecting = true;
reconnectTimeout = setTimeout(function() {
reconnectStream();
}, delay);
}); });
audioElement.addEventListener('stalled', function() { audioElement.addEventListener('stalled', function() {
console.log('Audio stalled'); if (isReconnecting) return; // Already reconnecting, skip
showStatus('⚠️ Stream stalled - click 🔄 if no audio', true); console.log('Audio stalled, will auto-reconnect in 5 seconds...');
showStatus('⚠️ Stream stalled - reconnecting...', true);
isReconnecting = true;
setTimeout(function() {
if (audioElement.readyState < 3) {
reconnectStream();
} else {
isReconnecting = false;
}
}, 5000);
});
// Handle ended event - stream shouldn't end, so reconnect
audioElement.addEventListener('ended', function() {
if (isReconnecting) return; // Already reconnecting, skip
console.log('Stream ended unexpectedly, reconnecting...');
showStatus('⚠️ Stream ended - reconnecting...', true);
isReconnecting = true;
setTimeout(function() {
reconnectStream();
}, 2000);
}); });
} }

View File

@ -127,18 +127,55 @@
// Auto-reconnect on stream errors // Auto-reconnect on stream errors
const audioElement = document.getElementById('live-audio'); const audioElement = document.getElementById('live-audio');
var streamErrorCount = 0;
var reconnectTimeout = null;
var isReconnecting = false;
audioElement.addEventListener('playing', function() {
console.log('Audio playing');
streamErrorCount = 0;
isReconnecting = false;
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
});
audioElement.addEventListener('error', function(e) { audioElement.addEventListener('error', function(e) {
console.log('Stream error, attempting reconnect in 3 seconds...'); console.error('Audio error:', e);
setTimeout(function() { if (isReconnecting) return;
streamErrorCount++;
var delay = Math.min(3000 * Math.pow(2, streamErrorCount - 1), 30000);
console.log('Stream error. Reconnecting in ' + (delay/1000) + 's... (attempt ' + streamErrorCount + ')');
isReconnecting = true;
reconnectTimeout = setTimeout(function() {
audioElement.load(); audioElement.load();
audioElement.play().catch(err => console.log('Reconnect failed:', err)); audioElement.play().catch(err => console.log('Reconnect failed:', err));
}, 3000); }, delay);
}); });
audioElement.addEventListener('stalled', function() { audioElement.addEventListener('stalled', function() {
console.log('Stream stalled, reloading...'); if (isReconnecting) return;
audioElement.load(); console.log('Stream stalled, will auto-reconnect in 5 seconds...');
audioElement.play().catch(err => console.log('Reload failed:', err)); isReconnecting = true;
setTimeout(function() {
if (audioElement.readyState < 3) {
audioElement.load();
audioElement.play().catch(err => console.log('Reload failed:', err));
} else {
isReconnecting = false;
}
}, 5000);
});
audioElement.addEventListener('ended', function() {
if (isReconnecting) return;
console.log('Stream ended unexpectedly, reconnecting...');
isReconnecting = true;
setTimeout(function() {
audioElement.load();
audioElement.play().catch(err => console.log('Reconnect failed:', err));
}, 2000);
}); });
// Notify parent window that popout is open // Notify parent window that popout is open