From 3a4153f35b13ae451fb2fa2d9905efc0cccff80d Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:48:04 +0200 Subject: [PATCH 1/4] Add Discord badge --- README.md | 1 + docs/llms.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53a5c3b..6b268e2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![](https://img.shields.io/npm/v/mediabunny)](https://www.npmjs.com/package/mediabunny) [![](https://img.shields.io/bundlephobia/minzip/mediabunny)](https://bundlephobia.com/package/mediabunny) [![](https://img.shields.io/npm/dm/mediabunny)](https://www.npmjs.com/package/mediabunny) +[![](https://img.shields.io/discord/1390044844285497344?logo=discord&label=Discord)](https://discord.gg/hmpkyYuS4U)
diff --git a/docs/llms.md b/docs/llms.md index 6fd3d9d..c01c4d2 100644 --- a/docs/llms.md +++ b/docs/llms.md @@ -7,7 +7,7 @@
-While Mediabunny is proudly human-coded, we want to encourage any and all usage of Mediabunny, even when the vibes are high. +While Mediabunny is proudly human-generated, we want to encourage any and all usage of Mediabunny, even when the vibes are high. Mediabunny is still new and is unlikely to be in the training data of modern LLMs, but we can still make the AI perform extremely well by just giving it a little more context. From 68a6202269070a085b7468accc079d10c4007320 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:58:04 +0200 Subject: [PATCH 2/4] Small fixup --- src/sample.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sample.ts b/src/sample.ts index 5f407cd..1e2947a 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -114,11 +114,11 @@ export class VideoSample { * in `init`. */ constructor( - data: BufferSource, + data: AllowSharedBufferSource, init: SetRequired ); constructor( - data: VideoFrame | CanvasImageSource | BufferSource, + data: VideoFrame | CanvasImageSource | AllowSharedBufferSource, init?: VideoSampleInit, ) { if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) { From 4860a4bfe7a6c8dbea9e81a28078b6a411791733 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:11:04 +0200 Subject: [PATCH 3/4] Probe encoder support with sample (fixes #90), improve encoder error handling --- dev/convert.html | 13 ++++++------ src/conversion.ts | 50 +++++++++++++++++++++++++++++++++++++-------- src/media-source.ts | 24 ++++++++++++++-------- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index fd662fd..5558bab 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -18,15 +18,13 @@ const file = fileInput.files[0]; const source = new Mediabunny.BlobSource(file); - const target = new Mediabunny.NullTarget() ?? new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({ + const target = new Mediabunny.BufferTarget() ?? new Mediabunny.StreamTarget(new WritableStream({ write: console.log }), { chunked: true, chunkSize: 2**20 }); - const outputFormat = new Mediabunny.Mp4OutputFormat({ - onMoov: console.log - }); + const outputFormat = new Mediabunny.Mp4OutputFormat({}); const button = document.createElement('button'); button.textContent = 'Cancel'; @@ -76,7 +74,8 @@ }, */ video: () => ({ - //codec: 'avc', + forceTranscode: true, + codec: 'avc', //fit: 'contain', //frameRate: 27.123, //width: 320, @@ -98,8 +97,8 @@ //height: 100, }), trim: { - //start: 0, - //end: 20 + start: 0, + end: 5 }, }); console.log(conversion); diff --git a/src/conversion.ts b/src/conversion.ts index c3c1a0a..9738d56 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -46,7 +46,9 @@ import { Rotation, } from './misc'; import { Output, TrackType } from './output'; +import { Mp4OutputFormat } from './output-format'; import { AudioSample, VideoSample } from './sample'; +import { NullTarget } from './target'; /** * The options for media file conversion. @@ -582,7 +584,7 @@ export class Conversion { || this._startTimestamp > 0 || firstTimestamp < 0 || !!trackOptions.frameRate; - const needsRerender = width !== originalWidth + let needsRerender = width !== originalWidth || height !== originalHeight || (totalRotation !== 0 && !outputSupportsRotation); @@ -662,6 +664,43 @@ export class Conversion { const source = new VideoSampleSource(encodingConfig); videoSource = source; + if (!needsRerender) { + // If we're directly passing decoded samples back to the encoder, sometimes the encoder may error due + // to lack of support of certain video frame formats, like when HDR is at play. To check for this, we + // first try to pass a single frame to the encoder to see how it behaves. If it throws, we then fall + // back to the rerender path. + // + // Creating a new temporary Output is sort of hacky, but due to a lack of an isolated encoder API right + // now, this is the simplest way. Will refactor in the future! + + const tempOutput = new Output({ + format: new Mp4OutputFormat(), // Supports all video codecs + target: new NullTarget(), + }); + + const tempSource = new VideoSampleSource(encodingConfig); + tempOutput.addVideoTrack(tempSource); + + await tempOutput.start(); + + const sink = new VideoSampleSink(track); + const firstSample = await sink.getSample(this._startTimestamp); + + if (firstSample) { + try { + await tempSource.add(firstSample); + firstSample.close(); + await tempOutput.finalize(); + } catch (error) { + console.info('Error when probing encoder support. Falling back to rerender path.', error); + needsRerender = true; + void tempOutput.cancel(); + } + } else { + await tempOutput.cancel(); + } + } + if (needsRerender) { this._trackPromises.push((async () => { await this._started; @@ -1084,13 +1123,8 @@ export class Conversion { this._maxTimestamps.set(trackId, Math.max(endTimestamp, this._maxTimestamps.get(trackId) ?? -Infinity)); - let totalTimestamps = 0; - for (const [, timestamp] of this._maxTimestamps) { - totalTimestamps += timestamp; - } - - const averageTimestamp = totalTimestamps / this._totalTrackCount; - const newProgress = clamp(averageTimestamp / this._totalDuration, 0, 1); + const minTimestamp = Math.min(...this._maxTimestamps.values()); + const newProgress = clamp(minTimestamp / this._totalDuration, 0, 1); if (newProgress !== this._lastProgress) { this._lastProgress = newProgress; diff --git a/src/media-source.ts b/src/media-source.ts index 89b5485..b72c791 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -353,6 +353,7 @@ class VideoEncoderWrapper { return; } + const encoderError = new Error(); return this.ensureEncoderPromise = (async () => { const encoderConfig = buildVideoEncoderConfig({ width: videoSample.codedWidth, @@ -411,7 +412,7 @@ class VideoEncoderWrapper { void this.muxer!.addEncodedVideoPacket(this.source._connectedTrack!, packet, meta); }, error: (error) => { - error.stack = new Error().stack; // Provide a more useful stack trace + error.stack = encoderError.stack; // Provide a more useful stack trace this.encoderError ??= error; }, }); @@ -426,7 +427,7 @@ class VideoEncoderWrapper { } async flushAndClose(forceClose: boolean) { - this.checkForEncoderError(); + if (!forceClose) this.checkForEncoderError(); if (this.customEncoder) { if (!forceClose) { @@ -439,10 +440,12 @@ class VideoEncoderWrapper { await this.encoder.flush(); } - this.encoder.close(); + if (this.encoder.state !== 'closed') { + this.encoder.close(); + } } - this.checkForEncoderError(); + if (!forceClose) this.checkForEncoderError(); } getQueueSize() { @@ -455,6 +458,7 @@ class VideoEncoderWrapper { checkForEncoderError() { if (this.encoderError) { + this.encoderError.stack = new Error().stack; // Provide an even more useful stack trace throw this.encoderError; } } @@ -990,6 +994,7 @@ class AudioEncoderWrapper { return; } + const encoderError = new Error(); return this.ensureEncoderPromise = (async () => { const { numberOfChannels, sampleRate } = audioSample; @@ -1050,7 +1055,7 @@ class AudioEncoderWrapper { void this.muxer!.addEncodedAudioPacket(this.source._connectedTrack!, packet, meta); }, error: (error) => { - error.stack = new Error().stack; // Provide a more useful stack trace + error.stack = encoderError.stack; // Provide a more useful stack trace this.encoderError ??= error; }, }); @@ -1158,7 +1163,7 @@ class AudioEncoderWrapper { } async flushAndClose(forceClose: boolean) { - this.checkForEncoderError(); + if (!forceClose) this.checkForEncoderError(); if (this.customEncoder) { if (!forceClose) { @@ -1171,10 +1176,12 @@ class AudioEncoderWrapper { await this.encoder.flush(); } - this.encoder.close(); + if (this.encoder.state !== 'closed') { + this.encoder.close(); + } } - this.checkForEncoderError(); + if (!forceClose) this.checkForEncoderError(); } getQueueSize() { @@ -1189,6 +1196,7 @@ class AudioEncoderWrapper { checkForEncoderError() { if (this.encoderError) { + this.encoderError.stack = new Error().stack; // Provide an even more useful stack trace throw this.encoderError; } } From 605c258029be1b6e12d252eede58eaf6620910c9 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:14:05 +0200 Subject: [PATCH 4/4] 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 21b6dba..80fe289 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.13.2", + "version": "1.13.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.13.2", + "version": "1.13.3", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.1.tgz", - "integrity": "sha512-Fht/ujfoS0jt8s9W4zDEilnWVsafkBaiIQ5BX0x1tdvM4PClexgYIWDZSf6kC82/c9Bo3ehZPIIfS3aJ9younA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.13.2.tgz", + "integrity": "sha512-quUwxiyA76+iAh2REQAFifHNthgVqzET37ys6v+GC8ljJpOXuP/g/XNsITc+/6r18gGuYQyF/9C4T/CeiTf7gA==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.13.2", + "version": "1.13.3", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 5248f65..01f6d9a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.13.2", + "version": "1.13.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 258c10f..8a85fe7 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.13.2", + "version": "1.13.3", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",