| 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() { |
| |
| this.buildUI(); |
| |
| |
| this.initWavesurfer(); |
| |
| |
| this.setupEvents(); |
| } |
| |
| buildUI() { |
| |
| this.container.innerHTML = ''; |
| this.container.classList.add('waveplayer'); |
| |
| |
| 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); |
| |
| |
| const waveformContainer = document.createElement('div'); |
| waveformContainer.className = 'waveplayer-waveform'; |
| |
| const controlsContainer = document.createElement('div'); |
| controlsContainer.className = 'waveplayer-controls'; |
| |
| |
| this.playButton = document.createElement('button'); |
| this.playButton.className = 'waveplayer-play-btn'; |
| this.playButton.innerHTML = ` |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="play-icon"> |
| <polygon points="5 3 19 12 5 21 5 3"></polygon> |
| </svg> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="pause-icon" style="display: none;"> |
| <rect x="6" y="4" width="4" height="16"></rect> |
| <rect x="14" y="4" width="4" height="16"></rect> |
| </svg> |
| `; |
| |
| |
| this.timeDisplay = document.createElement('div'); |
| this.timeDisplay.className = 'waveplayer-time'; |
| this.timeDisplay.textContent = '0:00 / 0:00'; |
| |
| |
| this.loadingIndicator = document.createElement('div'); |
| this.loadingIndicator.className = 'waveplayer-loading'; |
| this.loadingIndicator.innerHTML = ` |
| <div class="waveplayer-spinner"></div> |
| <span>Loading...</span> |
| `; |
| |
| |
| 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%')) { |
| |
| setTimeout(() => this.hideLoading(), 300); |
| } |
| } |
| }); |
| }); |
| |
| observer.observe(loadingTextElement, { |
| characterData: true, |
| childList: true, |
| subtree: true |
| }); |
| } |
| |
| |
| controlsContainer.appendChild(this.playButton); |
| controlsContainer.appendChild(this.timeDisplay); |
| |
| this.container.appendChild(controlsContainer); |
| this.container.appendChild(waveformContainer); |
| this.container.appendChild(this.loadingIndicator); |
| |
| |
| this.waveformContainer = waveformContainer; |
| } |
| |
| initWavesurfer() { |
| |
| this.wavesurfer = WaveSurfer.create({ |
| container: this.waveformContainer, |
| ...this.options, |
| |
| interact: true, |
| dragToSeek: true |
| }); |
| |
| |
| if (this.loadingIndicator) { |
| this.loadingIndicator.style.display = 'none'; |
| } |
| } |
| |
| setupEvents() { |
| |
| this.playButton.addEventListener('click', () => { |
| this.togglePlayPause(); |
| }); |
| |
| |
| this.playButton.addEventListener('touchstart', (e) => { |
| e.preventDefault(); |
| this.togglePlayPause(); |
| }); |
| |
| |
| this.waveformContainer.addEventListener('touchstart', (e) => { |
| |
| e.stopPropagation(); |
| }); |
| |
| |
| this.wavesurfer.on('ready', () => { |
| |
| if (this.loadingTimeout) { |
| clearTimeout(this.loadingTimeout); |
| } |
| |
| |
| this.hideLoading(); |
| this.updateTimeDisplay(); |
| |
| |
| if (this.loadingIndicator && this.loadingIndicator.querySelector('span')) { |
| this.loadingIndicator.querySelector('span').textContent = 'Loading...'; |
| } |
| |
| console.log('WavePlayer ready event fired'); |
| }); |
| |
| |
| this.wavesurfer.on('decode', () => { |
| |
| this.hideLoading(); |
| console.log('WavePlayer decode event fired'); |
| }); |
| |
| |
| this.wavesurfer.on('loading', (percent) => { |
| this.showLoading(percent); |
| |
| |
| 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); |
| |
| |
| |
| this.loadingTimeout = setTimeout(() => { |
| this.hideLoading(); |
| }, 10000); |
| } |
| |
| 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'; |
| |
| |
| 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}`; |
| } |
| } |
|
|
| |
| window.WavePlayer = WavePlayer; |