Skip to content

Commit a0e453e

Browse files
committed
Fix audio event listener leak and throttle timeupdate
1 parent 15e30c7 commit a0e453e

1 file changed

Lines changed: 63 additions & 29 deletions

File tree

ui/src/hooks/useAudioPlayback.ts

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export interface UseAudioPlaybackReturn {
9292
export 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

Comments
 (0)