diff --git a/src/preload/index.ts b/src/preload/index.ts index 3d9af79af3e..f8b7a9f3bb6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -266,9 +266,19 @@ let cachedNotificationSound: { audio: HTMLAudioElement } | null = null let isNotificationSoundPlaying = false +// Why: audio.play() can reject before ended/error fires; keep a cleanup hook +// so failed or replaced plays do not accumulate listeners on the cached Audio. +let cleanupNotificationSoundPlayback: (() => void) | null = null + +function clearNotificationSoundPlaybackState(): void { + cleanupNotificationSoundPlayback?.() + cleanupNotificationSoundPlayback = null + isNotificationSoundPlaying = false +} function disposeCachedNotificationSound(): void { if (cachedNotificationSound) { + clearNotificationSoundPlaybackState() cachedNotificationSound.audio.pause() cachedNotificationSound.audio.src = '' URL.revokeObjectURL(cachedNotificationSound.blobUrl) @@ -1549,11 +1559,21 @@ const api = { audio.volume = Math.min(1, Math.max(0, options.volume / 100)) } isNotificationSoundPlaying = true + cleanupNotificationSoundPlayback?.() const release = (): void => { + cleanup() + if (cleanupNotificationSoundPlayback === cleanup) { + cleanupNotificationSoundPlayback = null + } isNotificationSoundPlaying = false } - audio.addEventListener('ended', release, { once: true }) - audio.addEventListener('error', release, { once: true }) + const cleanup = (): void => { + audio.removeEventListener('ended', release) + audio.removeEventListener('error', release) + } + cleanupNotificationSoundPlayback = cleanup + audio.addEventListener('ended', release) + audio.addEventListener('error', release) try { await audio.play() } catch { @@ -1562,7 +1582,7 @@ const api = { } return { played: true } } catch { - isNotificationSoundPlaying = false + clearNotificationSoundPlaybackState() return { played: false, reason: 'playback-failed' } } }