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 () => {