Move legacy audio-player.js to dev directory
- audio-player.js was legacy code not used in production - Actual audio players are in app.js (personal stream) and streams-ui.js (streams page) - Moving to dev directory to keep production code clean
This commit is contained in:
@ -1,424 +0,0 @@
|
||||
/**
|
||||
* Audio Player Module
|
||||
* A shared audio player implementation based on the working "Your Stream" player
|
||||
*/
|
||||
|
||||
export class AudioPlayer {
|
||||
constructor() {
|
||||
// Audio state
|
||||
this.audioElement = null;
|
||||
this.currentUid = null;
|
||||
this.isPlaying = false;
|
||||
this.currentButton = null;
|
||||
this.audioUrl = '';
|
||||
this.lastPlayTime = 0;
|
||||
this.isLoading = false;
|
||||
this.loadTimeout = null; // For tracking loading timeouts
|
||||
|
||||
// Create a single audio element that we'll reuse
|
||||
this.audioElement = new Audio();
|
||||
this.audioElement.preload = 'none';
|
||||
this.audioElement.crossOrigin = 'anonymous';
|
||||
|
||||
// Bind methods
|
||||
this.loadAndPlay = this.loadAndPlay.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.cleanup = this.cleanup.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and play audio for a specific UID
|
||||
* @param {string} uid - The user ID for the audio stream
|
||||
* @param {HTMLElement} button - The play/pause button element
|
||||
*/
|
||||
/**
|
||||
* Validates that a UID is in the correct UUID format
|
||||
* @param {string} uid - The UID to validate
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
isValidUuid(uid) {
|
||||
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error and updates the button state
|
||||
* @param {HTMLElement} button - The button to update
|
||||
* @param {string} message - Error message to log
|
||||
*/
|
||||
handleError(button, message) {
|
||||
console.error(message);
|
||||
if (button) {
|
||||
this.updateButtonState(button, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadAndPlay(uid, button) {
|
||||
// Validate UID exists and is in correct format
|
||||
if (!uid) {
|
||||
this.handleError(button, 'No UID provided for audio playback');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isValidUuid(uid)) {
|
||||
this.handleError(button, `Invalid UID format: ${uid}. Expected UUID v4 format.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're in the middle of loading, check if it's for the same UID
|
||||
if (this.isLoading) {
|
||||
// If same UID, ignore duplicate request
|
||||
if (this.currentUid === uid) {
|
||||
console.log('Already loading this UID, ignoring duplicate request:', uid);
|
||||
return;
|
||||
}
|
||||
// If different UID, queue the new request
|
||||
console.log('Already loading, queuing request for UID:', uid);
|
||||
setTimeout(() => this.loadAndPlay(uid, button), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
// If already playing this stream, just toggle pause/play
|
||||
if (this.currentUid === uid && this.audioElement) {
|
||||
try {
|
||||
if (this.isPlaying) {
|
||||
console.log('Pausing current playback');
|
||||
try {
|
||||
this.audioElement.pause();
|
||||
this.lastPlayTime = this.audioElement.currentTime;
|
||||
this.isPlaying = false;
|
||||
this.updateButtonState(button, 'paused');
|
||||
} catch (pauseError) {
|
||||
console.warn('Error pausing audio, continuing with state update:', pauseError);
|
||||
this.isPlaying = false;
|
||||
this.updateButtonState(button, 'paused');
|
||||
}
|
||||
} else {
|
||||
console.log('Resuming playback from time:', this.lastPlayTime);
|
||||
try {
|
||||
// If we have a last play time, seek to it
|
||||
if (this.lastPlayTime > 0) {
|
||||
this.audioElement.currentTime = this.lastPlayTime;
|
||||
}
|
||||
await this.audioElement.play();
|
||||
this.isPlaying = true;
|
||||
this.updateButtonState(button, 'playing');
|
||||
} catch (playError) {
|
||||
console.error('Error resuming playback, reloading source:', playError);
|
||||
// If resume fails, try reloading the source
|
||||
this.currentUid = null; // Force reload of the source
|
||||
return this.loadAndPlay(uid, button);
|
||||
}
|
||||
}
|
||||
return; // Exit after handling pause/resume
|
||||
} catch (error) {
|
||||
console.error('Error toggling playback:', error);
|
||||
this.updateButtonState(button, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, we're loading a new stream
|
||||
this.isLoading = true;
|
||||
this.currentUid = uid;
|
||||
this.currentButton = button;
|
||||
this.isPlaying = true;
|
||||
this.updateButtonState(button, 'loading');
|
||||
|
||||
try {
|
||||
// Only clean up if switching streams
|
||||
if (this.currentUid !== uid) {
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
// Store the current button reference
|
||||
this.currentButton = button;
|
||||
this.currentUid = uid;
|
||||
|
||||
// Create a new audio element if we don't have one
|
||||
if (!this.audioElement) {
|
||||
this.audioElement = new Audio();
|
||||
} else if (this.audioElement.readyState > 0) {
|
||||
// If we already have a loaded source, just play it
|
||||
try {
|
||||
await this.audioElement.play();
|
||||
this.isPlaying = true;
|
||||
this.updateButtonState(button, 'playing');
|
||||
return;
|
||||
} catch (playError) {
|
||||
console.warn('Error playing existing source, will reload:', playError);
|
||||
// Continue to load a new source
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any existing sources
|
||||
while (this.audioElement.firstChild) {
|
||||
this.audioElement.removeChild(this.audioElement.firstChild);
|
||||
}
|
||||
|
||||
// Set the source URL with proper encoding and cache-busting timestamp
|
||||
// Using the format: /audio/{uid}/stream.opus?t={timestamp}
|
||||
const timestamp = new Date().getTime();
|
||||
this.audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${timestamp}`;
|
||||
console.log('Loading audio from URL:', this.audioUrl);
|
||||
this.audioElement.src = this.audioUrl;
|
||||
|
||||
// Load the new source (don't await, let canplay handle it)
|
||||
try {
|
||||
this.audioElement.load();
|
||||
// If load() doesn't throw, we'll wait for canplay event
|
||||
} catch (e) {
|
||||
// Ignore abort errors as they're expected during rapid toggling
|
||||
if (e.name !== 'AbortError') {
|
||||
console.error('Error loading audio source:', e);
|
||||
this.isLoading = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the current time when loading a new source
|
||||
this.audioElement.currentTime = 0;
|
||||
this.lastPlayTime = 0;
|
||||
|
||||
// Set up error handling
|
||||
this.audioElement.onerror = (e) => {
|
||||
console.error('Audio element error:', e, this.audioElement.error);
|
||||
this.isLoading = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
};
|
||||
|
||||
// Handle when audio is ready to play
|
||||
const onCanPlay = () => {
|
||||
this.audioElement.removeEventListener('canplay', onCanPlay);
|
||||
this.isLoading = false;
|
||||
if (this.lastPlayTime > 0) {
|
||||
this.audioElement.currentTime = this.lastPlayTime;
|
||||
}
|
||||
this.audioElement.play().then(() => {
|
||||
this.isPlaying = true;
|
||||
this.updateButtonState(button, 'playing');
|
||||
}).catch(e => {
|
||||
console.error('Error playing after load:', e);
|
||||
this.updateButtonState(button, 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// Define the error handler
|
||||
const errorHandler = (e) => {
|
||||
console.error('Audio element error:', e, this.audioElement.error);
|
||||
this.isLoading = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
};
|
||||
|
||||
// Define the play handler
|
||||
const playHandler = () => {
|
||||
// Clear any pending timeouts
|
||||
if (this.loadTimeout) {
|
||||
clearTimeout(this.loadTimeout);
|
||||
this.loadTimeout = null;
|
||||
}
|
||||
|
||||
this.audioElement.removeEventListener('canplay', playHandler);
|
||||
this.isLoading = false;
|
||||
|
||||
if (this.lastPlayTime > 0) {
|
||||
this.audioElement.currentTime = this.lastPlayTime;
|
||||
}
|
||||
|
||||
this.audioElement.play().then(() => {
|
||||
this.isPlaying = true;
|
||||
this.updateButtonState(button, 'playing');
|
||||
}).catch(e => {
|
||||
console.error('Error playing after load:', e);
|
||||
this.isPlaying = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
});
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
this.audioElement.addEventListener('error', errorHandler, { once: true });
|
||||
this.audioElement.addEventListener('canplay', playHandler, { once: true });
|
||||
|
||||
// Load and play the new source
|
||||
try {
|
||||
await this.audioElement.load();
|
||||
// Don't await play() here, let the canplay handler handle it
|
||||
|
||||
// Set a timeout to handle cases where canplay doesn't fire
|
||||
this.loadTimeout = setTimeout(() => {
|
||||
if (this.isLoading) {
|
||||
console.warn('Audio loading timed out for UID:', uid);
|
||||
this.isLoading = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
}
|
||||
}, 10000); // 10 second timeout
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error loading audio:', e);
|
||||
this.isLoading = false;
|
||||
this.updateButtonState(button, 'error');
|
||||
|
||||
// Clear any pending timeouts
|
||||
if (this.loadTimeout) {
|
||||
clearTimeout(this.loadTimeout);
|
||||
this.loadTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in loadAndPlay:', error);
|
||||
|
||||
// Only cleanup and show error if we're still on the same track
|
||||
if (this.currentUid === uid) {
|
||||
this.cleanup();
|
||||
this.updateButtonState(button, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop playback and clean up resources
|
||||
*/
|
||||
stop() {
|
||||
try {
|
||||
if (this.audioElement) {
|
||||
console.log('Stopping audio playback');
|
||||
this.audioElement.pause();
|
||||
this.lastPlayTime = this.audioElement.currentTime;
|
||||
this.isPlaying = false;
|
||||
if (this.currentButton) {
|
||||
this.updateButtonState(this.currentButton, 'paused');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping audio:', error);
|
||||
// Don't throw, just log the error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources
|
||||
*/
|
||||
cleanup() {
|
||||
// Update button state if we have a reference to the current button
|
||||
if (this.currentButton) {
|
||||
this.updateButtonState(this.currentButton, 'paused');
|
||||
}
|
||||
|
||||
// Pause the audio and store the current time
|
||||
if (this.audioElement) {
|
||||
try {
|
||||
try {
|
||||
this.audioElement.pause();
|
||||
this.lastPlayTime = this.audioElement.currentTime;
|
||||
} catch (e) {
|
||||
console.warn('Error pausing audio during cleanup:', e);
|
||||
}
|
||||
|
||||
try {
|
||||
// Clear any existing sources
|
||||
while (this.audioElement.firstChild) {
|
||||
this.audioElement.removeChild(this.audioElement.firstChild);
|
||||
}
|
||||
|
||||
// Clear the source and reset the audio element
|
||||
this.audioElement.removeAttribute('src');
|
||||
try {
|
||||
this.audioElement.load();
|
||||
} catch (e) {
|
||||
console.warn('Error in audio load during cleanup:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error cleaning up audio sources:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error during audio cleanup:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state
|
||||
this.currentUid = null;
|
||||
this.currentButton = null;
|
||||
this.audioUrl = '';
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the state of a play/pause button
|
||||
* @param {HTMLElement} button - The button to update
|
||||
* @param {string} state - The state to set ('playing', 'paused', 'loading', 'error')
|
||||
*/
|
||||
updateButtonState(button, state) {
|
||||
if (!button) return;
|
||||
|
||||
// Only update the current button's state
|
||||
if (state === 'playing') {
|
||||
// If this button is now playing, update all buttons
|
||||
document.querySelectorAll('.play-pause-btn').forEach(btn => {
|
||||
btn.classList.remove('playing', 'paused', 'loading', 'error');
|
||||
if (btn === button) {
|
||||
btn.classList.add('playing');
|
||||
} else {
|
||||
btn.classList.add('paused');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// For other states, just update the target button
|
||||
button.classList.remove('playing', 'paused', 'loading', 'error');
|
||||
if (state) {
|
||||
button.classList.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Update button icon and aria-label for the target button
|
||||
const icon = button.querySelector('i');
|
||||
if (icon) {
|
||||
if (state === 'playing') {
|
||||
icon.className = 'fas fa-pause';
|
||||
button.setAttribute('aria-label', 'Pause');
|
||||
} else {
|
||||
icon.className = 'fas fa-play';
|
||||
button.setAttribute('aria-label', 'Play');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
export const audioPlayer = new AudioPlayer();
|
||||
|
||||
// Export utility functions for direct use
|
||||
export function initAudioPlayer(container = document) {
|
||||
// Set up event delegation for play/pause buttons
|
||||
container.addEventListener('click', (e) => {
|
||||
const playButton = e.target.closest('.play-pause-btn');
|
||||
if (!playButton) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const uid = playButton.dataset.uid;
|
||||
if (!uid) return;
|
||||
|
||||
audioPlayer.loadAndPlay(uid, playButton);
|
||||
});
|
||||
|
||||
// Set up event delegation for stop buttons if they exist
|
||||
container.addEventListener('click', (e) => {
|
||||
const stopButton = e.target.closest('.stop-btn');
|
||||
if (!stopButton) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
audioPlayer.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize if this is the main module
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initAudioPlayer();
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user