Fix stream-player.js reconnect loop (play/pause race, stall backoff)

- Keep *is-reconnecting* true until 'playing' event fires, preventing
  pause/stalled handlers from triggering new reconnect cycles mid-flight
- Add exponential backoff for stall retries (5s, 10s, 20s... max 60s)
- Give up auto-reconnect after 10 stall attempts, show manual retry
- Add *stall-count* tracking, reset on successful playback
- Add *user-paused* guard to muted-tab pause handler
- Increase play() delay from 200ms to 500ms after load() for reliability

Fixes: AbortError from play()/pause() race, 429 Too Many Requests from
aggressive reconnect hammering, infinite reconnect loop on muted tabs.
This commit is contained in:
Glenn Thompson 2026-03-07 10:34:43 +03:00
parent bcfda2ebb6
commit f2e60b5648
1 changed files with 34 additions and 20 deletions

View File

@ -664,8 +664,10 @@
;; Error retry counter and reconnect state ;; Error retry counter and reconnect state
(defvar *stream-error-count* 0) (defvar *stream-error-count* 0)
(defvar *stall-count* 0)
(defvar *reconnect-timeout* nil) (defvar *reconnect-timeout* nil)
(defvar *is-reconnecting* false) (defvar *is-reconnecting* false)
(defvar *user-paused* false)
;; Reconnect stream - reuses existing audio element to preserve user gesture context ;; Reconnect stream - reuses existing audio element to preserve user gesture context
(defun reconnect-stream () (defun reconnect-stream ()
@ -704,15 +706,16 @@
(setf (ps:@ new-source type) (ps:@ config type)) (setf (ps:@ new-source type) (ps:@ config type))
(ps:chain audio (append-child new-source)))) (ps:chain audio (append-child new-source))))
;; Reload and play ;; Reload and play — keep *is-reconnecting* true until 'playing' fires
(ps:chain audio (load)) (ps:chain audio (load))
(setf *is-reconnecting* false)
(set-timeout (set-timeout
(lambda () (lambda ()
(ps:chain audio (play) (ps:chain audio (play)
(catch (lambda (error) (catch (lambda (error)
(ps:chain console (log "Reconnect play failed:" error)))))) (ps:chain console (log "Reconnect play failed:" error))
200))) ;; play() rejected — reset so next stall/error can retry
(setf *is-reconnecting* false)))))
500)))
;; Simple reconnect for popout player (just reload and play) ;; Simple reconnect for popout player (just reload and play)
(defun simple-reconnect (audio-element) (defun simple-reconnect (audio-element)
@ -737,6 +740,7 @@
(start-mini-spectrum) (start-mini-spectrum)
(hide-status) (hide-status)
(setf *stream-error-count* 0) (setf *stream-error-count* 0)
(setf *stall-count* 0)
(setf *is-reconnecting* false) (setf *is-reconnecting* false)
(when *reconnect-timeout* (when *reconnect-timeout*
(clear-timeout *reconnect-timeout*) (clear-timeout *reconnect-timeout*)
@ -759,15 +763,23 @@
(add-event-listener "stalled" (add-event-listener "stalled"
(lambda () (lambda ()
(unless *is-reconnecting* (unless *is-reconnecting*
(ps:chain console (log "Audio stalled, will auto-reconnect in 5 seconds...")) (setf *stall-count* (+ *stall-count* 1))
(show-status "⚠️ Stream stalled - reconnecting..." true) ;; Exponential backoff: 5s, 10s, 20s, max 60s
(let ((delay (ps:chain -math (min (* 5000 (ps:chain -math (pow 2 (- *stall-count* 1)))) 60000))))
(if (> *stall-count* 10)
;; Give up after 10 stall attempts — show manual retry
(progn
(ps:chain console (log "Too many stall retries, giving up auto-reconnect"))
(show-status "⚠️ Stream unavailable - click play to retry" true))
(progn
(ps:chain console (log (+ "Audio stalled (attempt " *stall-count* "), reconnecting in " (/ delay 1000) "s...")))
(show-status (+ "⚠️ Stream stalled - reconnecting (" *stall-count* ")...") true)
(setf *is-reconnecting* true) (setf *is-reconnecting* true)
(setf *reconnect-timeout* (setf *reconnect-timeout*
(set-timeout (set-timeout
(lambda () (lambda ()
;; Always reconnect on stall - ready-state is unreliable for streams
(reconnect-stream)) (reconnect-stream))
5000)))))) delay)))))))))
;; Handle ended event - stream shouldn't end, so reconnect ;; Handle ended event - stream shouldn't end, so reconnect
(ps:chain audio-element (ps:chain audio-element
@ -785,12 +797,14 @@
(lambda () (lambda ()
;; Stop mini spectrum when paused ;; Stop mini spectrum when paused
(stop-mini-spectrum) (stop-mini-spectrum)
;; If paused while muted and we didn't initiate it, browser may have throttled ;; Only treat as throttle if: muted, not reconnecting, and not user-initiated
(when (and (ps:@ audio-element muted) (not *is-reconnecting*)) (when (and (ps:@ audio-element muted)
(ps:chain console (log "Stream paused while muted (possible browser throttling), will reconnect in 3 seconds...")) (not *is-reconnecting*)
(not *user-paused*))
(ps:chain console (log "Stream paused while muted (possible browser throttling), will reconnect in 5 seconds..."))
(show-status "⚠️ Stream paused - reconnecting..." true) (show-status "⚠️ Stream paused - reconnecting..." true)
(setf *is-reconnecting* true) (setf *is-reconnecting* true)
(set-timeout (lambda () (reconnect-stream)) 3000)))))) (set-timeout (lambda () (reconnect-stream)) 5000))))))
;; Attach simple listeners for popout player ;; Attach simple listeners for popout player
(defun attach-popout-listeners (audio-element) (defun attach-popout-listeners (audio-element)