From fe4eebd4878129d26ffd2fe63b7151c83487b795 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 5 Nov 2025 10:54:03 +0100 Subject: [PATCH 01/11] Increase tolerance of pixels by 1 in tests --- test/browser/transparency.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/browser/transparency.test.ts b/test/browser/transparency.test.ts index e460e55..35527ae 100644 --- a/test/browser/transparency.test.ts +++ b/test/browser/transparency.test.ts @@ -148,8 +148,8 @@ test('Can encode transparent video', async () => { // Red (within error) expect(imageData.data[index2 + 0]).greaterThanOrEqual(253); - expect(imageData.data[index2 + 1]).lessThanOrEqual(2); - expect(imageData.data[index2 + 2]).lessThanOrEqual(2); + expect(imageData.data[index2 + 1]).lessThanOrEqual(3); + expect(imageData.data[index2 + 2]).lessThanOrEqual(3); expect(imageData.data[index2 + 3]).toBe(255); // Opaque From 3c8d5031582d971789cda569b211d2487d03e907 Mon Sep 17 00:00:00 2001 From: Jonny Burger Date: Thu, 6 Nov 2025 09:00:37 +0000 Subject: [PATCH 02/11] Merge pull request #209 from JonnyBurger/flac-consecutive-headers Ensure FLAC headers are consecutive --- src/flac/flac-demuxer.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/flac/flac-demuxer.ts b/src/flac/flac-demuxer.ts index bf71ee4..5248394 100644 --- a/src/flac/flac-demuxer.ts +++ b/src/flac/flac-demuxer.ts @@ -339,16 +339,34 @@ export class FlacDemuxer extends Demuxer { slice.skip(-2); const lengthIfNextFlacFrameHeaderIsLegit = slice.filePos - startPos; - const nextIsLegit = this.readFlacFrameHeader({ + const nextFrameHeader = this.readFlacFrameHeader({ slice, isFirstPacket: false, }); - if (!nextIsLegit) { + if (!nextFrameHeader) { slice.skip(-1); continue; } + // Ensure the frameOrSampleNum is consecutive. + // https://github.com/Vanilagy/mediabunny/issues/194 + + if (this.blockingBit === 0) { + // Case A: If the stream is fixed block size, this is the frame number, which increments by 1 + if (nextFrameHeader.num - frameHeader.num !== 1) { + slice.skip(-1); + continue; + } + } else { + // Case B: If the stream is variable block size, this is the sample number, which increments by + // amount of samples in a frame. + if (nextFrameHeader.num - frameHeader.num !== frameHeader.blockSize) { + slice.skip(-1); + continue; + } + } + return { num: frameHeader.num, blockSize: frameHeader.blockSize, @@ -442,6 +460,11 @@ export class FlacDemuxer extends Demuxer { return null; } + if (sampleRate !== this.audioInfo.sampleRate) { + // This cannot be a valid FLAC frame, the sample rate is not the same as in the stream info + return null; + } + const size = slice.filePos - startOffset; const crc = readU8(slice); From 21cff3283190698e69b703df3a4ccc37ae23464a Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:02:23 +0100 Subject: [PATCH 03/11] 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 76daac5..a00879d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.24.3", + "version": "1.24.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.24.3", + "version": "1.24.4", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.2.tgz", - "integrity": "sha512-+H2Jha7D8C92F1fysdN+nmBBAOgVV+pNv/Pxc+c//qo2E/idnv8MsZrft7VJsFE5WjK30ESkwW5ezM0oceFIkg==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.3.tgz", + "integrity": "sha512-uQu5lXg6c3z2YUKLimAsem1qYPdGfGziQ5CKaAMJAs0C0NNw5T/Zkn0foPML0gWqQJ5ZeC0wTgCxpiBQY8m1Tw==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.24.3", + "version": "1.24.4", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index 98efb29..f8fa949 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.24.3", + "version": "1.24.4", "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 c5acbf8..f4840ff 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.3", + "version": "1.24.4", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", From 5aabf6350a28a57896f6ad67579692b21f6db6b0 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:31:02 +0100 Subject: [PATCH 04/11] Clarify docs --- src/packet.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/packet.ts b/src/packet.ts index c71890c..f5de60e 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -126,7 +126,10 @@ export class EncodedPacket { } } - /** If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. */ + /** + * If this packet is a metadata-only packet. Metadata-only packets don't contain their packet data. They are the + * result of retrieving packets with {@link PacketRetrievalOptions.metadataOnly} set to `true`. + */ get isMetadataOnly() { return this.data === PLACEHOLDER_DATA; } From e0dda582b159894fda4c1326f5915e8e6117d838 Mon Sep 17 00:00:00 2001 From: hex Date: Fri, 7 Nov 2025 16:58:20 -0700 Subject: [PATCH 05/11] fix webkit video conversion bug --- src/misc.ts | 22 ++++++++++++++++++++++ src/source.ts | 9 ++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/misc.ts b/src/misc.ts index 6bdb52b..0ec4458 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -676,6 +676,28 @@ export const isSafari = () => { return result; }; +let isWebKitCache: boolean | null = null; +export const isWebKit = () => { + if (isWebKitCache !== null) return isWebKitCache; + if (typeof navigator === 'undefined') return (isWebKitCache = false); + + const ua = navigator.userAgent || ''; + const maxTouchPoints = navigator.maxTouchPoints || 0; + + // iOS/iPadOS detection: + // - All iOS/iPadOS browsers use WebKit (WKWebView) + // - iPadOS 13+ can report "Macintosh" in UA; detect via touch points + const isIOSLike + = /iPhone|iPad|iPod/i.test(ua) || (/Macintosh/i.test(ua) && maxTouchPoints > 1); + + // On iOS/iPadOS: always WebKit (even if UA says CriOS/Edg/OPR/etc.) + if (isIOSLike) return (isWebKitCache = true); + + // Off iOS: only Safari is WebKit on mainstream desktops + const result = isSafari(); + return (isWebKitCache = result); +}; + let isFirefoxCache: boolean | null = null; export const isFirefox = () => { if (isFirefoxCache !== null) { diff --git a/src/source.ts b/src/source.ts index adafa2e..91fe2b2 100644 --- a/src/source.ts +++ b/src/source.ts @@ -12,6 +12,7 @@ import { binarySearchLessOrEqual, closedIntervalsOverlap, isNumber, + isWebKit, MaybePromise, mergeRequestInit, promiseWithResolvers, @@ -209,7 +210,13 @@ export class BlobSource extends Source { private async _runWorker(worker: ReadWorker) { let reader = this._readers.get(worker); if (reader === undefined) { - if ('stream' in this._blob) { + // WebKit has critical bugs with blob.stream(): + // - WebKitBlobResource error 1 when streaming large files + // - Memory buildup and reload loops on iOS (network process crashes) + // - 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()) { // 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(); From d969266662da0157e03e2bb1551aa4f2628e0c71 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:30:26 +0100 Subject: [PATCH 06/11] Simplify isWebKit, get rid of isSafari --- src/media-sink.ts | 8 ++++---- src/misc.ts | 41 +++++++---------------------------------- src/source.ts | 1 + 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/src/media-sink.ts b/src/media-sink.ts index 1d0665a..195ff2a 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -26,7 +26,7 @@ import { insertSorted, isFirefox, isNumber, - isSafari, + isWebKit, last, mapAsyncGenerator, promiseWithResolvers, @@ -918,7 +918,7 @@ class VideoDecoderWrapper extends DecoderWrapper { } else { assert(this.decoder); - if (!isSafari()) { + if (!isWebKit()) { insertSorted(this.inputTimestamps, packet.timestamp, x => x); } @@ -1049,7 +1049,7 @@ class VideoDecoderWrapper extends DecoderWrapper { /** Handler for the WebCodecs VideoDecoder for ironing out browser differences. */ sampleHandler(sample: VideoSample) { - if (isSafari()) { + if (isWebKit()) { // For correct B-frame handling, we don't just hand over the frames directly but instead add them to // a queue, because we want to ensure frames are emitted in presentation order. We flush the queue // each time we receive a frame with a timestamp larger than the highest we've seen so far, as we @@ -1137,7 +1137,7 @@ class VideoDecoderWrapper extends DecoderWrapper { this.alphaRaslSkipped = false; } - if (isSafari()) { + if (isWebKit()) { for (const sample of this.sampleQueue) { this.finalizeAndEmitSample(sample); } diff --git a/src/misc.ts b/src/misc.ts index 0ec4458..c6f5119 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -658,44 +658,17 @@ export class CallSerializer { } } -let isSafariCache: boolean | null = null; -export const isSafari = () => { - if (isSafariCache !== null) { - return isSafariCache; - } - - const result = !!( - typeof navigator !== 'undefined' - && navigator.vendor?.match(/apple/i) - && !navigator.userAgent?.match(/crios/i) - && !navigator.userAgent?.match(/fxios/i) - && !navigator.userAgent?.match(/Opera|OPT\//) - ); - - isSafariCache = result; - return result; -}; - let isWebKitCache: boolean | null = null; export const isWebKit = () => { - if (isWebKitCache !== null) return isWebKitCache; - if (typeof navigator === 'undefined') return (isWebKitCache = false); + if (isWebKitCache !== null) { + return isWebKitCache; + } - const ua = navigator.userAgent || ''; - const maxTouchPoints = navigator.maxTouchPoints || 0; + // This even returns true for WebKit-wrapping browsers such as Chrome on iOS + const result = !!(typeof navigator !== 'undefined' && navigator.vendor?.match(/apple/i)); - // iOS/iPadOS detection: - // - All iOS/iPadOS browsers use WebKit (WKWebView) - // - iPadOS 13+ can report "Macintosh" in UA; detect via touch points - const isIOSLike - = /iPhone|iPad|iPod/i.test(ua) || (/Macintosh/i.test(ua) && maxTouchPoints > 1); - - // On iOS/iPadOS: always WebKit (even if UA says CriOS/Edg/OPR/etc.) - if (isIOSLike) return (isWebKitCache = true); - - // Off iOS: only Safari is WebKit on mainstream desktops - const result = isSafari(); - return (isWebKitCache = result); + isWebKitCache = result; + return result; }; let isFirefoxCache: boolean | null = null; diff --git a/src/source.ts b/src/source.ts index 91fe2b2..68ebac1 100644 --- a/src/source.ts +++ b/src/source.ts @@ -210,6 +210,7 @@ export class BlobSource extends Source { private async _runWorker(worker: ReadWorker) { let reader = this._readers.get(worker); if (reader === undefined) { + // https://github.com/Vanilagy/mediabunny/issues/184 // WebKit has critical bugs with blob.stream(): // - WebKitBlobResource error 1 when streaming large files // - Memory buildup and reload loops on iOS (network process crashes) From fd8c74d4db4a5566d55b2add5a5c8622ca33fe38 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:44:35 +0100 Subject: [PATCH 07/11] Fix invalid encoder AAC description on WebKit (fixes #206) --- src/codec.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++ src/media-source.ts | 30 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/codec.ts b/src/codec.ts index 2e90cfd..a396e70 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -621,6 +621,54 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS }; }; +export const buildAacAudioSpecificConfig = (config: { + objectType: number; + sampleRate: number; + numberOfChannels: number; +}) => { + let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate); + let customSampleRate: number | null = null; + + if (frequencyIndex === -1) { + frequencyIndex = 15; + customSampleRate = config.sampleRate; + } + + const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels); + if (channelConfiguration === -1) { + throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`); + } + + let bitCount = 5 + 4 + 4; + if (config.objectType >= 32) { + bitCount += 6; + } + if (frequencyIndex === 15) { + bitCount += 24; + } + + const byteCount = Math.ceil(bitCount / 8); + const bytes = new Uint8Array(byteCount); + const bitstream = new Bitstream(bytes); + + if (config.objectType < 32) { + bitstream.writeBits(5, config.objectType); + } else { + bitstream.writeBits(5, 31); + bitstream.writeBits(6, config.objectType - 32); + } + + bitstream.writeBits(4, frequencyIndex); + + if (frequencyIndex === 15) { + bitstream.writeBits(24, customSampleRate!); + } + + bitstream.writeBits(4, channelConfiguration); + + return bytes; +}; + export const OPUS_SAMPLE_RATE = 48_000; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; diff --git a/src/media-source.ts b/src/media-source.ts index c89b24a..86989a2 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -9,6 +9,8 @@ import { AUDIO_CODECS, AudioCodec, + buildAacAudioSpecificConfig, + parseAacAudioSpecificConfig, parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, @@ -24,9 +26,11 @@ import { CallSerializer, clamp, isFirefox, + last, promiseWithResolvers, setInt24, setUint24, + toUint8Array, } from './misc'; import { Muxer } from './muxer'; import { SubtitleParser } from './subtitles'; @@ -1515,6 +1519,32 @@ class AudioEncoderWrapper { this.encoder = new AudioEncoder({ output: (chunk, meta) => { + // WebKit emits an invalid description for AAC (https://bugs.webkit.org/show_bug.cgi?id=302253), + // which we try to detect here. If detected, we'll provide our own description instead, derived + // from the codec string and audio parameters. + if (this.encodingConfig.codec === 'aac' && meta?.decoderConfig) { + let needsDescriptionOverwrite = false; + if (!meta.decoderConfig.description || meta.decoderConfig.description.byteLength < 2) { + needsDescriptionOverwrite = true; + } else { + const audioSpecificConfig = parseAacAudioSpecificConfig( + toUint8Array(meta.decoderConfig.description), + ); + + needsDescriptionOverwrite = audioSpecificConfig.objectType === 0; + } + + if (needsDescriptionOverwrite) { + const objectType = Number(last(encoderConfig.codec.split('.'))); + + meta.decoderConfig.description = buildAacAudioSpecificConfig({ + objectType, + numberOfChannels: meta.decoderConfig.numberOfChannels, + sampleRate: meta.decoderConfig.sampleRate, + }); + } + } + const packet = EncodedPacket.fromEncodedChunk(chunk); this.encodingConfig.onEncodedPacket?.(packet, meta); From c0da28fb6c8aba0614bf852bd45c9558d3e2cef6 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:14:25 +0100 Subject: [PATCH 08/11] Floor Matroska default packet duration (fixes #170) --- dev/demux.html | 26 ++++++++++++++++++++++++++ src/matroska/matroska-demuxer.ts | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dev/demux.html b/dev/demux.html index 2bca413..d53045c 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -15,6 +15,31 @@ }); const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + + for await (const packet of sink.packets()) { + console.log(packet.timestamp, packet.duration, packet.timestamp + packet.duration); + } + + /* + const sink = new Mediabunny.VideoSampleSink(videoTrack); + + for await (const sample of sink.samples()) { + console.log(sample.timestamp, sample.duration); + sample.close(); + } + */ + + /* + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + + for await (const packet of sink.packets()) { + console.log(packet.timestamp, packet.timestamp + packet.duration); + } + */ + + + /* const sink = new Mediabunny.VideoSampleSink(videoTrack); console.log(await videoTrack.getDecoderConfig()) @@ -30,6 +55,7 @@ for await (const sample of sink.samples()) { console.log(sample); } + */ /* let total = 0; diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index c9655d5..79c84f4 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -1181,8 +1181,9 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.DefaultDuration: { if (!this.currentTrack) break; + // Floored to reduce chance of overlapping packets (https://github.com/Vanilagy/mediabunny/issues/170) this.currentTrack.defaultDuration - = this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9; + = Math.floor(this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9); }; break; case EBMLId.Name: { From 6022781e8c518a1fd5776711bb4dbea4b31ae5b1 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:53:28 +0100 Subject: [PATCH 09/11] 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 a00879d..271da7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.24.4", + "version": "1.24.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.24.4", + "version": "1.24.5", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.24.3", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.3.tgz", - "integrity": "sha512-uQu5lXg6c3z2YUKLimAsem1qYPdGfGziQ5CKaAMJAs0C0NNw5T/Zkn0foPML0gWqQJ5ZeC0wTgCxpiBQY8m1Tw==", + "version": "1.24.4", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.24.4.tgz", + "integrity": "sha512-dpWYBPTtMg152yNLXZQ7xb6hsXdYbKp9EuK8qq4npS+SZ08FVc1XHlXYhrOm31T+tUVJKgm95Yaqy69wTpZP9Q==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.24.4", + "version": "1.24.5", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index f8fa949..3b11949 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.24.4", + "version": "1.24.5", "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 f4840ff..b7d133b 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.4", + "version": "1.24.5", "description": "MP3 encoder extension for Mediabunny, based on LAME.", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs", From 8b39eb0d43e85fc6b378a905112d2c10802479d8 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:14:43 +0100 Subject: [PATCH 10/11] Revert Matroska default duration flooring --- dev/demux.html | 4 +++- src/matroska/matroska-demuxer.ts | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/dev/demux.html b/dev/demux.html index d53045c..1a864a3 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,11 +14,13 @@ source: new Mediabunny.BlobSource(file), }); - const videoTrack = await input.getPrimaryVideoTrack(); + const videoTrack = await input.getPrimaryAudioTrack(); const sink = new Mediabunny.EncodedPacketSink(videoTrack); for await (const packet of sink.packets()) { console.log(packet.timestamp, packet.duration, packet.timestamp + packet.duration); + + if (packet.timestamp > 10) break; } /* diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 79c84f4..e964bef 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -844,13 +844,13 @@ export class MatroskaDemuxer extends Demuxer { blocks.splice(blockIndex, 1); // Remove the original block + const blockDuration = originalBlock.duration || frameCount * (track.defaultDuration ?? 0); + // Now, let's insert each frame as its own block for (let i = 0; i < frameCount; i++) { const frameSize = frameSizes[i]!; const frameData = readBytes(slice, frameSize); - const blockDuration = originalBlock.duration || (frameCount * (track.defaultDuration ?? 0)); - // Distribute timestamps evenly across the block duration const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount); const frameDuration = blockDuration / frameCount; @@ -1181,9 +1181,8 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.DefaultDuration: { if (!this.currentTrack) break; - // Floored to reduce chance of overlapping packets (https://github.com/Vanilagy/mediabunny/issues/170) this.currentTrack.defaultDuration - = Math.floor(this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9); + = this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9; }; break; case EBMLId.Name: { From 3db4aa8b914c8439dfc54c1a97f5dd2a3f8f79cd Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:05:15 +0100 Subject: [PATCH 11/11] Fixed doubly stacked rotation correction when converting rotated video with a custom process function (fixes #218) --- dev/convert.html | 15 +++++++++------ src/conversion.ts | 8 ++++++-- src/sample.ts | 5 ++++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index bc1f9d1..79b7ae3 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -48,6 +48,7 @@ target }); + let ctx = null; const conversion = await Mediabunny.Conversion.init({ input: new Mediabunny.Input({ formats: Mediabunny.ALL_FORMATS, @@ -56,6 +57,7 @@ output, audio: (_, n) => ({ discard: n > 1, + //codec: 'opus', /* process: (sample) => { return sample; @@ -97,7 +99,7 @@ }, */ video: () => ({ - /* + //discard: true, process: (sample) => { if (!ctx) { // Create a canvas for image compositing @@ -108,13 +110,14 @@ ctx = canvas.getContext('2d'); } + console.log(ctx.canvas.width, ctx.canvas.height); + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - sample.draw(ctx, 0, 0); - ctx.drawImage(watermark, 32, 32); + sample.drawWithFit(ctx, { fit: 'fill' }); + //ctx.drawImage(watermark, 32, 32); return ctx.canvas; }, - */ //width: 300, //alpha: 'keep', //width: 320, @@ -170,8 +173,8 @@ } }, trim: { - start: 0, - end: 10 + //start: 0, + //end: 10 }, }); console.log(conversion); diff --git a/src/conversion.ts b/src/conversion.ts index abf820a..0cabe1a 100644 --- a/src/conversion.ts +++ b/src/conversion.ts @@ -182,7 +182,8 @@ export type ConversionVideoOptions = { * corrections. * * Must return a {@link VideoSample} or a `CanvasImageSource`, an array of them, or `null` for dropping the frame. - * When non-timestamped data is returned, the timestamp and duration from the source sample will be used. + * When non-timestamped data is returned, the timestamp and duration from the source sample will be used. Rotation + * metadata of the returned sample will be ignored. * * This function can also be used to manually resize frames. When doing so, you should signal the post-process * dimensions using the `processedWidth` and `processedHeight` fields, which enables the encoder to better know what @@ -873,7 +874,10 @@ export class Conversion { || trackOptions.process !== undefined; let needsRerender = width !== originalWidth || height !== originalHeight - || (totalRotation !== 0 && !outputSupportsRotation) + // TODO This is suboptimal: Forcing a rerender when both rotation and process are set is not + // performance-optimal, but right now there's no other way because we can't change the track rotation + // metadata after the output has already started. Should be possible with API changes in v2, though! + || (totalRotation !== 0 && (!outputSupportsRotation || trackOptions.process !== undefined)) || !!crop; const alpha = trackOptions.alpha ?? 'discard'; diff --git a/src/sample.ts b/src/sample.ts index 805aebe..35d7800 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -552,7 +552,6 @@ export class VideoSample implements Disposable { dHeight, ); - // Restore the previous transformation state context.restore(); } @@ -644,6 +643,8 @@ export class VideoSample implements Disposable { dy = (canvasHeight - newHeight) / 2; } + context.save(); + const aspectRatioChange = rotation % 180 === 0 ? 1 : newWidth / newHeight; context.translate(canvasWidth / 2, canvasHeight / 2); context.rotate(rotation * Math.PI / 180); @@ -655,6 +656,8 @@ export class VideoSample implements Disposable { // Important that we don't use .draw() here since that would take rotation into account, but we wanna handle it // ourselves here context.drawImage(this.toCanvasImageSource(), sx, sy, sWidth, sHeight, dx, dy, newWidth, newHeight); + + context.restore(); } /** @internal */