From 2cd128260c18fa02ddd857dea2f271d6958e1ef0 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Fri, 12 Dec 2025 18:11:09 +0300 Subject: [PATCH] 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 --- parenscript/front-page.lisp | 47 ++++++++++++++++--------- template/audio-player-frame.ctml | 59 ++++++++++++++++++++++++-------- template/popout-player.ctml | 49 ++++++++++++++++++++++---- 3 files changed, 117 insertions(+), 38 deletions(-) diff --git a/parenscript/front-page.lisp b/parenscript/front-page.lisp index ac5f59d..6a15c26 100644 --- a/parenscript/front-page.lisp +++ b/parenscript/front-page.lisp @@ -311,38 +311,50 @@ (defun attach-audio-event-listeners (audio-element) "Attach all necessary event listeners to an audio element" - ;; Error handler + ;; Error handler - retry indefinitely with exponential backoff (ps:chain audio-element (add-event-listener "error" (lambda (err) - (incf *stream-error-count*) (ps:chain console (log "Stream error:" err)) - - (if (< *stream-error-count* 3) - ;; Auto-retry for first few errors - (progn - (show-stream-status (+ "⚠️ Stream error. Reconnecting... (attempt " *stream-error-count* ")") "warning") - (setf *reconnect-timeout* - (set-timeout reconnect-stream 3000))) - ;; Too many errors, show manual reconnect - (progn - (show-stream-status "❌ Stream connection lost. Click Reconnect to try again." "error") - (show-reconnect-button)))))) + (when *is-reconnecting* + (return)) + (incf *stream-error-count*) + ;; Calculate delay with exponential backoff (3s, 6s, 12s, max 30s) + (let ((delay (ps:chain |Math| (min (* 3000 (ps:chain |Math| (pow 2 (- *stream-error-count* 1)))) 30000)))) + (show-stream-status (+ "⚠️ Stream error. Reconnecting in " (/ delay 1000) "s... (attempt " *stream-error-count* ")") "warning") + (setf *is-reconnecting* t) + (setf *reconnect-timeout* + (set-timeout reconnect-stream delay)))))) ;; Stalled handler (ps:chain audio-element (add-event-listener "stalled" (lambda () - (ps:chain console (log "Stream stalled")) - (show-stream-status "⚠️ Stream stalled. Attempting to recover..." "warning") + (when *is-reconnecting* + (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* (set-timeout (lambda () ;; Only reconnect if still stalled - (when (ps:@ audio-element paused) - (reconnect-stream))) + (if (< (ps:@ audio-element ready-state) 3) + (reconnect-stream) + (setf *is-reconnecting* false))) 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) (ps:chain audio-element (add-event-listener "waiting" @@ -355,6 +367,7 @@ (add-event-listener "playing" (lambda () (setf *stream-error-count* 0) + (setf *is-reconnecting* false) (hide-stream-status) (hide-reconnect-button) (when *reconnect-timeout* diff --git a/template/audio-player-frame.ctml b/template/audio-player-frame.ctml index 1dc5111..0d72fa1 100644 --- a/template/audio-player-frame.ctml +++ b/template/audio-player-frame.ctml @@ -54,18 +54,8 @@ }); } - // Add event listeners for debugging - audioElement.addEventListener('waiting', function() { - console.log('Audio buffering...'); - }); - - audioElement.addEventListener('playing', function() { - console.log('Audio playing'); - }); - - audioElement.addEventListener('error', function(e) { - console.error('Audio error:', e); - }); + // Note: Main event listeners are attached via attachAudioListeners() + // which is called at the bottom of this script const selector = document.getElementById('stream-quality'); const streamQuality = localStorage.getItem('stream-quality') || 'aac'; @@ -251,6 +241,11 @@ }, 300); } + // Error retry counter and reconnect state + var streamErrorCount = 0; + var reconnectTimeout = null; + var isReconnecting = false; + // Attach event listeners to audio element function attachAudioListeners(audioElement) { audioElement.addEventListener('waiting', function() { @@ -260,16 +255,50 @@ audioElement.addEventListener('playing', function() { console.log('Audio playing'); 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) { 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() { - console.log('Audio stalled'); - showStatus('⚠️ Stream stalled - click 🔄 if no audio', true); + if (isReconnecting) return; // Already reconnecting, skip + 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); }); } diff --git a/template/popout-player.ctml b/template/popout-player.ctml index 569182f..c22b7a6 100644 --- a/template/popout-player.ctml +++ b/template/popout-player.ctml @@ -127,18 +127,55 @@ // Auto-reconnect on stream errors 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) { - console.log('Stream error, attempting reconnect in 3 seconds...'); - setTimeout(function() { + console.error('Audio error:', e); + 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.play().catch(err => console.log('Reconnect failed:', err)); - }, 3000); + }, delay); }); audioElement.addEventListener('stalled', function() { - console.log('Stream stalled, reloading...'); - audioElement.load(); - audioElement.play().catch(err => console.log('Reload failed:', err)); + if (isReconnecting) return; + console.log('Stream stalled, will auto-reconnect in 5 seconds...'); + 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