88 IconButton ,
99} from '@mui/joy' ;
1010import { Play , Pause , Volume2 } from 'lucide-react' ;
11+ import WaveSurfer from 'wavesurfer.js' ;
1112
1213interface AudioPlayerProps {
1314 audioData : {
@@ -20,47 +21,69 @@ interface AudioPlayerProps {
2021}
2122
2223const AudioPlayer : React . FC < AudioPlayerProps > = ( { audioData, metadata } ) => {
23- const [ isPlaying , setIsPlaying ] = React . useState ( false ) ;
24- const [ audio ] = React . useState ( new Audio ( audioData . audio_data_url ) ) ;
24+ const [ wavesurfer , setWavesurfer ] = React . useState < WaveSurfer | null > ( null ) ;
25+ const waveformRef = React . useRef < HTMLDivElement > ( null ) ;
26+ const isDestroyedRef = React . useRef ( false ) ;
2527
26- const togglePlay = ( ) => {
27- if ( isPlaying ) {
28- audio . pause ( ) ;
29- setIsPlaying ( false ) ;
30- } else {
31- audio . play ( ) ;
32- setIsPlaying ( true ) ;
28+ // Initialize Wavesurfer
29+ React . useEffect ( ( ) => {
30+ if ( waveformRef . current && ! wavesurfer && ! isDestroyedRef . current ) {
31+ const ws = WaveSurfer . create ( {
32+ container : waveformRef . current ,
33+ waveColor : '#4f46e5' ,
34+ // progressColor: '#7c3aed',
35+ cursorColor : 'var(--joy-palette-primary-400)' ,
36+ height : 60 ,
37+ normalize : true ,
38+ barWidth : 3 ,
39+ barGap : 2 ,
40+ barRadius : 3 ,
41+ fillParent : true ,
42+ pixelRatio : 1 ,
43+ mediaControls : true ,
44+ } ) ;
45+
46+ ws . load ( audioData . audio_data_url ) ;
47+
48+ setWavesurfer ( ws ) ;
49+
50+ return ( ) => {
51+ isDestroyedRef . current = true ;
52+ if ( ws && ! ws . isDestroyed ) {
53+ try {
54+ ws . pause ( ) ;
55+ ws . destroy ( ) ;
56+ } catch ( error ) {
57+ // Ignore errors during cleanup
58+ console . warn ( 'Error destroying wavesurfer:' , error ) ;
59+ }
60+ }
61+ setWavesurfer ( null ) ;
62+ } ;
3363 }
34- } ;
64+ } , [ audioData . audio_data_url ] ) ;
3565
36- // Handle audio ended event
66+ // Reset destroyed flag when audio URL changes
3767 React . useEffect ( ( ) => {
38- const handleEnded = ( ) => setIsPlaying ( false ) ;
39- audio . addEventListener ( 'ended' , handleEnded ) ;
40- return ( ) => audio . removeEventListener ( 'ended' , handleEnded ) ;
41- } , [ audio ] ) ;
68+ isDestroyedRef . current = false ;
69+ } , [ audioData . audio_data_url ] ) ;
4270
4371 return (
44- < Card variant = "outlined" sx = { { maxWidth : 400 } } >
72+ < Card >
4573 < CardContent >
4674 < Stack spacing = { 2 } >
47- { /* Audio Controls */ }
48- < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
49- < IconButton
50- size = "sm"
51- variant = "solid"
52- color = "primary"
53- onClick = { togglePlay }
54- >
55- { isPlaying ? < Pause size = { 16 } /> : < Play size = { 16 } /> }
56- </ IconButton >
57- < Volume2 size = { 16 } />
58- < Typography level = "body-sm" >
59- { metadata ?. duration
60- ? `${ metadata . duration . toFixed ( 1 ) } s`
61- : 'Audio' }
62- </ Typography >
63- </ Box >
75+ { /* Waveform */ }
76+ < Box
77+ ref = { waveformRef }
78+ sx = { {
79+ width : '100%' ,
80+ minHeight : '60px' ,
81+ border : '1px solid' ,
82+ borderColor : 'divider' ,
83+ borderRadius : 'sm' ,
84+ padding : 1 ,
85+ } }
86+ />
6487
6588 { /* File Path Only */ }
6689 { metadata ?. path && (
0 commit comments