/** * AudioPlayer Component * * Full-featured audio player with playback controls, seek, volume, and speed adjustment. * Handles various error scenarios and browser compatibility. */ function AudioPlayer({ audioUrl, filename }) { const audioRef = React.useRef(null); const [isPlaying, setIsPlaying] = React.useState(false); const [currentTime, setCurrentTime] = React.useState(0); const [duration, setDuration] = React.useState(0); const [volume, setVolume] = React.useState(1); const [playbackRate, setPlaybackRate] = React.useState(1); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(null); // Setup audio event listeners React.useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleLoadedMetadata = () => { setDuration(audio.duration); setIsLoading(false); setError(null); }; const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); }; const handleEnded = () => { setIsPlaying(false); setCurrentTime(0); }; const handleError = (e) => { console.error('[AudioPlayer] Error:', e); setIsLoading(false); const audioElement = e.target; const errorCode = audioElement.error?.code; switch (errorCode) { case 1: // MEDIA_ERR_ABORTED setError('Playback aborted'); break; case 2: // MEDIA_ERR_NETWORK setError('Network error - check connection'); break; case 3: // MEDIA_ERR_DECODE setError('File corrupted or invalid format'); break; case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED setError('Audio format not supported'); break; default: setError('Failed to load audio'); } }; const handleCanPlay = () => { setError(null); }; audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('ended', handleEnded); audio.addEventListener('error', handleError); audio.addEventListener('canplay', handleCanPlay); return () => { audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('timeupdate', handleTimeUpdate); audio.removeEventListener('ended', handleEnded); audio.removeEventListener('error', handleError); audio.removeEventListener('canplay', handleCanPlay); }; }, [audioUrl]); // Update audio volume React.useEffect(() => { if (audioRef.current) { audioRef.current.volume = volume; } }, [volume]); // Update playback rate React.useEffect(() => { if (audioRef.current) { audioRef.current.playbackRate = playbackRate; } }, [playbackRate]); const togglePlay = async () => { const audio = audioRef.current; if (!audio || error) return; try { if (isPlaying) { audio.pause(); setIsPlaying(false); } else { await audio.play(); setIsPlaying(true); } } catch (playError) { console.error('[AudioPlayer] Play error:', playError); setError('Playback failed'); } }; const handleSeek = (e) => { const audio = audioRef.current; if (!audio || error) return; const newTime = parseFloat(e.target.value); audio.currentTime = newTime; setCurrentTime(newTime); }; const handleVolumeChange = (e) => { setVolume(parseFloat(e.target.value)); }; const changePlaybackRate = (rate) => { setPlaybackRate(rate); }; const formatTime = (seconds) => { if (isNaN(seconds) || !isFinite(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; // Component styles const styles = { player: { background: 'var(--bg-secondary)', borderRadius: '12px', padding: '1rem', width: '100%' }, filename: { fontSize: '0.9rem', fontWeight: '600', marginBottom: '0.75rem', color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: '0.5rem' }, loading: { textAlign: 'center', padding: '1rem', color: 'var(--text-secondary)', fontSize: '0.875rem' }, error: { textAlign: 'center', padding: '1rem', color: 'var(--danger)', fontSize: '0.875rem', background: 'rgba(239, 68, 68, 0.1)', borderRadius: '8px' }, controls: { display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.75rem' }, playButton: { background: 'var(--primary)', color: 'white', border: 'none', borderRadius: '50%', width: '40px', height: '40px', fontSize: '1.25rem', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'transform 0.2s', }, seekContainer: { flex: 1, display: 'flex', alignItems: 'center' }, seekBar: { width: '100%', cursor: 'pointer' }, time: { fontSize: '0.75rem', color: 'var(--text-secondary)', minWidth: '80px', textAlign: 'right', flexShrink: 0 }, secondaryControls: { display: 'flex', alignItems: 'center', gap: '1rem', paddingTop: '0.5rem', borderTop: '1px solid var(--border-color)' }, volumeContainer: { display: 'flex', alignItems: 'center', gap: '0.5rem', flex: 1 }, volumeBar: { flex: 1, maxWidth: '100px', cursor: 'pointer' }, speedContainer: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, speedLabel: { fontSize: '0.75rem', color: 'var(--text-secondary)' }, speedButton: { background: 'transparent', border: '1px solid var(--border-color)', borderRadius: '4px', padding: '0.25rem 0.5rem', fontSize: '0.7rem', cursor: 'pointer', color: 'var(--text-secondary)', transition: 'all 0.2s' }, speedButtonActive: { background: 'var(--primary)', border: '1px solid var(--primary)', color: 'white' } }; return (
); } // Export to global scope window.AudioPlayer = AudioPlayer;