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