From 5039c730bf66fa361cdfbcfc1ce40ed0613b45ad Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:00:38 +0100 Subject: [PATCH 1/7] Fix MediaStreamTracks not being stopped inside of the worker (fixes #204) --- dev/convert.html | 2 +- dev/demux.html | 25 ++++++++++++++++++++++--- src/media-source.ts | 12 ++++++++---- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index 53dda3c..bc1f9d1 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -115,7 +115,7 @@ return ctx.canvas; }, */ - width: 300, + //width: 300, //alpha: 'keep', //width: 320, //discard: true, diff --git a/dev/demux.html b/dev/demux.html index 953fa06..2bca413 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,6 +14,24 @@ source: new Mediabunny.BlobSource(file), }); + const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.VideoSampleSink(videoTrack); + + console.log(await videoTrack.getDecoderConfig()) + + const sink2 = new Mediabunny.EncodedPacketSink(videoTrack); + for await (const packet of sink2.packets()) { + console.log(packet.data.join(', '))//, window.findNalUnitsInLengthPrefixed(packet.data, 4).map(x => window.extractNalUnitTypeForAvc(x))); + if (packet.timestamp > 1) break; + } + + //return; + + for await (const sample of sink.samples()) { + console.log(sample); + } + + /* let total = 0; input.source.onread = (start, end) => { total += end - start; @@ -22,6 +40,7 @@ const videoTrack = await input.getPrimaryVideoTrack(); const sink = new Mediabunny.EncodedPacketSink(videoTrack); + */ //console.log(await sink.getPacket(0)); /* @@ -38,7 +57,7 @@ - const stats = await videoTrack.computePacketStats(); + //const stats = await videoTrack.computePacketStats(); /* const sink = new Mediabunny.EncodedPacketSink(videoTrack); @@ -47,9 +66,9 @@ } */ //console.log(await videoTrack.computeDuration()); - console.log("Done", stats, total, file.size) + //console.log("Done", stats, total, file.size) - console.log(input); + //console.log(input); /* const videoTrack = await input.getPrimaryVideoTrack(); diff --git a/src/media-source.ts b/src/media-source.ts index 2ca13a2..c89b24a 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -1159,7 +1159,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { type: 'videoTrack', trackId: this._workerTrackId, track: this._track, - }, [this._track]); + }); this._workerListener = (event: MessageEvent) => { const message = event.data as MediaStreamTrackProcessorWorkerMessage; @@ -1978,17 +1978,19 @@ const mediaStreamTrackProcessorWorkerCode = () => { }); const abortControllers = new Map(); - const stoppedTracks = new Set(); + const activeTracks = new Map(); self.addEventListener('message', (event) => { const message = event.data as MediaStreamTrackProcessorControllerMessage; switch (message.type) { case 'videoTrack': { + activeTracks.set(message.trackId, message.track); + const processor = new MediaStreamTrackProcessor({ track: message.track }); const consumer = new WritableStream({ write: (videoFrame) => { - if (stoppedTracks.has(message.trackId)) { + if (!activeTracks.has(message.trackId)) { videoFrame.close(); return; } @@ -2026,7 +2028,9 @@ const mediaStreamTrackProcessorWorkerCode = () => { abortControllers.delete(message.trackId); } - stoppedTracks.add(message.trackId); + const track = activeTracks.get(message.trackId); + track?.stop(); + activeTracks.delete(message.trackId); sendMessage({ type: 'trackStopped', From 3217490d8157c04959cb9d94656e8180aaf65c7b Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 14:03:54 +0100 Subject: [PATCH 2/7] Fix read worker abort bugs (fixes #197) --- src/source.ts | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/source.ts b/src/source.ts index 2fe74d4..adafa2e 100644 --- a/src/source.ts +++ b/src/source.ts @@ -234,11 +234,19 @@ export class BlobSource extends Source { break; } + if (worker.aborted) { + break; + } + this.onread?.(worker.currentPos, worker.currentPos + value.length); this._orchestrator.supplyWorkerData(worker, value); } else { const data = await this._blob.slice(worker.currentPos, worker.targetPos).arrayBuffer(); + if (worker.aborted) { + break; + } + this.onread?.(worker.currentPos, worker.currentPos + data.byteLength); this._orchestrator.supplyWorkerData(worker, new Uint8Array(data)); } @@ -457,7 +465,7 @@ export class UrlSource extends Source { /** @internal */ private async _runWorker(worker: ReadWorker) { // The outer loop is for resuming a request if it dies mid-response - while (!worker.aborted) { + while (true) { const existing = this._existingResponses.get(worker); this._existingResponses.delete(worker); @@ -534,6 +542,10 @@ export class UrlSource extends Source { } } + if (worker.aborted) { + break; + } + const { done, value } = readResult; if (done) { @@ -552,6 +564,10 @@ export class UrlSource extends Source { this.onread?.(worker.currentPos, worker.currentPos + value.length); this._orchestrator.supplyWorkerData(worker, value); } + + if (worker.aborted) { + break; + } } worker.running = false; @@ -796,6 +812,10 @@ export class StreamSource extends Source { let data = this._options.read(worker.currentPos, originalTargetPos); if (data instanceof Promise) data = await data; + if (worker.aborted) { + break; + } + if (data instanceof Uint8Array) { data = toUint8Array(data); // Normalize things like Node.js Buffer to Uint8Array @@ -833,6 +853,10 @@ export class StreamSource extends Source { throw new TypeError('ReadableStream returned by options.read must yield Uint8Array chunks.'); } + if (worker.aborted) { + break; + } + const data = toUint8Array(value); // Normalize things like Node.js Buffer to Uint8Array this.onread?.(worker.currentPos, worker.currentPos + data.length); @@ -1420,7 +1444,10 @@ class ReadOrchestrator { currentPos: startPos, targetPos, running: false, - aborted: false, + // Due to async shenanigans, it can happen that workers are started after disposal. In this case, instead of + // simply not creating the worker, we allow it to run but immediately label it as aborted, so it can then + // shut itself down. + aborted: this.disposed, pendingSlices: [], age: this.nextAge++, }; @@ -1473,10 +1500,7 @@ class ReadOrchestrator { /** Called by a worker when it has read some data. */ supplyWorkerData(worker: ReadWorker, bytes: Uint8Array) { - if (this.disposed) { - // Writes may still come in after disposal, but we just ignore those - return; - } + assert(!worker.aborted); const start = worker.currentPos; const end = start + bytes.length; From d2d893ff301abd523804f2040c9d1fb183c6df6e Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:36:09 +0100 Subject: [PATCH 3/7] Bump patch --- package-lock.json | 12 ++++++------ package.json | 2 +- packages/mp3-encoder/package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23c1888..76daac5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.24.2", + "version": "1.24.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.24.2", + "version": "1.24.3", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.1.tgz", - "integrity": "sha512-k7ki4KGgFE7F4XaUn0sDpPzdB8Hgms5Hq3QonzXyVSMh75oRuyjNhuAWJtAAlzs45gUerlIHZCvHd6/5H7duow==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.2.tgz", + "integrity": "sha512-+H2Jha7D8C92F1fysdN+nmBBAOgVV+pNv/Pxc+c//qo2E/idnv8MsZrft7VJsFE5WjK30ESkwW5ezM0oceFIkg==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.24.2", + "version": "1.24.3", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index b6b5c5b..98efb29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.24.2", + "version": "1.24.3", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "workspaces": [ diff --git a/packages/mp3-encoder/package.json b/packages/mp3-encoder/package.json index 12c2d76..c5acbf8 100644 --- a/packages/mp3-encoder/package.json +++ b/packages/mp3-encoder/package.json @@ -1,7 +1,7 @@ { "name": "@mediabunny/mp3-encoder", "author": "Vanilagy", - "version": "1.24.2", + "version": "1.24.3", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", From 90d5aed0510469c9ad1d7c8c35e1a0fbbe07eb5a Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:46:24 +0100 Subject: [PATCH 4/7] Test test --- test/browser/transparency.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index 743e826..b2308d6 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -139,7 +139,8 @@ test('Can encode transparent video', async () => { probeContext.drawImage(video, 0, 0); let imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); - expect(imageData.data[3]).lessThanOrEqual(2); // Transparent (within error) + const otherIndex = (100 + 100 * probeCanvas.width) * 4; + expect(imageData.data[otherIndex + 3]).lessThanOrEqual(2); // Transparent (within error) const pos = { x: 300, y: 300 }; // Dead center in the red square const index = (pos.x + pos.y * probeCanvas.width) * 4; From b12fee70a3c6e2946790f2c250407e8868f992b9 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:47:49 +0100 Subject: [PATCH 5/7] Log test --- test/browser/transparency.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index b2308d6..486a612 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -140,6 +140,7 @@ test('Can encode transparent video', async () => { let imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); const otherIndex = (100 + 100 * probeCanvas.width) * 4; + console.log('dayte', imageData.data[otherIndex + 3]); expect(imageData.data[otherIndex + 3]).lessThanOrEqual(2); // Transparent (within error) const pos = { x: 300, y: 300 }; // Dead center in the red square From 68c37b49dc33a119e1637de008c038f609746c62 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:50:49 +0100 Subject: [PATCH 6/7] Another test test --- test/browser/transparency.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index 486a612..98d089f 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -151,6 +151,8 @@ test('Can encode transparent video', async () => { expect(imageData.data[index + 1]).lessThanOrEqual(2); expect(imageData.data[index + 2]).lessThanOrEqual(2); + console.log('the other', imageData.data[index + 3]); + expect(imageData.data[index + 3]).greaterThanOrEqual(253); // Opaque (within error) // Let's also check it's read correctly by Mediabunny @@ -171,6 +173,8 @@ test('Can encode transparent video', async () => { firstSample.draw(probeContext, 0, 0); imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); + + console.log('pish', imageData.data[3]); expect(imageData.data[3]).lessThanOrEqual(2); // Transparent (within error) }); From aad31009fe6698a9de12c1492ed4d97e54a0c192 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:56:16 +0100 Subject: [PATCH 7/7] Make test more strict --- test/browser/transparency.test.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index 98d089f..7b7f40b 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -139,21 +139,18 @@ test('Can encode transparent video', async () => { probeContext.drawImage(video, 0, 0); let imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); - const otherIndex = (100 + 100 * probeCanvas.width) * 4; - console.log('dayte', imageData.data[otherIndex + 3]); - expect(imageData.data[otherIndex + 3]).lessThanOrEqual(2); // Transparent (within error) + const index1 = (100 + 100 * probeCanvas.width) * 4; + expect(imageData.data[index1 + 3]).toBe(0); // Transparent const pos = { x: 300, y: 300 }; // Dead center in the red square - const index = (pos.x + pos.y * probeCanvas.width) * 4; + const index2 = (pos.x + pos.y * probeCanvas.width) * 4; // Red (within error) - expect(imageData.data[index + 0]).greaterThanOrEqual(253); - expect(imageData.data[index + 1]).lessThanOrEqual(2); - expect(imageData.data[index + 2]).lessThanOrEqual(2); + expect(imageData.data[index2 + 0]).greaterThanOrEqual(253); + expect(imageData.data[index2 + 1]).lessThanOrEqual(2); + expect(imageData.data[index2 + 2]).lessThanOrEqual(2); - console.log('the other', imageData.data[index + 3]); - - expect(imageData.data[index + 3]).greaterThanOrEqual(253); // Opaque (within error) + expect(imageData.data[index2 + 3]).toBe(255); // Opaque // Let's also check it's read correctly by Mediabunny using input = new Input({ @@ -174,8 +171,7 @@ test('Can encode transparent video', async () => { imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); - console.log('pish', imageData.data[3]); - expect(imageData.data[3]).lessThanOrEqual(2); // Transparent (within error) + expect(imageData.data[3]).toBe(0); // Transparent }); test('Can encode video with alternating transparency', async () => {