diff --git a/src/fetcher.ts b/src/fetcher.ts index dc743578a..ac9c07be0 100644 --- a/src/fetcher.ts +++ b/src/fetcher.ts @@ -4,6 +4,8 @@ async function watchProgress(response: Response, progressCallback: (percentage: const contentLength = Number(response.headers.get('Content-Length')) || 0 let receivedLength = 0 + const maxIterations = 100000 // Safety limit to prevent infinite loops + let iterations = 0 // Process the data const processChunk = async (value: Uint8Array | undefined) => { @@ -14,6 +16,12 @@ async function watchProgress(response: Response, progressCallback: (percentage: } const read = async () => { + // Safety check to prevent infinite recursion + if (iterations++ > maxIterations) { + console.error('Fetcher: Maximum iterations reached, stopping read loop') + return + } + let data try { data = await reader.read() diff --git a/src/plugins/record.ts b/src/plugins/record.ts index d18cc61ad..1ed89aea0 100644 --- a/src/plugins/record.ts +++ b/src/plugins/record.ts @@ -67,6 +67,7 @@ class RecordPlugin extends BasePlugin { private micStream: MicStream | null = null private unsubscribeDestroy?: () => void private unsubscribeRecordEnd?: () => void + private recordedBlobUrl: string | null = null /** Create an instance of the Record plugin */ constructor(options: RecordPluginOptions) { @@ -213,6 +214,11 @@ class RecordPlugin extends BasePlugin { /** Request access to the microphone and start monitoring incoming audio */ public async startMic(options?: RecordPluginDeviceOptions): Promise { + // Stop previous mic stream if exists to clean up AudioContext + if (this.micStream) { + this.stopMic() + } + let stream: MediaStream try { stream = await navigator.mediaDevices.getUserMedia({ @@ -272,7 +278,12 @@ class RecordPlugin extends BasePlugin { this.emit(ev, blob) if (this.options.renderRecordedAudio) { this.applyOriginalOptionsIfNeeded() - this.wavesurfer?.load(URL.createObjectURL(blob)) + // Revoke previous blob URL before creating a new one + if (this.recordedBlobUrl) { + URL.revokeObjectURL(this.recordedBlobUrl) + } + this.recordedBlobUrl = URL.createObjectURL(blob) + this.wavesurfer?.load(this.recordedBlobUrl) } } @@ -355,6 +366,11 @@ class RecordPlugin extends BasePlugin { super.destroy() this.stopRecording() this.stopMic() + // Revoke blob URL to free memory + if (this.recordedBlobUrl) { + URL.revokeObjectURL(this.recordedBlobUrl) + this.recordedBlobUrl = null + } } private applyOriginalOptionsIfNeeded() { diff --git a/src/plugins/regions.ts b/src/plugins/regions.ts index cebfdd3ea..3e2f77649 100644 --- a/src/plugins/regions.ts +++ b/src/plugins/regions.ts @@ -102,6 +102,8 @@ class SingleRegion extends EventEmitter implements Region { public subscriptions: (() => void)[] = [] public updatingSide?: UpdateSide = undefined private isRemoved = false + private contentClickListener?: (e: MouseEvent) => void + private contentBlurListener?: () => void constructor( params: RegionParams, @@ -218,7 +220,7 @@ class SingleRegion extends EventEmitter implements Region { let elementTop = 0 let elementHeight = 100 - if (this.channelIdx >= 0 && this.channelIdx < this.numberOfChannels) { + if (this.channelIdx >= 0 && this.numberOfChannels > 0 && this.channelIdx < this.numberOfChannels) { elementHeight = 100 / this.numberOfChannels elementTop = elementHeight * this.channelIdx } @@ -284,8 +286,10 @@ class SingleRegion extends EventEmitter implements Region { ) if (this.contentEditable && this.content) { - this.content.addEventListener('click', (e) => this.onContentClick(e)) - this.content.addEventListener('blur', () => this.onContentBlur()) + this.contentClickListener = (e) => this.onContentClick(e) + this.contentBlurListener = () => this.onContentBlur() + this.content.addEventListener('click', this.contentClickListener) + this.content.addEventListener('blur', this.contentBlurListener) } } @@ -375,6 +379,16 @@ class SingleRegion extends EventEmitter implements Region { public setContent(content: RegionParams['content']) { if (!this.element) return + // Remove event listeners from old content before removing it + if (this.content && this.contentEditable) { + if (this.contentClickListener) { + this.content.removeEventListener('click', this.contentClickListener) + } + if (this.contentBlurListener) { + this.content.removeEventListener('blur', this.contentBlurListener) + } + } + this.content?.remove() if (!content) { this.content = undefined @@ -394,6 +408,11 @@ class SingleRegion extends EventEmitter implements Region { } if (this.contentEditable) { this.content.contentEditable = 'true' + // Re-add event listeners to new content + this.contentClickListener = (e) => this.onContentClick(e) + this.contentBlurListener = () => this.onContentBlur() + this.content.addEventListener('click', this.contentClickListener) + this.content.addEventListener('blur', this.contentBlurListener) } this.content.setAttribute('part', 'region-content') this.element.appendChild(this.content) diff --git a/src/renderer.ts b/src/renderer.ts index 311fc70f6..c33fe7c69 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -297,7 +297,10 @@ class Renderer extends EventEmitter { destroy() { this.subscriptions.forEach((unsubscribe) => unsubscribe()) this.container.remove() - this.resizeObserver?.disconnect() + if (this.resizeObserver) { + this.resizeObserver.disconnect() + this.resizeObserver = null + } this.unsubscribeOnScroll?.forEach((unsubscribe) => unsubscribe()) this.unsubscribeOnScroll = [] } @@ -329,6 +332,7 @@ class Renderer extends EventEmitter { // Convert array of color values to linear gradient private convertColorValues(color?: WaveSurferOptions['waveColor']): string | CanvasGradient { if (!Array.isArray(color)) return color || '' + if (color.length === 0) return '#999' // Return default color for empty array if (color.length < 2) return color[0] || '' const canvasElement = document.createElement('canvas') diff --git a/src/wavesurfer.ts b/src/wavesurfer.ts index 22f892d5d..ca0db0b7b 100644 --- a/src/wavesurfer.ts +++ b/src/wavesurfer.ts @@ -200,7 +200,10 @@ class WaveSurfer extends Player { if (initialUrl || (peaks && duration)) { // Swallow async errors because they cannot be caught from a constructor call. // Subscribe to the wavesurfer's error event to handle them. - this.load(initialUrl, peaks, duration).catch(() => null) + this.load(initialUrl, peaks, duration).catch((err) => { + // Log error for debugging while still emitting error event + console.error('WaveSurfer initial load error:', err) + }) } }) } @@ -318,34 +321,38 @@ class WaveSurfer extends Player { // Drag { - let debounce: ReturnType - this.subscriptions.push( - this.renderer.on('drag', (relativeX) => { - if (!this.options.interact) return - - // Update the visual position - this.renderer.renderProgress(relativeX) - - // Set the audio position with a debounce - clearTimeout(debounce) - let debounceTime - - if (this.isPlaying()) { - debounceTime = 0 - } else if (this.options.dragToSeek === true) { - debounceTime = 200 - } else if (typeof this.options.dragToSeek === 'object' && this.options.dragToSeek !== undefined) { - debounceTime = this.options.dragToSeek['debounceTime'] - } + let debounce: ReturnType | undefined + const unsubscribeDrag = this.renderer.on('drag', (relativeX) => { + if (!this.options.interact) return + + // Update the visual position + this.renderer.renderProgress(relativeX) + + // Set the audio position with a debounce + clearTimeout(debounce) + let debounceTime + + if (this.isPlaying()) { + debounceTime = 0 + } else if (this.options.dragToSeek === true) { + debounceTime = 200 + } else if (typeof this.options.dragToSeek === 'object' && this.options.dragToSeek !== undefined) { + debounceTime = this.options.dragToSeek['debounceTime'] + } - debounce = setTimeout(() => { - this.seekTo(relativeX) - }, debounceTime) + debounce = setTimeout(() => { + this.seekTo(relativeX) + }, debounceTime) - this.emit('interaction', relativeX * this.getDuration()) - this.emit('drag', relativeX) - }), - ) + this.emit('interaction', relativeX * this.getDuration()) + this.emit('drag', relativeX) + }) + + // Clear debounce timeout on destroy + this.subscriptions.push(() => { + clearTimeout(debounce) + unsubscribeDrag() + }) } }