Fix wedged player with reconnect button and volume preservation
- Add reconnect button (🔄) to frameset player bar
- Recreate audio element on reconnect to fix wedged MediaElementSource
- Properly close and reinitialize AudioContext for spectrum analyzer
- Preserve volume and muted state when reconnecting
- Show status messages for connection issues
- Reduce Now Playing update interval to 5 seconds
- Add front-page.js to player-content.ctml for Now Playing updates
- Create About pages (about.ctml and about-content.ctml)
- Add About link to navigation in both modes
This commit is contained in:
parent
8fd0b06b69
commit
8c19e0fbde
|
|
@ -930,6 +930,20 @@
|
||||||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
|
;; About page (non-frameset mode)
|
||||||
|
(define-page about-page #@"/about" ()
|
||||||
|
"About Asteroid Radio"
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "about")
|
||||||
|
:title "About - Asteroid Radio"))
|
||||||
|
|
||||||
|
;; About content (for frameset mode)
|
||||||
|
(define-page about-content #@"/about-content" ()
|
||||||
|
"About page content (displayed in content frame)"
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "about-content")
|
||||||
|
:title "About - Asteroid Radio"))
|
||||||
|
|
||||||
(define-api asteroid/status () ()
|
(define-api asteroid/status () ()
|
||||||
"Get server status"
|
"Get server status"
|
||||||
(api-output `(("status" . "running")
|
(api-output `(("status" . "running")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@
|
||||||
(ps:ps*
|
(ps:ps*
|
||||||
'(progn
|
'(progn
|
||||||
|
|
||||||
|
;; Stream connection state
|
||||||
|
(defvar *stream-error-count* 0)
|
||||||
|
(defvar *last-play-attempt* 0)
|
||||||
|
(defvar *is-reconnecting* false)
|
||||||
|
(defvar *reconnect-timeout* nil)
|
||||||
|
|
||||||
;; Stream quality configuration
|
;; Stream quality configuration
|
||||||
(defun get-stream-config (stream-base-url encoding)
|
(defun get-stream-config (stream-base-url encoding)
|
||||||
(let ((config (ps:create
|
(let ((config (ps:create
|
||||||
|
|
@ -137,6 +143,249 @@
|
||||||
(ps:chain local-storage (remove-item "useFrameset"))
|
(ps:chain local-storage (remove-item "useFrameset"))
|
||||||
(setf (ps:@ window location href) "/asteroid/"))
|
(setf (ps:@ window location href) "/asteroid/"))
|
||||||
|
|
||||||
|
;; Stream status UI functions
|
||||||
|
(defun show-stream-status (message status-type)
|
||||||
|
"Show a status message to the user. status-type: 'error', 'warning', 'success', 'info'"
|
||||||
|
(let ((indicator (ps:chain document (get-element-by-id "stream-status-indicator"))))
|
||||||
|
(when indicator
|
||||||
|
(setf (ps:@ indicator inner-text) message)
|
||||||
|
(setf (ps:@ indicator style display) "block")
|
||||||
|
(setf (ps:@ indicator style background)
|
||||||
|
(cond
|
||||||
|
((= status-type "error") "#550000")
|
||||||
|
((= status-type "warning") "#554400")
|
||||||
|
((= status-type "success") "#005500")
|
||||||
|
(t "#003355")))
|
||||||
|
(setf (ps:@ indicator style border)
|
||||||
|
(cond
|
||||||
|
((= status-type "error") "1px solid #ff0000")
|
||||||
|
((= status-type "warning") "1px solid #ffaa00")
|
||||||
|
((= status-type "success") "1px solid #00ff00")
|
||||||
|
(t "1px solid #00aaff"))))))
|
||||||
|
|
||||||
|
(defun hide-stream-status ()
|
||||||
|
"Hide the status indicator"
|
||||||
|
(let ((indicator (ps:chain document (get-element-by-id "stream-status-indicator"))))
|
||||||
|
(when indicator
|
||||||
|
(setf (ps:@ indicator style display) "none"))))
|
||||||
|
|
||||||
|
(defun show-reconnect-button ()
|
||||||
|
"Show the reconnect button"
|
||||||
|
(let ((btn (ps:chain document (get-element-by-id "reconnect-btn"))))
|
||||||
|
(when btn
|
||||||
|
(setf (ps:@ btn style display) "inline-block"))))
|
||||||
|
|
||||||
|
(defun hide-reconnect-button ()
|
||||||
|
"Hide the reconnect button"
|
||||||
|
(let ((btn (ps:chain document (get-element-by-id "reconnect-btn"))))
|
||||||
|
(when btn
|
||||||
|
(setf (ps:@ btn style display) "none"))))
|
||||||
|
|
||||||
|
;; Recreate audio element to fix wedged state
|
||||||
|
(defun recreate-audio-element ()
|
||||||
|
"Recreate the audio element entirely to fix wedged MediaElementSource"
|
||||||
|
(let* ((container (ps:chain document (get-element-by-id "audio-container")))
|
||||||
|
(old-audio (ps:chain document (get-element-by-id "live-audio")))
|
||||||
|
(stream-base-url (ps:chain document (get-element-by-id "stream-base-url")))
|
||||||
|
(stream-quality (or (ps:chain local-storage (get-item "stream-quality")) "aac"))
|
||||||
|
(config (get-stream-config (ps:@ stream-base-url value) stream-quality)))
|
||||||
|
|
||||||
|
(when (and container old-audio)
|
||||||
|
;; Reset spectrum analyzer before removing audio
|
||||||
|
(when (ps:@ window |resetSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (reset-spectrum-analyzer)))
|
||||||
|
|
||||||
|
;; Remove old audio element
|
||||||
|
(ps:chain old-audio (pause))
|
||||||
|
(setf (ps:@ old-audio src) "")
|
||||||
|
(ps:chain old-audio (remove))
|
||||||
|
|
||||||
|
;; Create new audio element
|
||||||
|
(let ((new-audio (ps:chain document (create-element "audio"))))
|
||||||
|
(setf (ps:@ new-audio id) "live-audio")
|
||||||
|
(setf (ps:@ new-audio controls) t)
|
||||||
|
(setf (ps:@ new-audio crossorigin) "anonymous")
|
||||||
|
(setf (ps:@ new-audio style width) "100%")
|
||||||
|
(setf (ps:@ new-audio style margin) "10px 0")
|
||||||
|
|
||||||
|
;; Create source element
|
||||||
|
(let ((source (ps:chain document (create-element "source"))))
|
||||||
|
(setf (ps:@ source id) "audio-source")
|
||||||
|
(setf (ps:@ source src) (ps:@ config url))
|
||||||
|
(setf (ps:@ source type) (ps:@ config type))
|
||||||
|
(ps:chain new-audio (append-child source)))
|
||||||
|
|
||||||
|
;; Add to container
|
||||||
|
(ps:chain container (append-child new-audio))
|
||||||
|
|
||||||
|
;; Re-attach event listeners
|
||||||
|
(attach-audio-event-listeners new-audio)
|
||||||
|
|
||||||
|
(ps:chain console (log "Audio element recreated"))
|
||||||
|
new-audio))))
|
||||||
|
|
||||||
|
;; Main reconnect function
|
||||||
|
(defun reconnect-stream ()
|
||||||
|
"Reconnect the stream - called by user or automatically"
|
||||||
|
(when *is-reconnecting*
|
||||||
|
(return))
|
||||||
|
|
||||||
|
(setf *is-reconnecting* t)
|
||||||
|
(show-stream-status "🔄 Reconnecting to stream..." "info")
|
||||||
|
(hide-reconnect-button)
|
||||||
|
|
||||||
|
;; Clear any pending reconnect timeout
|
||||||
|
(when *reconnect-timeout*
|
||||||
|
(clear-timeout *reconnect-timeout*)
|
||||||
|
(setf *reconnect-timeout* nil))
|
||||||
|
|
||||||
|
(let ((audio-element (ps:chain document (get-element-by-id "live-audio"))))
|
||||||
|
(if audio-element
|
||||||
|
;; Try simple reload first
|
||||||
|
(progn
|
||||||
|
(ps:chain audio-element (pause))
|
||||||
|
(ps:chain audio-element (load))
|
||||||
|
|
||||||
|
;; Resume AudioContext if suspended
|
||||||
|
(when (ps:@ window |resetSpectrumAnalyzer|)
|
||||||
|
(ps:chain window (reset-spectrum-analyzer)))
|
||||||
|
|
||||||
|
;; Try to play after a short delay
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain audio-element (play)
|
||||||
|
(then (lambda ()
|
||||||
|
(setf *stream-error-count* 0)
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(show-stream-status "✓ Stream reconnected!" "success")
|
||||||
|
(set-timeout hide-stream-status 3000)
|
||||||
|
|
||||||
|
;; Reinitialize spectrum analyzer
|
||||||
|
(when (ps:@ window |initSpectrumAnalyzer|)
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain window (init-spectrum-analyzer)))
|
||||||
|
500))))
|
||||||
|
(catch (lambda (err)
|
||||||
|
(ps:chain console (log "Simple reconnect failed, recreating audio element:" err))
|
||||||
|
;; Simple reload failed, recreate the audio element
|
||||||
|
(let ((new-audio (recreate-audio-element)))
|
||||||
|
(when new-audio
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain new-audio (play)
|
||||||
|
(then (lambda ()
|
||||||
|
(setf *stream-error-count* 0)
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(show-stream-status "✓ Stream reconnected!" "success")
|
||||||
|
(set-timeout hide-stream-status 3000)))
|
||||||
|
(catch (lambda (err2)
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(incf *stream-error-count*)
|
||||||
|
(show-stream-status "❌ Could not reconnect. Click play to try again." "error")
|
||||||
|
(show-reconnect-button)
|
||||||
|
(ps:chain console (log "Reconnect failed:" err2))))))
|
||||||
|
500)))))))
|
||||||
|
500))
|
||||||
|
|
||||||
|
;; No audio element found, try to recreate
|
||||||
|
(let ((new-audio (recreate-audio-element)))
|
||||||
|
(if new-audio
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain new-audio (play)
|
||||||
|
(then (lambda ()
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(show-stream-status "✓ Stream connected!" "success")
|
||||||
|
(set-timeout hide-stream-status 3000)))
|
||||||
|
(catch (lambda (err)
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(show-stream-status "❌ Could not connect. Click play to try again." "error")
|
||||||
|
(show-reconnect-button)))))
|
||||||
|
500)
|
||||||
|
(progn
|
||||||
|
(setf *is-reconnecting* false)
|
||||||
|
(show-stream-status "❌ Could not create audio player. Please reload the page." "error")))))))
|
||||||
|
|
||||||
|
;; Attach event listeners to audio element
|
||||||
|
(defun attach-audio-event-listeners (audio-element)
|
||||||
|
"Attach all necessary event listeners to an audio element"
|
||||||
|
|
||||||
|
;; Error handler
|
||||||
|
(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))))))
|
||||||
|
|
||||||
|
;; 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")
|
||||||
|
(setf *reconnect-timeout*
|
||||||
|
(set-timeout
|
||||||
|
(lambda ()
|
||||||
|
;; Only reconnect if still stalled
|
||||||
|
(when (ps:@ audio-element paused)
|
||||||
|
(reconnect-stream)))
|
||||||
|
5000)))))
|
||||||
|
|
||||||
|
;; Waiting handler (buffering)
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "waiting"
|
||||||
|
(lambda ()
|
||||||
|
(ps:chain console (log "Stream buffering..."))
|
||||||
|
(show-stream-status "⏳ Buffering..." "info"))))
|
||||||
|
|
||||||
|
;; Playing handler - clear any error states
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "playing"
|
||||||
|
(lambda ()
|
||||||
|
(setf *stream-error-count* 0)
|
||||||
|
(hide-stream-status)
|
||||||
|
(hide-reconnect-button)
|
||||||
|
(when *reconnect-timeout*
|
||||||
|
(clear-timeout *reconnect-timeout*)
|
||||||
|
(setf *reconnect-timeout* nil)))))
|
||||||
|
|
||||||
|
;; Pause handler - track when paused for long pause detection
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "pause"
|
||||||
|
(lambda ()
|
||||||
|
(setf *last-play-attempt* (ps:chain |Date| (now))))))
|
||||||
|
|
||||||
|
;; Play handler - detect long pauses that need reconnection
|
||||||
|
(ps:chain audio-element
|
||||||
|
(add-event-listener "play"
|
||||||
|
(lambda ()
|
||||||
|
(let ((pause-duration (- (ps:chain |Date| (now)) *last-play-attempt*)))
|
||||||
|
;; If paused for more than 30 seconds, reconnect to get fresh stream
|
||||||
|
(when (> pause-duration 30000)
|
||||||
|
(ps:chain console (log "Long pause detected, reconnecting for fresh stream..."))
|
||||||
|
(reconnect-stream))))))
|
||||||
|
|
||||||
|
;; Spectrum analyzer hooks
|
||||||
|
(when (ps:@ window |initSpectrumAnalyzer|)
|
||||||
|
(ps:chain audio-element (add-event-listener "play"
|
||||||
|
(lambda () (ps:chain window (init-spectrum-analyzer))))))
|
||||||
|
|
||||||
|
(when (ps:@ window |stopSpectrumAnalyzer|)
|
||||||
|
(ps:chain audio-element (add-event-listener "pause"
|
||||||
|
(lambda () (ps:chain window (stop-spectrum-analyzer)))))))
|
||||||
|
|
||||||
(defun redirect-when-frame ()
|
(defun redirect-when-frame ()
|
||||||
(let* ((path (ps:@ window location pathname))
|
(let* ((path (ps:@ window location pathname))
|
||||||
(is-frameset-page (not (= (ps:@ window parent) (ps:@ window self))))
|
(is-frameset-page (not (= (ps:@ window parent) (ps:@ window self))))
|
||||||
|
|
@ -164,80 +413,10 @@
|
||||||
;; Update now playing
|
;; Update now playing
|
||||||
(update-now-playing)
|
(update-now-playing)
|
||||||
|
|
||||||
;; Auto-reconnect on stream errors
|
;; Attach event listeners to audio element
|
||||||
(let ((audio-element (ps:chain document (get-element-by-id "live-audio"))))
|
(let ((audio-element (ps:chain document (get-element-by-id "live-audio"))))
|
||||||
(when audio-element
|
(when audio-element
|
||||||
(ps:chain audio-element
|
(attach-audio-event-listeners audio-element)))
|
||||||
(add-event-listener
|
|
||||||
"error"
|
|
||||||
(lambda (err)
|
|
||||||
(ps:chain console (log "Stream error, attempting reconnect in 3 seconds..." err))
|
|
||||||
(set-timeout
|
|
||||||
(lambda ()
|
|
||||||
(ps:chain audio-element (load))
|
|
||||||
(ps:chain (ps:chain audio-element (play))
|
|
||||||
(catch (lambda (err)
|
|
||||||
(ps:chain console (log "Reconnect failed:" err))))))
|
|
||||||
3000))))
|
|
||||||
|
|
||||||
(ps:chain audio-element
|
|
||||||
(add-event-listener
|
|
||||||
"stalled"
|
|
||||||
(lambda ()
|
|
||||||
(ps:chain console (log "Stream stalled, reloading..."))
|
|
||||||
(ps:chain audio-element (load))
|
|
||||||
(ps:chain (ps:chain audio-element (play))
|
|
||||||
(catch (lambda (err)
|
|
||||||
(ps:chain console (log "Reload failed:" err))))))))
|
|
||||||
|
|
||||||
(let ((pause-timestamp nil)
|
|
||||||
(is-reconnecting false)
|
|
||||||
(needs-reconnect false)
|
|
||||||
(pause-reconnect-threshold 10000))
|
|
||||||
|
|
||||||
(ps:chain audio-element
|
|
||||||
(add-event-listener "pause"
|
|
||||||
(lambda ()
|
|
||||||
(setf pause-timestamp (ps:chain |Date| (now)))
|
|
||||||
(ps:chain console (log "Stream paused at:" pause-timestamp)))))
|
|
||||||
|
|
||||||
(ps:chain audio-element
|
|
||||||
(add-event-listener "play"
|
|
||||||
(lambda ()
|
|
||||||
(when (and (not is-reconnecting)
|
|
||||||
pause-timestamp
|
|
||||||
(> (- (ps:chain |Date| (now)) pause-timestamp) pause-reconnect-threshold))
|
|
||||||
(setf needs-reconnect true)
|
|
||||||
(ps:chain console (log "Long pause detected, will reconnect when playing starts...")))
|
|
||||||
(setf pause-timestamp nil))))
|
|
||||||
|
|
||||||
(ps:chain audio-element
|
|
||||||
(add-event-listener "playing"
|
|
||||||
(lambda ()
|
|
||||||
(when (and needs-reconnect (not is-reconnecting))
|
|
||||||
(setf is-reconnecting true)
|
|
||||||
(setf needs-reconnect false)
|
|
||||||
(ps:chain console (log "Reconnecting stream after long pause to clear stale buffers..."))
|
|
||||||
|
|
||||||
(ps:chain audio-element (pause))
|
|
||||||
|
|
||||||
(when (ps:@ window |resetSpectrumAnalyzer|)
|
|
||||||
(ps:chain window (reset-spectrum-analyzer)))
|
|
||||||
|
|
||||||
(ps:chain audio-element (load))
|
|
||||||
|
|
||||||
(set-timeout
|
|
||||||
(lambda ()
|
|
||||||
(ps:chain audio-element (play)
|
|
||||||
(catch (lambda (err)
|
|
||||||
(ps:chain console (log "Reconnect play failed:" err)))))
|
|
||||||
|
|
||||||
(when (ps:@ window |initSpectrumAnalyzer|)
|
|
||||||
(ps:chain window (init-spectrum-analyzer))
|
|
||||||
(ps:chain console (log "Spectrum analyzer reinitialized after reconnect")))
|
|
||||||
|
|
||||||
(setf is-reconnecting false))
|
|
||||||
200))))))))
|
|
||||||
|
|
||||||
;; Check frameset preference
|
;; Check frameset preference
|
||||||
(let ((path (ps:@ window location pathname))
|
(let ((path (ps:@ window location pathname))
|
||||||
|
|
@ -249,8 +428,8 @@
|
||||||
|
|
||||||
(redirect-when-frame)))))
|
(redirect-when-frame)))))
|
||||||
|
|
||||||
;; Update now playing every 10 seconds
|
;; Update now playing every 5 seconds
|
||||||
(set-interval update-now-playing 10000)
|
(set-interval update-now-playing 5000)
|
||||||
|
|
||||||
;; Listen for messages from popout window
|
;; Listen for messages from popout window
|
||||||
(ps:chain window
|
(ps:chain window
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,16 @@
|
||||||
(when *animation-id*
|
(when *animation-id*
|
||||||
(cancel-animation-frame *animation-id*)
|
(cancel-animation-frame *animation-id*)
|
||||||
(setf *animation-id* nil))
|
(setf *animation-id* nil))
|
||||||
|
;; Close the old AudioContext if it exists
|
||||||
|
(when *audio-context*
|
||||||
|
(ps:try
|
||||||
|
(ps:chain *audio-context* (close))
|
||||||
|
(:catch (e)
|
||||||
|
(ps:chain console (log "Error closing AudioContext:" e)))))
|
||||||
(setf *audio-context* nil)
|
(setf *audio-context* nil)
|
||||||
(setf *analyser* nil)
|
(setf *analyser* nil)
|
||||||
(setf *media-source* nil)
|
(setf *media-source* nil)
|
||||||
|
(setf *current-audio-element* nil)
|
||||||
(ps:chain console (log "Spectrum analyzer reset for reconnection")))
|
(ps:chain console (log "Spectrum analyzer reset for reconnection")))
|
||||||
|
|
||||||
(defun init-spectrum-analyzer ()
|
(defun init-spectrum-analyzer ()
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1041,6 +1041,20 @@
|
||||||
:flex 1
|
:flex 1
|
||||||
:min-width "300px")
|
:min-width "300px")
|
||||||
|
|
||||||
|
(.persistent-reconnect-btn
|
||||||
|
:background transparent
|
||||||
|
:color "#00ff00"
|
||||||
|
:border "1px solid #00ff00"
|
||||||
|
:padding "5px 10px"
|
||||||
|
:cursor "pointer"
|
||||||
|
:font-family #(main-font)
|
||||||
|
:font-size "0.85em"
|
||||||
|
:white-space nowrap
|
||||||
|
:margin-right "5px")
|
||||||
|
|
||||||
|
((:and .persistent-reconnect-btn :hover)
|
||||||
|
:background "#2a3441")
|
||||||
|
|
||||||
(.persistent-disable-btn
|
(.persistent-disable-btn
|
||||||
:background transparent
|
:background transparent
|
||||||
:color "#00ff00"
|
:color "#00ff00"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>About - Asteroid Radio</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
<span>ABOUT ASTEROID RADIO</span>
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
</h1>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/asteroid/content" target="_self">Home</a>
|
||||||
|
<a href="/asteroid/player-content" target="_self">Player</a>
|
||||||
|
<a href="/asteroid/about-content" target="_self">About</a>
|
||||||
|
<a href="/asteroid/status" target="_self">Status</a>
|
||||||
|
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎵 Asteroid Music for Hackers</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Asteroid Radio is a community-driven internet radio station born from the SystemCrafters community.
|
||||||
|
We celebrate the intersection of music, technology, and hacker culture—broadcasting for those who
|
||||||
|
appreciate both great code and great music.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We met through a shared set of technical biases and a love for building systems from first principles.
|
||||||
|
Asteroid Radio embodies that ethos: <strong>music for hackers, built by hackers</strong>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🛠️ Built with Common Lisp</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
This entire platform is built using <strong>Common Lisp</strong>, demonstrating the power and elegance
|
||||||
|
of Lisp for modern web applications. We use:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height: 1.8; margin-left: 20px;">
|
||||||
|
<li><strong><a href="https://codeberg.org/shirakumo/radiance" style="color: #00ff00;">Radiance</a></strong> - Web application framework</li>
|
||||||
|
<li><strong><a href="https://codeberg.org/shinmera/clip" style="color: #00ff00;">Clip</a></strong> - HTML5-compliant template engine</li>
|
||||||
|
<li><strong><a href="https://codeberg.org/shinmera/LASS" style="color: #00ff00;">LASS</a></strong> - Lisp Augmented Style Sheets</li>
|
||||||
|
<li><strong><a href="https://gitlab.common-lisp.net/parenscript/parenscript" style="color: #00ff00;">ParenScript</a></strong> - Lisp-to-JavaScript compiler</li>
|
||||||
|
<li><strong><a href="https://icecast.org/" style="color: #00ff00;">Icecast</a></strong> - Streaming media server</li>
|
||||||
|
<li><strong><a href="https://www.liquidsoap.info/" style="color: #00ff00;">Liquidsoap</a></strong> - Audio stream generation</li>
|
||||||
|
</ul>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
By building in Common Lisp, we're doubling down on our technical values and creating features
|
||||||
|
for "our people"—those who appreciate the elegance of Lisp and the power of understanding your tools deeply.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">📖 Open Source & AGPL Licensed</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Asteroid Radio is <strong>free and open source software</strong>, licensed under the
|
||||||
|
<strong><a href="https://www.gnu.org/licenses/agpl-3.0.en.html" style="color: #00ff00;">GNU Affero General Public License v3.0 (AGPL)</a></strong>.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
The source code is available at:
|
||||||
|
<a href="https://github.com/Fade/asteroid" style="color: #00ff00; font-weight: bold;">https://github.com/Fade/asteroid</a>
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We believe in transparency, collaboration, and the freedom to study, modify, and share the software we use.
|
||||||
|
The AGPL ensures that improvements to Asteroid Radio remain free and available to everyone.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎧 Features</h2>
|
||||||
|
<ul style="line-height: 1.8; margin-left: 20px;">
|
||||||
|
<li><strong>Live Streaming</strong> - Multiple quality options (AAC, MP3)</li>
|
||||||
|
<li><strong>Persistent Player</strong> - Frameset mode for uninterrupted playback while browsing</li>
|
||||||
|
<li><strong>Spectrum Analyzer</strong> - Real-time audio visualization with customizable themes</li>
|
||||||
|
<li><strong>Track Library</strong> - Browse and search the music collection</li>
|
||||||
|
<li><strong>User Profiles</strong> - Track your listening history</li>
|
||||||
|
<li><strong>Admin Tools</strong> - Manage tracks, users, and playlists</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🤝 Community</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We're part of the <strong><a href="https://systemcrafters.net/" style="color: #00ff00;">SystemCrafters</a></strong>
|
||||||
|
community—a group of developers, hackers, and enthusiasts who believe in building systems from first principles,
|
||||||
|
understanding our tools deeply, and sharing knowledge freely.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Join us in celebrating the intersection of great music and great code!
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>About - Asteroid Radio</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/asteroid/static/favicon.ico">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/asteroid/static/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<h1 style="display: flex; align-items: center; justify-content: center; gap: 15px;">
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
<span>ABOUT ASTEROID RADIO</span>
|
||||||
|
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
|
||||||
|
</h1>
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/asteroid/">Home</a>
|
||||||
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
|
<a href="/asteroid/status">Status</a>
|
||||||
|
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||||
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
|
||||||
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎵 Asteroid Music for Hackers</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Asteroid Radio is a community-driven internet radio station born from the SystemCrafters community.
|
||||||
|
We celebrate the intersection of music, technology, and hacker culture—broadcasting for those who
|
||||||
|
appreciate both great code and great music.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We met through a shared set of technical biases and a love for building systems from first principles.
|
||||||
|
Asteroid Radio embodies that ethos: <strong>music for hackers, built by hackers</strong>.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🛠️ Built with Common Lisp</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
This entire platform is built using <strong>Common Lisp</strong>, demonstrating the power and elegance
|
||||||
|
of Lisp for modern web applications. We use:
|
||||||
|
</p>
|
||||||
|
<ul style="line-height: 1.8; margin-left: 20px;">
|
||||||
|
<li><strong><a href="https://codeberg.org/shirakumo/radiance" style="color: #00ff00;">Radiance</a></strong> - Web application framework</li>
|
||||||
|
<li><strong><a href="https://codeberg.org/shinmera/clip" style="color: #00ff00;">Clip</a></strong> - HTML5-compliant template engine</li>
|
||||||
|
<li><strong><a href="https://codeberg.org/shinmera/LASS" style="color: #00ff00;">LASS</a></strong> - Lisp Augmented Style Sheets</li>
|
||||||
|
<li><strong><a href="https://gitlab.common-lisp.net/parenscript/parenscript" style="color: #00ff00;">ParenScript</a></strong> - Lisp-to-JavaScript compiler</li>
|
||||||
|
<li><strong><a href="https://icecast.org/" style="color: #00ff00;">Icecast</a></strong> - Streaming media server</li>
|
||||||
|
<li><strong><a href="https://www.liquidsoap.info/" style="color: #00ff00;">Liquidsoap</a></strong> - Audio stream generation</li>
|
||||||
|
</ul>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
By building in Common Lisp, we're doubling down on our technical values and creating features
|
||||||
|
for "our people"—those who appreciate the elegance of Lisp and the power of understanding your tools deeply.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">📖 Open Source & AGPL Licensed</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Asteroid Radio is <strong>free and open source software</strong>, licensed under the
|
||||||
|
<strong><a href="https://www.gnu.org/licenses/agpl-3.0.en.html" style="color: #00ff00;">GNU Affero General Public License v3.0 (AGPL)</a></strong>.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
The source code is available at:
|
||||||
|
<a href="https://github.com/Fade/asteroid" style="color: #00ff00; font-weight: bold;">https://github.com/Fade/asteroid</a>
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We believe in transparency, collaboration, and the freedom to study, modify, and share the software we use.
|
||||||
|
The AGPL ensures that improvements to Asteroid Radio remain free and available to everyone.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🎧 Features</h2>
|
||||||
|
<ul style="line-height: 1.8; margin-left: 20px;">
|
||||||
|
<li><strong>Live Streaming</strong> - Multiple quality options (AAC, MP3)</li>
|
||||||
|
<li><strong>Persistent Player</strong> - Frameset mode for uninterrupted playback while browsing</li>
|
||||||
|
<li><strong>Spectrum Analyzer</strong> - Real-time audio visualization with customizable themes</li>
|
||||||
|
<li><strong>Track Library</strong> - Browse and search the music collection</li>
|
||||||
|
<li><strong>User Profiles</strong> - Track your listening history</li>
|
||||||
|
<li><strong>Admin Tools</strong> - Manage tracks, users, and playlists</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom: 30px;">
|
||||||
|
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;">🤝 Community</h2>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
We're part of the <strong><a href="https://systemcrafters.net/" style="color: #00ff00;">SystemCrafters</a></strong>
|
||||||
|
community—a group of developers, hackers, and enthusiasts who believe in building systems from first principles,
|
||||||
|
understanding our tools deeply, and sharing knowledge freely.
|
||||||
|
</p>
|
||||||
|
<p style="line-height: 1.6;">
|
||||||
|
Join us in celebrating the intersection of great music and great code!
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -28,10 +28,17 @@
|
||||||
|
|
||||||
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
<span class="now-playing-mini" id="mini-now-playing">Loading...</span>
|
||||||
|
|
||||||
|
<button id="reconnect-btn" onclick="reconnectStream()" class="persistent-reconnect-btn" title="Reconnect if audio stops working">
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
|
||||||
<button onclick="disableFramesetMode()" class="persistent-disable-btn">
|
<button onclick="disableFramesetMode()" class="persistent-disable-btn">
|
||||||
✕ Disable
|
✕ Disable
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Status indicator for connection issues -->
|
||||||
|
<div id="stream-status" style="display: none; background: #550000; color: #ff6666; padding: 4px 10px; text-align: center; font-size: 0.85em;"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Configure audio element for better streaming
|
// Configure audio element for better streaming
|
||||||
|
|
@ -134,6 +141,145 @@
|
||||||
// Redirect parent window to regular view
|
// Redirect parent window to regular view
|
||||||
window.parent.location.href = '/asteroid/';
|
window.parent.location.href = '/asteroid/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show status message
|
||||||
|
function showStatus(message, isError) {
|
||||||
|
const status = document.getElementById('stream-status');
|
||||||
|
if (status) {
|
||||||
|
status.textContent = message;
|
||||||
|
status.style.display = 'block';
|
||||||
|
status.style.background = isError ? '#550000' : '#005500';
|
||||||
|
status.style.color = isError ? '#ff6666' : '#66ff66';
|
||||||
|
if (!isError) {
|
||||||
|
setTimeout(() => { status.style.display = 'none'; }, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatus() {
|
||||||
|
const status = document.getElementById('stream-status');
|
||||||
|
if (status) {
|
||||||
|
status.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconnect stream - recreates audio element to fix wedged state
|
||||||
|
function reconnectStream() {
|
||||||
|
console.log('Reconnecting stream...');
|
||||||
|
showStatus('🔄 Reconnecting...', false);
|
||||||
|
|
||||||
|
const container = document.querySelector('.persistent-player');
|
||||||
|
const oldAudio = document.getElementById('persistent-audio');
|
||||||
|
const streamBaseUrl = document.getElementById('stream-base-url').value;
|
||||||
|
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
|
||||||
|
const config = getStreamConfig(streamBaseUrl, streamQuality);
|
||||||
|
|
||||||
|
if (!container || !oldAudio) {
|
||||||
|
showStatus('❌ Could not reconnect - reload page', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current volume and muted state
|
||||||
|
const savedVolume = oldAudio.volume;
|
||||||
|
const savedMuted = oldAudio.muted;
|
||||||
|
console.log('Saving volume:', savedVolume, 'muted:', savedMuted);
|
||||||
|
|
||||||
|
// Reset spectrum analyzer if it exists
|
||||||
|
if (window.resetSpectrumAnalyzer) {
|
||||||
|
window.resetSpectrumAnalyzer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop and remove old audio
|
||||||
|
oldAudio.pause();
|
||||||
|
oldAudio.src = '';
|
||||||
|
oldAudio.load();
|
||||||
|
|
||||||
|
// Create new audio element
|
||||||
|
const newAudio = document.createElement('audio');
|
||||||
|
newAudio.id = 'persistent-audio';
|
||||||
|
newAudio.controls = true;
|
||||||
|
newAudio.preload = 'metadata';
|
||||||
|
newAudio.crossOrigin = 'anonymous';
|
||||||
|
|
||||||
|
// Restore volume and muted state
|
||||||
|
newAudio.volume = savedVolume;
|
||||||
|
newAudio.muted = savedMuted;
|
||||||
|
|
||||||
|
// Create source
|
||||||
|
const source = document.createElement('source');
|
||||||
|
source.id = 'audio-source';
|
||||||
|
source.src = config.url;
|
||||||
|
source.type = config.type;
|
||||||
|
newAudio.appendChild(source);
|
||||||
|
|
||||||
|
// Replace old audio with new
|
||||||
|
oldAudio.replaceWith(newAudio);
|
||||||
|
|
||||||
|
// Re-attach event listeners
|
||||||
|
attachAudioListeners(newAudio);
|
||||||
|
|
||||||
|
// Try to play
|
||||||
|
setTimeout(() => {
|
||||||
|
newAudio.play()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Reconnected successfully');
|
||||||
|
showStatus('✓ Reconnected!', false);
|
||||||
|
// Reinitialize spectrum analyzer - try in this frame first
|
||||||
|
if (window.initSpectrumAnalyzer) {
|
||||||
|
setTimeout(() => window.initSpectrumAnalyzer(), 500);
|
||||||
|
}
|
||||||
|
// Also try in content frame (where spectrum canvas usually is)
|
||||||
|
try {
|
||||||
|
const contentFrame = window.parent.frames['content-frame'];
|
||||||
|
if (contentFrame && contentFrame.initSpectrumAnalyzer) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (contentFrame.resetSpectrumAnalyzer) {
|
||||||
|
contentFrame.resetSpectrumAnalyzer();
|
||||||
|
}
|
||||||
|
contentFrame.initSpectrumAnalyzer();
|
||||||
|
console.log('Spectrum analyzer reinitialized in content frame');
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Could not reinit spectrum in content frame:', e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log('Reconnect play failed:', err);
|
||||||
|
showStatus('Click play to start stream', false);
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach event listeners to audio element
|
||||||
|
function attachAudioListeners(audioElement) {
|
||||||
|
audioElement.addEventListener('waiting', function() {
|
||||||
|
console.log('Audio buffering...');
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('playing', function() {
|
||||||
|
console.log('Audio playing');
|
||||||
|
hideStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('error', function(e) {
|
||||||
|
console.error('Audio error:', e);
|
||||||
|
showStatus('⚠️ Stream error - click 🔄 to reconnect', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('stalled', function() {
|
||||||
|
console.log('Audio stalled');
|
||||||
|
showStatus('⚠️ Stream stalled - click 🔄 if no audio', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach listeners to initial audio element
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const audioElement = document.getElementById('persistent-audio');
|
||||||
|
if (audioElement) {
|
||||||
|
attachAudioListeners(audioElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/asteroid/content" target="content-frame">Home</a>
|
<a href="/asteroid/content" target="_self">Home</a>
|
||||||
<a href="/asteroid/player-content" target="content-frame">Player</a>
|
<a href="/asteroid/player-content" target="_self">Player</a>
|
||||||
<a href="/asteroid/status" target="content-frame">Status</a>
|
<a href="/asteroid/about-content" target="_self">About</a>
|
||||||
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
|
<a href="/asteroid/status" target="_self">Status</a>
|
||||||
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
|
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
|
||||||
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
|
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
|
||||||
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
|
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
|
||||||
|
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
|
||||||
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/asteroid/">Home</a>
|
<a href="/asteroid/">Home</a>
|
||||||
<a href="/asteroid/player">Player</a>
|
<a href="/asteroid/player">Player</a>
|
||||||
|
<a href="/asteroid/about">About</a>
|
||||||
<a href="/asteroid/status">Status</a>
|
<a href="/asteroid/status">Status</a>
|
||||||
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
|
||||||
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
|
||||||
|
|
@ -84,6 +85,9 @@
|
||||||
<p><strong class="live-stream-label" class="live-stream-label">BROADCASTING:</strong> <span id="stream-status" style="">Asteroid music for Hackers</span></p>
|
<p><strong class="live-stream-label" class="live-stream-label">BROADCASTING:</strong> <span id="stream-status" style="">Asteroid music for Hackers</span></p>
|
||||||
|
|
||||||
<div style="display: flex; gap: 10px; justify-content: end; margin-bottom: 20px;">
|
<div style="display: flex; gap: 10px; justify-content: end; margin-bottom: 20px;">
|
||||||
|
<button id="reconnect-btn" class="btn btn-warning" onclick="reconnectStream()" style="font-size: 0.9em; display: none;">
|
||||||
|
🔄 Reconnect Stream
|
||||||
|
</button>
|
||||||
<button id="popout-btn" class="btn btn-info" onclick="openPopoutPlayer()" style="font-size: 0.9em;">
|
<button id="popout-btn" class="btn btn-info" onclick="openPopoutPlayer()" style="font-size: 0.9em;">
|
||||||
🗗 Pop Out Player
|
🗗 Pop Out Player
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -92,10 +96,15 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio id="live-audio" controls crossorigin="anonymous" style="width: 100%; margin: 10px 0;">
|
<!-- Stream connection status -->
|
||||||
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
<div id="stream-status-indicator" style="display: none; padding: 8px; margin-bottom: 10px; border-radius: 4px; text-align: center;"></div>
|
||||||
Your browser does not support the audio element.
|
|
||||||
</audio>
|
<div id="audio-container">
|
||||||
|
<audio id="live-audio" controls crossorigin="anonymous" style="width: 100%; margin: 10px 0;">
|
||||||
|
<source id="audio-source" lquery="(attr :src default-stream-url :type default-stream-encoding)">
|
||||||
|
Your browser does not support the audio element.
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="now-playing" class="now-playing"></div>
|
<div id="now-playing" class="now-playing"></div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
|
<script src="/asteroid/static/js/front-page.js"></script>
|
||||||
<script src="/asteroid/static/js/player.js"></script>
|
<script src="/asteroid/static/js/player.js"></script>
|
||||||
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
<script src="/api/asteroid/spectrum-analyzer.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/asteroid/static/favicon-16x16.png">
|
||||||
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
<script src="/asteroid/static/js/auth-ui.js"></script>
|
<script src="/asteroid/static/js/auth-ui.js"></script>
|
||||||
|
<script src="/asteroid/static/js/front-page.js"></script>
|
||||||
<script src="/asteroid/static/js/player.js"></script>
|
<script src="/asteroid/static/js/player.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue