Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 = ` | |
<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> | |
`; | |
// 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 = ` | |
<div class="waveplayer-spinner"></div> | |
<span>Loading...</span> | |
`; | |
// 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; |