@@ -102,11 +102,13 @@ interface SlidingWaveformProps {
102102/**
103103 * Renders a sliding window of audio amplitude over time.
104104 * New samples appear on the right and scroll left as time passes.
105+ * Falls back to a simple pulsing indicator if Web Audio API fails.
105106 */
106107const SlidingWaveform : React . FC < SlidingWaveformProps > = ( props ) => {
107108 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
108109 const containerRef = useRef < HTMLDivElement > ( null ) ;
109110 const [ containerWidth , setContainerWidth ] = useState ( 600 ) ;
111+ const [ audioError , setAudioError ] = useState ( false ) ;
110112
111113 // Audio analysis state (refs to avoid re-renders)
112114 const audioContextRef = useRef < AudioContext | null > ( null ) ;
@@ -136,24 +138,29 @@ const SlidingWaveform: React.FC<SlidingWaveformProps> = (props) => {
136138 const stream = props . mediaRecorder . stream ;
137139 if ( ! stream ) return ;
138140
139- const audioContext = new AudioContext ( ) ;
140- const analyser = audioContext . createAnalyser ( ) ;
141- analyser . fftSize = 256 ;
142- analyser . smoothingTimeConstant = 0.3 ;
143-
144- const source = audioContext . createMediaStreamSource ( stream ) ;
145- source . connect ( analyser ) ;
146-
147- audioContextRef . current = audioContext ;
148- analyserRef . current = analyser ;
149- samplesRef . current = new Array < number > ( NUM_SAMPLES ) . fill ( 0 ) ;
150- lastSampleTimeRef . current = performance . now ( ) ;
151-
152- return ( ) => {
153- void audioContext . close ( ) ;
154- audioContextRef . current = null ;
155- analyserRef . current = null ;
156- } ;
141+ try {
142+ const audioContext = new AudioContext ( ) ;
143+ const analyser = audioContext . createAnalyser ( ) ;
144+ analyser . fftSize = 256 ;
145+ analyser . smoothingTimeConstant = 0.3 ;
146+
147+ const source = audioContext . createMediaStreamSource ( stream ) ;
148+ source . connect ( analyser ) ;
149+
150+ audioContextRef . current = audioContext ;
151+ analyserRef . current = analyser ;
152+ samplesRef . current = new Array < number > ( NUM_SAMPLES ) . fill ( 0 ) ;
153+ lastSampleTimeRef . current = performance . now ( ) ;
154+
155+ return ( ) => {
156+ void audioContext . close ( ) ;
157+ audioContextRef . current = null ;
158+ analyserRef . current = null ;
159+ } ;
160+ } catch ( err ) {
161+ console . error ( "Failed to initialize audio visualization:" , err ) ;
162+ setAudioError ( true ) ;
163+ }
157164 } , [ props . mediaRecorder ] ) ;
158165
159166 // Animation loop: sample audio amplitude and render bars
@@ -217,9 +224,30 @@ const SlidingWaveform: React.FC<SlidingWaveformProps> = (props) => {
217224
218225 // Run animation loop
219226 useEffect ( ( ) => {
227+ if ( audioError ) return ;
220228 animationFrameRef . current = requestAnimationFrame ( draw ) ;
221229 return ( ) => cancelAnimationFrame ( animationFrameRef . current ) ;
222- } , [ draw ] ) ;
230+ } , [ draw , audioError ] ) ;
231+
232+ // Fallback: simple pulsing indicator if Web Audio API unavailable
233+ if ( audioError ) {
234+ return (
235+ < div className = "flex h-full w-full items-center justify-center gap-1" >
236+ { [ ...Array ( 5 ) ] . map ( ( _ , i ) => (
237+ < div
238+ key = { i }
239+ className = "animate-pulse rounded-full"
240+ style = { {
241+ width : 4 ,
242+ height : 12 + ( i % 3 ) * 4 ,
243+ backgroundColor : props . color ,
244+ animationDelay : `${ i * 100 } ms` ,
245+ } }
246+ />
247+ ) ) }
248+ </ div >
249+ ) ;
250+ }
223251
224252 return (
225253 < div ref = { containerRef } className = "flex h-full w-full items-center justify-center" >
0 commit comments