fix: Reduce Icecast burst size and prevent now-playing updates during pause
- Reduced Icecast burst-size from 64KB to 8KB to minimize buffer accumulation - Fixed spectrum analyzer to only create MediaElementSource once - Added resetSpectrumAnalyzer() function to allow reconnection - Prevent now-playing info updates when stream is paused across all players: * Player page * Front page * Pop-out player * Frame player * Admin page - After pause >10s, reconnect stream and reinitialize spectrum analyzer - Preserves spectrum analyzer functionality after pause/unpause - Eliminates stuttering and buffer accumulation issues
This commit is contained in:
parent
924f6498de
commit
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]
|
||||
- [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
|
||||
2) [X] icecast is also binding the external interface on b612, which it
|
||||
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,
|
||||
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.
|
||||
|
||||
6) [ ] The administrative interface should be beefed up.
|
||||
6.1) [ ] Deactivate users
|
||||
6.2) [ ] Change user access permissions
|
||||
6.1) [X] Deactivate users
|
||||
6.2) [X] Change user access permissions
|
||||
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.
|
||||
8) [ ] User profile pages should probably be fleshed out.
|
||||
9) [ ] the stream management features aren't there for Admins or DJs.
|
||||
10) [ ] 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.
|
||||
12) [ ] ensure each info field 'Listeners: ..' &c has only one instance per page.
|
||||
10) [X] The "Scan Library" feature is not working in the main branch
|
||||
11) [X] The player widget should be styled so it fits the site theme on systems running 'light' thmes.
|
||||
12) [X] ensure each info field 'Listeners: ..' &c has only one instance per page.
|
||||
|
||||
* Server runtime configuration [0/1]
|
||||
- [ ] parameterize all configuration for runtime loading [0/2]
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
<header-timeout>15</header-timeout>
|
||||
<source-timeout>10</source-timeout>
|
||||
<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>
|
||||
|
||||
<authentication>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,18 @@
|
|||
(defvar *canvas* nil)
|
||||
(defvar *canvas-ctx* 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 ()
|
||||
"Initialize the spectrum analyzer"
|
||||
|
|
@ -37,27 +49,35 @@
|
|||
(:catch (e)
|
||||
(ps:chain console (log "Cross-frame access error:" e)))))
|
||||
|
||||
(when (and audio-element canvas-element (not *audio-context*))
|
||||
;; Create Audio Context
|
||||
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
||||
(ps:@ window |webkitAudioContext|))))
|
||||
(when (and audio-element canvas-element)
|
||||
;; Store current audio element
|
||||
(setf *current-audio-element* audio-element)
|
||||
|
||||
;; Create Analyser Node
|
||||
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
||||
(setf (ps:@ *analyser* |fftSize|) 256)
|
||||
(setf (ps:@ *analyser* |smoothingTimeConstant|) 0.8)
|
||||
|
||||
;; Connect audio source to analyser
|
||||
(let ((source (ps:chain *audio-context* (create-media-element-source audio-element))))
|
||||
(ps:chain source (connect *analyser*))
|
||||
(ps:chain *analyser* (connect (ps:@ *audio-context* destination))))
|
||||
;; Only create audio context and media source once
|
||||
(when (not *audio-context*)
|
||||
;; Create Audio Context
|
||||
(setf *audio-context* (ps:new (or (ps:@ window |AudioContext|)
|
||||
(ps:@ window |webkitAudioContext|))))
|
||||
|
||||
;; Create Analyser Node
|
||||
(setf *analyser* (ps:chain *audio-context* (create-analyser)))
|
||||
(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
|
||||
(setf *canvas* canvas-element)
|
||||
(setf *canvas-ctx* (ps:chain *canvas* (get-context "2d")))
|
||||
|
||||
;; Start visualization
|
||||
(draw-spectrum))))
|
||||
;; Start visualization if not already running
|
||||
(when (not *animation-id*)
|
||||
(draw-spectrum)))))
|
||||
|
||||
(defun draw-spectrum ()
|
||||
"Draw the spectrum analyzer visualization"
|
||||
|
|
|
|||
|
|
@ -635,6 +635,12 @@ function displayQueueSearchResults(results) {
|
|||
|
||||
// Live stream info update
|
||||
async function updateLiveStreamInfo() {
|
||||
// Don't update if stream is paused
|
||||
const audioElement = document.getElementById('live-stream-audio');
|
||||
if (audioElement && audioElement.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
const contentType = response.headers.get("content-type");
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ function changeStreamQuality() {
|
|||
|
||||
// Update now playing info from Icecast
|
||||
async function updateNowPlaying() {
|
||||
// Don't update if stream is paused
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
if (audioElement && audioElement.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||
const contentType = response.headers.get("content-type")
|
||||
|
|
@ -102,9 +108,42 @@ window.addEventListener('DOMContentLoaded', function() {
|
|||
// Update playing information right after load
|
||||
updateNowPlaying();
|
||||
|
||||
// Auto-reconnect on stream errors
|
||||
// Auto-reconnect on stream errors and after long pauses
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
if (audioElement) {
|
||||
// Track pause timestamp to detect long pauses and reconnect
|
||||
let pauseTimestamp = null;
|
||||
const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
|
||||
|
||||
audioElement.addEventListener('pause', function() {
|
||||
pauseTimestamp = Date.now();
|
||||
console.log('Stream paused at:', pauseTimestamp);
|
||||
});
|
||||
|
||||
audioElement.addEventListener('play', function() {
|
||||
if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||
console.log('Reconnecting stream after long pause to clear stale buffers...');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
pauseTimestamp = null;
|
||||
});
|
||||
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||
setTimeout(function() {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (liveAudio) {
|
||||
// Reduce buffer to minimize delay
|
||||
liveAudio.preload = 'none';
|
||||
|
||||
// Track pause timestamp to detect long pauses and reconnect
|
||||
let pauseTimestamp = null;
|
||||
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() {
|
||||
if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||
console.log('Reconnecting live stream after long pause to clear stale buffers...');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
pauseTimestamp = null;
|
||||
});
|
||||
}
|
||||
// Restore user quality preference
|
||||
const selector = document.getElementById('live-stream-quality');
|
||||
|
|
@ -598,6 +631,12 @@ function changeLiveStreamQuality() {
|
|||
|
||||
// Live stream informatio update
|
||||
async function updateNowPlaying() {
|
||||
// Don't update if stream is paused
|
||||
const liveAudio = document.getElementById('live-stream-audio');
|
||||
if (liveAudio && liveAudio.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing')
|
||||
const contentType = response.headers.get("content-type")
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Track pause timestamp to detect long pauses and reconnect
|
||||
let pauseTimestamp = null;
|
||||
const PAUSE_RECONNECT_THRESHOLD = 10000; // 10 seconds
|
||||
|
||||
audioElement.addEventListener('pause', function() {
|
||||
pauseTimestamp = Date.now();
|
||||
console.log('Frame player stream paused at:', pauseTimestamp);
|
||||
});
|
||||
|
||||
// Add event listeners for debugging
|
||||
audioElement.addEventListener('waiting', function() {
|
||||
console.log('Audio buffering...');
|
||||
|
|
@ -60,6 +69,30 @@
|
|||
console.error('Audio error:', e);
|
||||
});
|
||||
|
||||
audioElement.addEventListener('play', function() {
|
||||
if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||
console.log('Reconnecting frame player stream after long pause to clear stale buffers...');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
pauseTimestamp = null;
|
||||
});
|
||||
|
||||
const selector = document.getElementById('stream-quality');
|
||||
const streamQuality = localStorage.getItem('stream-quality') || 'aac';
|
||||
if (selector && selector.value !== streamQuality) {
|
||||
|
|
@ -112,6 +145,12 @@
|
|||
|
||||
// Update mini now playing display
|
||||
async function updateMiniNowPlaying() {
|
||||
// Don't update if stream is paused
|
||||
const audioElement = document.getElementById('persistent-audio');
|
||||
if (audioElement && audioElement.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
if (response.ok) {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,12 @@
|
|||
|
||||
// Update now playing info for popout
|
||||
async function updatePopoutNowPlaying() {
|
||||
// Don't update if stream is paused
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
if (audioElement && audioElement.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/asteroid/partial/now-playing-inline');
|
||||
const html = await response.text();
|
||||
|
|
@ -125,8 +131,42 @@
|
|||
// Initial update
|
||||
updatePopoutNowPlaying();
|
||||
|
||||
// Auto-reconnect on stream errors
|
||||
// Auto-reconnect on stream errors and after long pauses
|
||||
const audioElement = document.getElementById('live-audio');
|
||||
|
||||
// Track pause timestamp to detect long pauses and reconnect
|
||||
let pauseTimestamp = null;
|
||||
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() {
|
||||
if (pauseTimestamp && (Date.now() - pauseTimestamp) > PAUSE_RECONNECT_THRESHOLD) {
|
||||
console.log('Reconnecting popout stream after long pause to clear stale buffers...');
|
||||
|
||||
// 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');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
pauseTimestamp = null;
|
||||
});
|
||||
|
||||
audioElement.addEventListener('error', function(e) {
|
||||
console.log('Stream error, attempting reconnect in 3 seconds...');
|
||||
setTimeout(function() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue