Compare commits
3 Commits
924f6498de
...
4ec90c0f27
| Author | SHA1 | Date |
|---|---|---|
|
|
4ec90c0f27 | |
|
|
7c7b2c921e | |
|
|
6e8260172f |
16
TODO.org
16
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]
|
* Setup asteroid.radio server at Hetzner [7/7]
|
||||||
- [X] Provision a VPS
|
- [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
|
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
|
2) [X] icecast is also binding the external interface on b612, which it
|
||||||
should not be. HAproxy is there to mediate this flow.
|
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,
|
4) [X] The templates still advertise the default administrator password,
|
||||||
which is no bueno.
|
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.
|
integrate it into HAproxy.
|
||||||
|
|
||||||
6) [ ] The administrative interface should be beefed up.
|
6) [ ] The administrative interface should be beefed up.
|
||||||
6.1) [ ] Deactivate users
|
6.1) [X] Deactivate users
|
||||||
6.2) [ ] Change user access permissions
|
6.2) [X] Change user access permissions
|
||||||
6.3) [ ] Listener statistics, breakdown by day/hour, new users, % changed &c
|
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.
|
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.
|
8) [ ] User profile pages should probably be fleshed out.
|
||||||
9) [ ] the stream management features aren't there for Admins or DJs.
|
9) [ ] the stream management features aren't there for Admins or DJs.
|
||||||
10) [ ] The "Scan Library" feature is not working in the main branch
|
10) [X] 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.
|
11) [X] 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.
|
12) [X] ensure each info field 'Listeners: ..' &c has only one instance per page.
|
||||||
|
|
||||||
* Server runtime configuration [0/1]
|
* Server runtime configuration [0/1]
|
||||||
- [ ] parameterize all configuration for runtime loading [0/2]
|
- [ ] parameterize all configuration for runtime loading [0/2]
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,8 @@
|
||||||
(:file "conditions")
|
(:file "conditions")
|
||||||
(:file "database")
|
(:file "database")
|
||||||
(:file "template-utils")
|
(:file "template-utils")
|
||||||
(:file "spectrum-analyzer")
|
(:module :parenscript
|
||||||
|
:components ((:file "spectrum-analyzer")))
|
||||||
(:file "stream-media")
|
(:file "stream-media")
|
||||||
(:file "user-management")
|
(:file "user-management")
|
||||||
(:file "playlist-management")
|
(:file "playlist-management")
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@
|
||||||
<header-timeout>15</header-timeout>
|
<header-timeout>15</header-timeout>
|
||||||
<source-timeout>10</source-timeout>
|
<source-timeout>10</source-timeout>
|
||||||
<burst-on-connect>1</burst-on-connect>
|
<burst-on-connect>1</burst-on-connect>
|
||||||
<burst-size>65535</burst-size>
|
<!-- Reduced from 65535 to minimize buffer accumulation during pause -->
|
||||||
|
<burst-size>8192</burst-size>
|
||||||
</limits>
|
</limits>
|
||||||
|
|
||||||
<authentication>
|
<authentication>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - API Endpoints Reference
|
#+TITLE: Asteroid Radio - API Endpoints Reference
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - API Reference
|
#+TITLE: Asteroid Radio - API Reference
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Current Interfaces
|
* Current Interfaces
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - Development Guide
|
#+TITLE: Asteroid Radio - Development Guide
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Development Setup
|
* Development Setup
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - Docker Streaming Setup
|
#+TITLE: Asteroid Radio - Docker Streaming Setup
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Docker Streaming Overview
|
* Docker Streaming Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - Installation Guide
|
#+TITLE: Asteroid Radio - Installation Guide
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Installation Overview
|
* Installation Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Playlist System - Complete (MVP)
|
#+TITLE: Playlist System - Complete (MVP)
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: PostgreSQL Setup for Asteroid Radio
|
#+TITLE: PostgreSQL Setup for Asteroid Radio
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - Project Development History
|
#+TITLE: Asteroid Radio - Project Development History
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
#+DESCRIPTION: Comprehensive history of the Asteroid Radio project from inception to present
|
#+DESCRIPTION: Comprehensive history of the Asteroid Radio project from inception to present
|
||||||
|
|
||||||
* Project Overview
|
* Project Overview
|
||||||
|
|
@ -11,7 +11,8 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
- *Backend*: Common Lisp (SBCL), Radiance web framework
|
- *Backend*: Common Lisp (SBCL), Radiance web framework
|
||||||
- *Streaming*: Icecast2, Liquidsoap
|
- *Streaming*: Icecast2, Liquidsoap
|
||||||
- *Database*: PostgreSQL (configured, ready for migration)
|
- *Database*: PostgreSQL (configured, ready for migration)
|
||||||
- *Frontend*: HTML5, JavaScript, CLIP templating, LASS (CSS in Lisp)
|
- *Frontend*: HTML5, JavaScript, Parenscript, CLIP templating, LASS (CSS in Lisp)
|
||||||
|
- *Audio Visualization*: Web Audio API, Canvas
|
||||||
- *Infrastructure*: Docker, Docker Compose
|
- *Infrastructure*: Docker, Docker Compose
|
||||||
|
|
||||||
* Project Timeline
|
* Project Timeline
|
||||||
|
|
@ -231,19 +232,43 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
- Synchronized with upstream/main
|
- Synchronized with upstream/main
|
||||||
- Prepared comprehensive documentation PR
|
- Prepared comprehensive documentation PR
|
||||||
|
|
||||||
|
** Phase 9: Visual Audio Features (December 2025)
|
||||||
|
|
||||||
|
*** 2025-12-06: Real-Time Spectrum Analyzer
|
||||||
|
- *Lead*: Brian O'Reilly (Fade), Glenn Thompson
|
||||||
|
- Implemented spectrum analyzer using Parenscript
|
||||||
|
- Web Audio API integration for real-time visualization
|
||||||
|
- Dynamic JavaScript generation via API endpoint
|
||||||
|
- Canvas-based frequency display
|
||||||
|
- Works across all player modes (inline, pop-out, frameset)
|
||||||
|
- Lisp-to-JavaScript compilation for maintainability
|
||||||
|
|
||||||
* Development Statistics
|
* Development Statistics
|
||||||
|
|
||||||
** Contributors (by commit count)
|
** Contributors (by commit count)
|
||||||
1. Glenn Thompson (glenneth/Glenneth) - 135+ commits
|
1. Glenn Thompson (glenneth/Glenneth) - 236 commits
|
||||||
2. Brian O'Reilly (Fade) - 55+ commits
|
2. Brian O'Reilly (Fade) - 109 commits
|
||||||
3. Luis Pereira (easilok) - 23+ commits
|
3. Luis Pereira (easilok) - 63 commits
|
||||||
|
|
||||||
** Total Commits: 213+ commits
|
** Total Commits: 408 commits
|
||||||
|
|
||||||
|
** Code Statistics
|
||||||
|
- *Total Lines of Code*: ~9,300 lines
|
||||||
|
- *Common Lisp*: 2,753 lines (.lisp, .asd)
|
||||||
|
- *JavaScript*: 2,315 lines (.js)
|
||||||
|
- *Templates*: 1,505 lines (.ctml)
|
||||||
|
- *Other*: 2,720 lines (CSS, Shell, Python, etc.)
|
||||||
|
- *Source Files*: 50 files
|
||||||
|
|
||||||
|
** Release Information
|
||||||
|
- *Current Version*: Development (pre-1.0)
|
||||||
|
- *Tagged Releases*: None (continuous development)
|
||||||
|
- *Deployment Status*: Production-ready
|
||||||
|
|
||||||
** Active Development Period
|
** Active Development Period
|
||||||
- Start: August 12, 2025
|
- Start: August 12, 2025
|
||||||
- Current: November 1, 2025
|
- Current: December 6, 2025
|
||||||
- Duration: ~2.75 months of active development
|
- Duration: ~4 months of active development
|
||||||
|
|
||||||
* Major Features Implemented
|
* Major Features Implemented
|
||||||
|
|
||||||
|
|
@ -266,6 +291,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
- ✅ Multiple quality options (AAC 96k, MP3 128k, MP3 64k)
|
- ✅ Multiple quality options (AAC 96k, MP3 128k, MP3 64k)
|
||||||
- ✅ ReplayGain volume normalization
|
- ✅ ReplayGain volume normalization
|
||||||
- ✅ Live now-playing information
|
- ✅ Live now-playing information
|
||||||
|
- ✅ Real-time spectrum analyzer visualization
|
||||||
- ✅ Icecast integration
|
- ✅ Icecast integration
|
||||||
- ✅ Liquidsoap DJ controls
|
- ✅ Liquidsoap DJ controls
|
||||||
- ✅ Stream queue management
|
- ✅ Stream queue management
|
||||||
|
|
@ -325,7 +351,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
- Parallel music scanning
|
- Parallel music scanning
|
||||||
- Client-side caching
|
- Client-side caching
|
||||||
|
|
||||||
* Current State (November 2025)
|
* Current State (December 2025)
|
||||||
|
|
||||||
** Production Ready Features
|
** Production Ready Features
|
||||||
- Full music streaming platform
|
- Full music streaming platform
|
||||||
|
|
@ -333,6 +359,7 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
- Admin control panel
|
- Admin control panel
|
||||||
- DJ controls
|
- DJ controls
|
||||||
- Multiple player modes
|
- Multiple player modes
|
||||||
|
- Real-time spectrum analyzer
|
||||||
- Complete Docker deployment (streams + application)
|
- Complete Docker deployment (streams + application)
|
||||||
- Multi-environment support with dynamic URLs
|
- Multi-environment support with dynamic URLs
|
||||||
- Comprehensive documentation
|
- Comprehensive documentation
|
||||||
|
|
@ -392,9 +419,9 @@ Asteroid Radio is a web-based internet radio station built with Common Lisp, fea
|
||||||
|
|
||||||
* Conclusion
|
* Conclusion
|
||||||
|
|
||||||
Asteroid Radio has evolved from a simple concept into a full-featured internet radio platform in just 2.75 months of active development. The project demonstrates the power of Common Lisp for web development and the collaborative nature of open-source development.
|
Asteroid Radio has evolved from a simple concept into a full-featured internet radio platform in just 4 months of active development. The project demonstrates the power of Common Lisp for web development and the collaborative nature of open-source development.
|
||||||
|
|
||||||
With complete Docker deployment, comprehensive documentation, and a growing feature set, Asteroid Radio is ready for production use while continuing to evolve with regular improvements, bug fixes, and new features based on user needs and technical requirements.
|
With complete Docker deployment, real-time audio visualization, comprehensive documentation, and a growing feature set, Asteroid Radio is ready for production use while continuing to evolve with regular improvements, bug fixes, and new features based on user needs and technical requirements.
|
||||||
|
|
||||||
** Project Links
|
** Project Links
|
||||||
- Repository: https://github.com/fade/asteroid
|
- Repository: https://github.com/fade/asteroid
|
||||||
|
|
@ -403,4 +430,4 @@ With complete Docker deployment, comprehensive documentation, and a growing feat
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last Updated: 2025-11-01*
|
*Last Updated: 2025-12-06*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio - Project Overview
|
#+TITLE: Asteroid Radio - Project Overview
|
||||||
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
|
#+AUTHOR: Glenn Thompson & Brian O'Reilly (Fade)
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* 🎯 Mission
|
* 🎯 Mission
|
||||||
|
|
||||||
|
|
@ -45,6 +45,8 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
|
||||||
- **HTML5** with semantic templates
|
- **HTML5** with semantic templates
|
||||||
- **CSS3** with dark hacker theme
|
- **CSS3** with dark hacker theme
|
||||||
- **JavaScript** for interactive features
|
- **JavaScript** for interactive features
|
||||||
|
- **Parenscript** - Lisp-to-JavaScript compiler for spectrum analyzer
|
||||||
|
- **Web Audio API** - Real-time audio visualization
|
||||||
- **VT323 Font** for retro terminal aesthetic
|
- **VT323 Font** for retro terminal aesthetic
|
||||||
|
|
||||||
**Streaming:**
|
**Streaming:**
|
||||||
|
|
@ -81,6 +83,7 @@ Asteroid Radio is a modern, web-based music streaming platform designed for hack
|
||||||
- ✅ **Music Library** - Track management with pagination, search, and filtering
|
- ✅ **Music Library** - Track management with pagination, search, and filtering
|
||||||
- ✅ **User Playlists** - Create, manage, and play personal music collections
|
- ✅ **User Playlists** - Create, manage, and play personal music collections
|
||||||
- ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
|
- ✅ **Multiple Player Modes** - Inline, pop-out, and persistent frameset players
|
||||||
|
- ✅ **Real-Time Spectrum Analyzer** - Visual audio frequency display using Web Audio API and Parenscript
|
||||||
- ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
|
- ✅ **Stream Queue Control** - Admin control over broadcast stream queue (M3U-based)
|
||||||
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
|
- ✅ **REST API** - Comprehensive JSON API with 15+ endpoints
|
||||||
- ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
|
- ✅ **Music Streaming** - Multiple quality formats (128k MP3, 96k AAC, 64k MP3)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ Pagination system for efficient browsing of large music libraries.
|
||||||
- **Music Library**: Track management with pagination, search, and filtering
|
- **Music Library**: Track management with pagination, search, and filtering
|
||||||
- **Playlists**: User playlists with creation and playback
|
- **Playlists**: User playlists with creation and playback
|
||||||
- **Multiple Player Modes**: Inline, pop-out, and persistent frameset players
|
- **Multiple Player Modes**: Inline, pop-out, and persistent frameset players
|
||||||
|
- **Real-Time Spectrum Analyzer**: Visual audio frequency display using Web Audio API
|
||||||
- **Stream Queue Control**: Admin control over broadcast stream queue
|
- **Stream Queue Control**: Admin control over broadcast stream queue
|
||||||
- **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers
|
- **Docker Streaming Infrastructure**: Icecast2 + Liquidsoap containers
|
||||||
- **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3
|
- **Three Quality Streams**: 128kbps MP3, 96kbps AAC, 64kbps MP3
|
||||||
|
|
@ -147,5 +148,5 @@ For detailed technical information, see the **[[file:PROJECT-OVERVIEW.org][Proje
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last Updated: 2025-10-26*
|
*Last Updated: 2025-12-06*
|
||||||
*Documentation Version: 3.0*
|
*Documentation Version: 3.1*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Stream Queue Control System
|
#+TITLE: Stream Queue Control System
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Asteroid Radio Testing Guide
|
#+TITLE: Asteroid Radio Testing Guide
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: Track Pagination System - Complete
|
#+TITLE: Track Pagination System - Complete
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#+TITLE: User Management System - Complete
|
#+TITLE: User Management System - Complete
|
||||||
#+AUTHOR: Asteroid Radio Development Team
|
#+AUTHOR: Asteroid Radio Development Team
|
||||||
#+DATE: 2025-10-26
|
#+DATE: 2025-12-06
|
||||||
|
|
||||||
* Overview
|
* Overview
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,18 @@
|
||||||
(defvar *canvas* nil)
|
(defvar *canvas* nil)
|
||||||
(defvar *canvas-ctx* nil)
|
(defvar *canvas-ctx* nil)
|
||||||
(defvar *animation-id* 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 ()
|
(defun init-spectrum-analyzer ()
|
||||||
"Initialize the spectrum analyzer"
|
"Initialize the spectrum analyzer"
|
||||||
|
|
@ -37,27 +49,35 @@
|
||||||
(:catch (e)
|
(:catch (e)
|
||||||
(ps:chain console (log "Cross-frame access error:" e)))))
|
(ps:chain console (log "Cross-frame access error:" e)))))
|
||||||
|
|
||||||
(when (and audio-element canvas-element (not *audio-context*))
|
(when (and audio-element canvas-element)
|
||||||
;; Create Audio Context
|
;; Store current audio element
|
||||||
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
(setf *current-audio-element* audio-element)
|
||||||
(ps:@ window |webkitAudioContext|))))
|
|
||||||
|
|
||||||
;; Create Analyser Node
|
;; Only create audio context and media source once
|
||||||
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
(when (not *audio-context*)
|
||||||
(setf (ps:@ *analyser* |fftSize|) 256)
|
;; Create Audio Context
|
||||||
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
||||||
|
(ps:@ window |webkitAudioContext|))))
|
||||||
;; Connect audio source to analyser
|
|
||||||
(let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
|
;; Create Analyser Node
|
||||||
(ps:chain source (connect *analyser*))
|
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
||||||
(ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
|
(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
|
;; Setup canvas
|
||||||
(setf *canvas* canvas-element)
|
(setf *canvas* canvas-element)
|
||||||
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
|
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
|
||||||
|
|
||||||
;; Start visualization
|
;; Start visualization if not already running
|
||||||
(draw-spectrum))))
|
(when (not *animation-id*)
|
||||||
|
(draw-spectrum)))))
|
||||||
|
|
||||||
(defun draw-spectrum ()
|
(defun draw-spectrum ()
|
||||||
"Draw the spectrum analyzer visualization"
|
"Draw the spectrum analyzer visualization"
|
||||||
|
|
@ -635,6 +635,12 @@ function displayQueueSearchResults(results) {
|
||||||
|
|
||||||
// Live stream info update
|
// Live stream info update
|
||||||
async function updateLiveStreamInfo() {
|
async function updateLiveStreamInfo() {
|
||||||
|
// Don't update if stream is paused
|
||||||
|
const audioElement = document.getElementById('live-stream-audio');
|
||||||
|
if (audioElement && audioElement.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ function changeStreamQuality() {
|
||||||
|
|
||||||
// Update now playing info from Icecast
|
// Update now playing info from Icecast
|
||||||
async function updateNowPlaying() {
|
async function updateNowPlaying() {
|
||||||
|
// Don't update if stream is paused
|
||||||
|
const audioElement = document.getElementById('live-audio');
|
||||||
|
if (audioElement && audioElement.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||||
const contentType = response.headers.get("content-type")
|
const contentType = response.headers.get("content-type")
|
||||||
|
|
@ -102,9 +108,60 @@ window.addEventListener('DOMContentLoaded', function() {
|
||||||
// Update playing information right after load
|
// Update playing information right after load
|
||||||
updateNowPlaying();
|
updateNowPlaying();
|
||||||
|
|
||||||
// Auto-reconnect on stream errors
|
// Auto-reconnect on stream errors and after long pauses
|
||||||
const audioElement = document.getElementById('live-audio');
|
const audioElement = document.getElementById('live-audio');
|
||||||
if (audioElement) {
|
if (audioElement) {
|
||||||
|
// Track pause timestamp to detect long pauses and reconnect
|
||||||
|
let pauseTimestamp = null;
|
||||||
|
let isReconnecting = false;
|
||||||
|
let needsReconnect = false;
|
||||||
|
const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
|
||||||
|
|
||||||
|
audioElement.addEventListener('pause', function() {
|
||||||
|
pauseTimestamp = Date.now();
|
||||||
|
console.log('Stream paused at:', pauseTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('play', function() {
|
||||||
|
// Check if we need to reconnect after long pause
|
||||||
|
if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||||
|
needsReconnect = true;
|
||||||
|
console.log('Long pause detected, will reconnect when playing starts...');
|
||||||
|
}
|
||||||
|
pauseTimestamp = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept the playing event to stop stale audio
|
||||||
|
audioElement.addEventListener('playing', function() {
|
||||||
|
if (needsReconnect && !isReconnecting) {
|
||||||
|
isReconnecting = true;
|
||||||
|
needsReconnect = false;
|
||||||
|
console.log('Reconnecting stream after long pause to clear stale buffers...');
|
||||||
|
|
||||||
|
// Stop the stale audio immediately
|
||||||
|
audioElement.pause();
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
isReconnecting = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
audioElement.addEventListener('error', function(e) {
|
audioElement.addEventListener('error', function(e) {
|
||||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (liveAudio) {
|
if (liveAudio) {
|
||||||
// Reduce buffer to minimize delay
|
// Reduce buffer to minimize delay
|
||||||
liveAudio.preload = 'none';
|
liveAudio.preload = 'none';
|
||||||
|
|
||||||
|
// Track pause timestamp to detect long pauses and reconnect
|
||||||
|
let pauseTimestamp = null;
|
||||||
|
let isReconnecting = false;
|
||||||
|
let needsReconnect = false;
|
||||||
|
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() {
|
||||||
|
// Check if we need to reconnect after long pause
|
||||||
|
if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||||
|
needsReconnect = true;
|
||||||
|
console.log('Long pause detected, will reconnect when playing starts...');
|
||||||
|
}
|
||||||
|
pauseTimestamp = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept the playing event to stop stale audio
|
||||||
|
liveAudio.addEventListener('playing', function() {
|
||||||
|
if (needsReconnect && !isReconnecting) {
|
||||||
|
isReconnecting = true;
|
||||||
|
needsReconnect = false;
|
||||||
|
console.log('Reconnecting live stream after long pause to clear stale buffers...');
|
||||||
|
|
||||||
|
// Stop the stale audio immediately
|
||||||
|
liveAudio.pause();
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
isReconnecting = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Restore user quality preference
|
// Restore user quality preference
|
||||||
const selector = document.getElementById('live-stream-quality');
|
const selector = document.getElementById('live-stream-quality');
|
||||||
|
|
@ -598,6 +649,12 @@ function changeLiveStreamQuality() {
|
||||||
|
|
||||||
// Live stream informatio update
|
// Live stream informatio update
|
||||||
async function updateNowPlaying() {
|
async function updateNowPlaying() {
|
||||||
|
// Don't update if stream is paused
|
||||||
|
const liveAudio = document.getElementById('live-stream-audio');
|
||||||
|
if (liveAudio && liveAudio.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||||
const contentType = response.headers.get("content-type")
|
const contentType = response.headers.get("content-type")
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,64 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track pause timestamp to detect long pauses and reconnect
|
||||||
|
let pauseTimestamp = null;
|
||||||
|
let isReconnecting = false;
|
||||||
|
let needsReconnect = false;
|
||||||
|
const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
|
||||||
|
|
||||||
|
audioElement.addEventListener('pause', function() {
|
||||||
|
pauseTimestamp = Date.now();
|
||||||
|
console.log('Frame player stream paused at:', pauseTimestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
audioElement.addEventListener('play', function() {
|
||||||
|
// Check if we need to reconnect after long pause
|
||||||
|
if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||||
|
needsReconnect = true;
|
||||||
|
console.log('Long pause detected, will reconnect when playing starts...');
|
||||||
|
}
|
||||||
|
pauseTimestamp = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept the playing event to stop stale audio
|
||||||
|
audioElement.addEventListener('playing', function() {
|
||||||
|
if (needsReconnect && !isReconnecting) {
|
||||||
|
isReconnecting = true;
|
||||||
|
needsReconnect = false;
|
||||||
|
console.log('Reconnecting frame player stream after long pause to clear stale buffers...');
|
||||||
|
|
||||||
|
// Stop the stale audio immediately
|
||||||
|
audioElement.pause();
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
isReconnecting = false;
|
||||||
|
}, 200);
|
||||||
|
} else {
|
||||||
|
console.log('Audio playing');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add event listeners for debugging
|
// Add event listeners for debugging
|
||||||
audioElement.addEventListener('waiting', function() {
|
audioElement.addEventListener('waiting', function() {
|
||||||
console.log('Audio buffering...');
|
console.log('Audio buffering...');
|
||||||
});
|
});
|
||||||
|
|
||||||
audioElement.addEventListener('playing', function() {
|
|
||||||
console.log('Audio playing');
|
|
||||||
});
|
|
||||||
|
|
||||||
audioElement.addEventListener('error', function(e) {
|
audioElement.addEventListener('error', function(e) {
|
||||||
console.error('Audio error:', e);
|
console.error('Audio error:', e);
|
||||||
});
|
});
|
||||||
|
|
@ -112,6 +161,12 @@
|
||||||
|
|
||||||
// Update mini now playing display
|
// Update mini now playing display
|
||||||
async function updateMiniNowPlaying() {
|
async function updateMiniNowPlaying() {
|
||||||
|
// Don't update if stream is paused
|
||||||
|
const audioElement = document.getElementById('persistent-audio');
|
||||||
|
if (audioElement && audioElement.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,12 @@
|
||||||
|
|
||||||
// Update now playing info for popout
|
// Update now playing info for popout
|
||||||
async function updatePopoutNowPlaying() {
|
async function updatePopoutNowPlaying() {
|
||||||
|
// Don't update if stream is paused
|
||||||
|
const audioElement = document.getElementById('live-audio');
|
||||||
|
if (audioElement && audioElement.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
@ -125,8 +131,60 @@
|
||||||
// Initial update
|
// Initial update
|
||||||
updatePopoutNowPlaying();
|
updatePopoutNowPlaying();
|
||||||
|
|
||||||
// Auto-reconnect on stream errors
|
// Auto-reconnect on stream errors and after long pauses
|
||||||
const audioElement = document.getElementById('live-audio');
|
const audioElement = document.getElementById('live-audio');
|
||||||
|
|
||||||
|
// Track pause timestamp to detect long pauses and reconnect
|
||||||
|
let pauseTimestamp = null;
|
||||||
|
let isReconnecting = false;
|
||||||
|
let needsReconnect = false;
|
||||||
|
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() {
|
||||||
|
// Check if we need to reconnect after long pause
|
||||||
|
if (!isReconnecting && pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||||
|
needsReconnect = true;
|
||||||
|
console.log('Long pause detected, will reconnect when playing starts...');
|
||||||
|
}
|
||||||
|
pauseTimestamp = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept the playing event to stop stale audio
|
||||||
|
audioElement.addEventListener('playing', function() {
|
||||||
|
if (needsReconnect && !isReconnecting) {
|
||||||
|
isReconnecting = true;
|
||||||
|
needsReconnect = false;
|
||||||
|
console.log('Reconnecting popout stream after long pause to clear stale buffers...');
|
||||||
|
|
||||||
|
// Stop the stale audio immediately
|
||||||
|
audioElement.pause();
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
isReconnecting = false;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
audioElement.addEventListener('error', function(e) {
|
audioElement.addEventListener('error', function(e) {
|
||||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue