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/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", 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', 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; diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index 743e826..7b7f40b 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -139,17 +139,18 @@ 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 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); - 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({ @@ -169,7 +170,8 @@ test('Can encode transparent video', async () => { firstSample.draw(probeContext, 0, 0); imageData = probeContext.getImageData(0, 0, probeCanvas.width, probeCanvas.height); - expect(imageData.data[3]).lessThanOrEqual(2); // Transparent (within error) + + expect(imageData.data[3]).toBe(0); // Transparent }); test('Can encode video with alternating transparency', async () => {