@@ -92,6 +92,7 @@ export interface UseAudioPlaybackReturn {
9292export function useAudioPlayback ( ) : UseAudioPlaybackReturn {
9393 const audioRef = useRef < HTMLAudioElement | null > ( null ) ;
9494 const blobUrlRef = useRef < string | null > ( null ) ;
95+ const abortControllerRef = useRef < AbortController | null > ( null ) ;
9596
9697 const [ state , setState ] = useState < PlaybackState > ( "idle" ) ;
9798 const [ currentTime , setCurrentTime ] = useState ( 0 ) ;
@@ -101,6 +102,8 @@ export function useAudioPlayback(): UseAudioPlaybackReturn {
101102
102103 // Cleanup blob URL and audio element
103104 const cleanup = useCallback ( ( ) => {
105+ abortControllerRef . current ?. abort ( ) ;
106+ abortControllerRef . current = null ;
104107 if ( audioRef . current ) {
105108 audioRef . current . pause ( ) ;
106109 audioRef . current . src = "" ;
@@ -121,35 +124,66 @@ export function useAudioPlayback(): UseAudioPlaybackReturn {
121124 const getAudio = useCallback ( ( ) => {
122125 if ( ! audioRef . current ) {
123126 audioRef . current = new Audio ( ) ;
124-
125- // Set up event listeners
126- audioRef . current . addEventListener ( "timeupdate" , ( ) => {
127- setCurrentTime ( audioRef . current ?. currentTime ?? 0 ) ;
128- } ) ;
129-
130- audioRef . current . addEventListener ( "loadedmetadata" , ( ) => {
131- setDuration ( audioRef . current ?. duration ?? 0 ) ;
132- } ) ;
133-
134- audioRef . current . addEventListener ( "ended" , ( ) => {
135- setState ( "idle" ) ;
136- setCurrentTime ( 0 ) ;
137- } ) ;
138-
139- audioRef . current . addEventListener ( "error" , ( ) => {
140- setState ( "error" ) ;
141- setError ( "Failed to play audio" ) ;
142- } ) ;
143-
144- audioRef . current . addEventListener ( "play" , ( ) => {
145- setState ( "playing" ) ;
146- } ) ;
147-
148- audioRef . current . addEventListener ( "pause" , ( ) => {
149- if ( audioRef . current && ! audioRef . current . ended ) {
150- setState ( "paused" ) ;
151- }
152- } ) ;
127+ abortControllerRef . current = new AbortController ( ) ;
128+ const { signal } = abortControllerRef . current ;
129+
130+ // Throttle timeupdate with requestAnimationFrame
131+ let rafId : number | null = null ;
132+ audioRef . current . addEventListener (
133+ "timeupdate" ,
134+ ( ) => {
135+ if ( rafId !== null ) return ;
136+ rafId = requestAnimationFrame ( ( ) => {
137+ setCurrentTime ( audioRef . current ?. currentTime ?? 0 ) ;
138+ rafId = null ;
139+ } ) ;
140+ } ,
141+ { signal }
142+ ) ;
143+
144+ audioRef . current . addEventListener (
145+ "loadedmetadata" ,
146+ ( ) => {
147+ setDuration ( audioRef . current ?. duration ?? 0 ) ;
148+ } ,
149+ { signal }
150+ ) ;
151+
152+ audioRef . current . addEventListener (
153+ "ended" ,
154+ ( ) => {
155+ setState ( "idle" ) ;
156+ setCurrentTime ( 0 ) ;
157+ } ,
158+ { signal }
159+ ) ;
160+
161+ audioRef . current . addEventListener (
162+ "error" ,
163+ ( ) => {
164+ setState ( "error" ) ;
165+ setError ( "Failed to play audio" ) ;
166+ } ,
167+ { signal }
168+ ) ;
169+
170+ audioRef . current . addEventListener (
171+ "play" ,
172+ ( ) => {
173+ setState ( "playing" ) ;
174+ } ,
175+ { signal }
176+ ) ;
177+
178+ audioRef . current . addEventListener (
179+ "pause" ,
180+ ( ) => {
181+ if ( audioRef . current && ! audioRef . current . ended ) {
182+ setState ( "paused" ) ;
183+ }
184+ } ,
185+ { signal }
186+ ) ;
153187 }
154188 return audioRef . current ;
155189 } , [ ] ) ;
0 commit comments