class WavePlayer { constructor(container, options = {}) { this.container = container; this.options = { waveColor: '#d1d6e0', progressColor: '#5046e5', cursorColor: '#5046e5', cursorWidth: 2, height: 80, responsive: true, barWidth: 2, barGap: 1, hideScrollbar: true, ...options }; this.isPlaying = false; this.wavesurfer = null; this.loadingIndicator = null; this.playButton = null; this.init(); } init() { // Create player UI this.buildUI(); // Initialize wavesurfer this.initWavesurfer(); // Setup event listeners this.setupEvents(); } buildUI() { // Clear container this.container.innerHTML = ''; this.container.classList.add('waveplayer'); // Add style to hide native audio elements that might be rendered by wavesurfer const style = document.createElement('style'); style.textContent = ` .waveplayer audio { display: none !important; } /* Mobile optimizations */ @media (max-width: 768px) { .waveplayer-play-btn { width: 44px; height: 44px; margin-right: 12px; } .waveplayer-waveform { height: 70px; cursor: pointer; touch-action: none; /* Prevents scroll/zoom on touch */ } } `; this.container.appendChild(style); // Create elements const waveformContainer = document.createElement('div'); waveformContainer.className = 'waveplayer-waveform'; const controlsContainer = document.createElement('div'); controlsContainer.className = 'waveplayer-controls'; // Play button this.playButton = document.createElement('button'); this.playButton.className = 'waveplayer-play-btn'; this.playButton.innerHTML = ` `; // Time display this.timeDisplay = document.createElement('div'); this.timeDisplay.className = 'waveplayer-time'; this.timeDisplay.textContent = '0:00 / 0:00'; // Loading indicator this.loadingIndicator = document.createElement('div'); this.loadingIndicator.className = 'waveplayer-loading'; this.loadingIndicator.innerHTML = `
Loading... `; // Set up MutationObserver to detect when loading reaches 100% const loadingTextElement = this.loadingIndicator.querySelector('span'); if (loadingTextElement) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'characterData' || mutation.type === 'childList') { const text = loadingTextElement.textContent; if (text && text.includes('100%')) { // If we see "100%", hide the loading indicator after a short delay setTimeout(() => this.hideLoading(), 300); } } }); }); observer.observe(loadingTextElement, { characterData: true, childList: true, subtree: true }); } // Append elements controlsContainer.appendChild(this.playButton); controlsContainer.appendChild(this.timeDisplay); this.container.appendChild(controlsContainer); this.container.appendChild(waveformContainer); this.container.appendChild(this.loadingIndicator); // Store reference to waveform container this.waveformContainer = waveformContainer; } initWavesurfer() { // Initialize WaveSurfer this.wavesurfer = WaveSurfer.create({ container: this.waveformContainer, ...this.options, // Add mobile touch support interact: true, dragToSeek: true }); // Force reset any loading indicators if (this.loadingIndicator) { this.loadingIndicator.style.display = 'none'; } } setupEvents() { // Play/pause button this.playButton.addEventListener('click', () => { this.togglePlayPause(); }); // Add touch support for mobile this.playButton.addEventListener('touchstart', (e) => { e.preventDefault(); this.togglePlayPause(); }); // Add touch support for waveform container this.waveformContainer.addEventListener('touchstart', (e) => { // This helps ensure the touch events propagate correctly to wavesurfer e.stopPropagation(); }); // Wavesurfer events this.wavesurfer.on('ready', () => { // Clear loading timeout if (this.loadingTimeout) { clearTimeout(this.loadingTimeout); } // Explicitly ensure loading indicator is hidden this.hideLoading(); this.updateTimeDisplay(); // Force loading message to be reset if (this.loadingIndicator && this.loadingIndicator.querySelector('span')) { this.loadingIndicator.querySelector('span').textContent = 'Loading...'; } console.log('WavePlayer ready event fired'); }); // Add specific handler for decode event (fired when audio is decoded) this.wavesurfer.on('decode', () => { // Also hide loading indicator after decode this.hideLoading(); console.log('WavePlayer decode event fired'); }); // Add specific handler for loading complete this.wavesurfer.on('loading', (percent) => { this.showLoading(percent); // If loading reaches 100%, make sure to hide the loader after a small delay if (percent === 100) { setTimeout(() => { this.hideLoading(); console.log('WavePlayer loading 100% - force hiding loader'); }, 500); } }); this.wavesurfer.on('play', () => { this.isPlaying = true; this.updatePlayButton(); }); this.wavesurfer.on('pause', () => { this.isPlaying = false; this.updatePlayButton(); }); this.wavesurfer.on('finish', () => { this.isPlaying = false; this.updatePlayButton(); }); this.wavesurfer.on('audioprocess', () => { this.updateTimeDisplay(); }); this.wavesurfer.on('seek', () => { this.updateTimeDisplay(); }); this.wavesurfer.on('error', (err) => { console.error('WaveSurfer error:', err); this.hideLoading(); }); } loadAudio(url) { this.showLoading(); this.wavesurfer.load(url); // Safety timeout to ensure loading indicator gets hidden // even if the 'ready' event doesn't fire properly this.loadingTimeout = setTimeout(() => { this.hideLoading(); }, 10000); // 10 seconds max loading time } play() { this.wavesurfer.play(); } pause() { this.wavesurfer.pause(); } togglePlayPause() { this.wavesurfer.playPause(); } stop() { this.wavesurfer.stop(); } updatePlayButton() { const playIcon = this.playButton.querySelector('.play-icon'); const pauseIcon = this.playButton.querySelector('.pause-icon'); if (this.isPlaying) { playIcon.style.display = 'none'; pauseIcon.style.display = 'block'; } else { playIcon.style.display = 'block'; pauseIcon.style.display = 'none'; } } showLoading(percent) { this.loadingIndicator.style.display = 'flex'; if (percent !== undefined) { this.loadingIndicator.querySelector('span').textContent = `Loading: ${Math.round(percent)}%`; } } hideLoading() { if (this.loadingIndicator) { this.loadingIndicator.style.display = 'none'; // Reset loading text const loadingText = this.loadingIndicator.querySelector('span'); if (loadingText) { loadingText.textContent = 'Loading...'; } } } formatTime(seconds) { const minutes = Math.floor(seconds / 60); const secondsRemainder = Math.round(seconds) % 60; const paddedSeconds = secondsRemainder.toString().padStart(2, '0'); return `${minutes}:${paddedSeconds}`; } updateTimeDisplay() { if (!this.wavesurfer.isReady) return; const currentTime = this.formatTime(this.wavesurfer.getCurrentTime()); const duration = this.formatTime(this.wavesurfer.getDuration()); this.timeDisplay.textContent = `${currentTime} / ${duration}`; } } // Allow global access window.WavePlayer = WavePlayer;