From 9e203c0c8ad3544b49522e89100c887795ba7666 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:44:58 +0200 Subject: [PATCH] Manually close file readers to avoid indefinite memory leak (fixes #144) --- src/source.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/source.ts b/src/source.ts index 6bab482..fbe0576 100644 --- a/src/source.ts +++ b/src/source.ts @@ -496,6 +496,14 @@ export type BlobSourceOptions = { useStreamReader?: boolean; }; +const blobReaderRegistry = typeof FinalizationRegistry !== 'undefined' + ? new FinalizationRegistry>((reader) => { + // Browsers don't GC readers that aren't "done", which creates indefinite memory leaks, + // see https://github.com/Vanilagy/mediabunny/issues/144#issuecomment-5465467062. So, we need to do it instead. + void reader.cancel().catch(() => {}); + }) + : null; + /** * A source backed by a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). Since a * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) is also a `Blob`, this is the source to use when @@ -510,6 +518,8 @@ export class BlobSource extends Source { _options: BlobSourceOptions; /** @internal */ _orchestrator: ReadOrchestrator; + /** @internal */ + _readers = new WeakMap | null>(); /** * Creates a new {@link BlobSource} backed by the specified @@ -562,9 +572,6 @@ export class BlobSource extends Source { return this._orchestrator.read(start, end, minReadPosition, maxReadPosition); } - /** @internal */ - _readers = new WeakMap | null>(); - /** @internal */ private async _runWorker(worker: ReadWorker) { assert(worker.strictTarget); @@ -578,10 +585,16 @@ export class BlobSource extends Source { // - ReadableStream stalls under backpressure (especially video) // Affects Safari and all iOS browsers (Chrome, Firefox, etc.). // Use arrayBuffer() fallback for WebKit browsers. - if ('stream' in this._blob && !isWebKit() && this._options.useStreamReader !== false) { + if ( + 'stream' in this._blob && !isWebKit() + && this._options.useStreamReader !== false + && blobReaderRegistry // Without it, we cannot guarantee cleanup of abandoned readers + ) { // Get a reader of the blob starting at the required offset, and then keep it around const slice = this._blob.slice(worker.currentPos); reader = slice.stream().getReader(); + + blobReaderRegistry.register(worker, reader); } else { // We'll need to use more primitive ways reader = null;