From 407dd05953ece2ffbabe5923a1bc3079cc73b8d7 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:00:03 +0200 Subject: [PATCH] Make Matroska block decoding lazy --- dev/convert.html | 12 +++-- package-lock.json | 12 ++--- package.json | 2 +- packages/mp3-encoder/package.json | 2 +- src/codec-data.ts | 3 +- src/matroska/matroska-demuxer.ts | 81 +++++++++++++++++++------------ src/reader.ts | 9 ++-- 7 files changed, 72 insertions(+), 49 deletions(-) diff --git a/dev/convert.html b/dev/convert.html index ea8de68..4d6702e 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -54,7 +54,8 @@ source }), output, - audio: { + audio: (_, n) => ({ + discard: n > 1, //codec: 'pcm-s16', //sampleRate: 16000, //numberOfChannels: 1, @@ -65,7 +66,7 @@ //sampleRate: 4000 //discard: true //forceTranscode: true, - }, + }), /* video: { discard: true, @@ -91,6 +92,7 @@ }, */ video: () => ({ + discard: true, //discard: true, //crop: { // left: 0, @@ -124,7 +126,7 @@ //width: 200, //height: 100, }), - tags: { + tags: {} ?? { title: 'Bigggy', artist: 'Buck Bunny', images: [{ @@ -142,8 +144,8 @@ } }, trim: { - start: 10, - end: 20 + //start: 10, + //end: 20 }, }); console.log(conversion); diff --git a/package-lock.json b/package-lock.json index 79780ca..3e1c1e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.16.0", + "version": "1.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.16.0", + "version": "1.16.1", "license": "MPL-2.0", "workspaces": [ "packages/*" @@ -7749,9 +7749,9 @@ } }, "node_modules/mediabunny": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.15.2.tgz", - "integrity": "sha512-OvIuSK10wNdz+7Dd4bmeq/IWkYG0bkm33FQb29vmyDztpr47ru8XSxYansa3EHLy6lLEm4rUADUENOcmrnN0jg==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.16.0.tgz", + "integrity": "sha512-JXNR8MVo4bMHN4WMLSBjCUMBAxPjEJKtfAqBJI7gJayMOMeLp00K3WE5CH5uLSyhk0yCr26bGNjIEBF0/pQjZA==", "license": "MPL-2.0", "peer": true, "workspaces": [ @@ -12242,7 +12242,7 @@ }, "packages/mp3-encoder": { "name": "@mediabunny/mp3-encoder", - "version": "1.16.0", + "version": "1.16.1", "license": "MPL-2.0", "devDependencies": { "@types/emscripten": "^1.40.1" diff --git a/package.json b/package.json index c5f1bfb..f4d99a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.16.0", + "version": "1.16.1", "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 7151341..96598bf 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.16.0", + "version": "1.16.1", "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/codec-data.ts b/src/codec-data.ts index 895569f..9ebf360 100644 --- a/src/codec-data.ts +++ b/src/codec-data.ts @@ -12,6 +12,7 @@ import { assert, assertNever, Bitstream, + getUint24, last, readExpGolomb, readSignedExpGolomb, @@ -116,7 +117,7 @@ const findNalUnitsInLengthPrefixed = (packetData: Uint8Array, lengthSize: 1 | 2 } else if (lengthSize === 2) { nalUnitLength = dataView.getUint16(offset, false); } else if (lengthSize === 3) { - nalUnitLength = (dataView.getUint16(offset, false) << 8) + dataView.getUint8(offset + 2); + nalUnitLength = getUint24(dataView, offset, false); } else if (lengthSize === 4) { nalUnitLength = dataView.getUint32(offset, false); } else { diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 89617ce..8277b73 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -116,7 +116,7 @@ type Cluster = { }; type ClusterTrackData = { - track: InternalTrack | null; + track: InternalTrack; startTimestamp: number; endTimestamp: number; firstKeyFrameTimestamp: number | null; @@ -141,6 +141,7 @@ type ClusterBlock = { referencedTimestamps: number[]; data: Uint8Array; lacing: BlockLacing; + decoded: boolean; }; type CuePoint = { @@ -667,7 +668,7 @@ export class MatroskaDemuxer extends Demuxer { const nextEntry = trackData.presentationTimestamps[i + 1]!; currentBlock.duration = nextEntry.timestamp - currentBlock.timestamp; } else if (currentBlock.duration === 0) { - if (track?.defaultDuration != null) { + if (track.defaultDuration != null) { if (currentBlock.lacing === BlockLacing.None) { currentBlock.duration = track.defaultDuration; } else { @@ -695,13 +696,11 @@ export class MatroskaDemuxer extends Demuxer { trackData.startTimestamp = firstBlock.timestamp; trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration; - if (track) { - insertSorted(track.clusters, cluster, x => x.elementStartPos); + insertSorted(track.clusters, cluster, x => x.elementStartPos); - const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null; - if (hasKeyFrame) { - insertSorted(track.clustersWithKeyFrame, cluster, x => x.elementStartPos); - } + const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null; + if (hasKeyFrame) { + insertSorted(track.clustersWithKeyFrame, cluster, x => x.elementStartPos); } } @@ -714,8 +713,13 @@ export class MatroskaDemuxer extends Demuxer { getTrackDataInCluster(cluster: Cluster, trackNumber: number) { let trackData = cluster.trackData.get(trackNumber); if (!trackData) { + const track = cluster.segment.tracks.find(x => x.id === trackNumber); + if (!track) { + return null; + } + trackData = { - track: cluster.segment.tracks.find(x => x.id === trackNumber) ?? null, + track, startTimestamp: 0, endTimestamp: 0, firstKeyFrameTimestamp: null, @@ -728,7 +732,7 @@ export class MatroskaDemuxer extends Demuxer { return trackData; } - expandLacedBlocks(blocks: ClusterBlock[], track: InternalTrack | null) { + expandLacedBlocks(blocks: ClusterBlock[], track: InternalTrack) { // https://www.matroska.org/technical/notes.html#block-lacing for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { @@ -737,6 +741,12 @@ export class MatroskaDemuxer extends Demuxer { continue; } + // Decode the block data if it hasn't been decoded yet (needed for lacing expansion) + if (!originalBlock.decoded) { + originalBlock.data = this.decodeBlockData(track, originalBlock.data); + originalBlock.decoded = true; + } + const slice = FileSlice.tempFromBytes(originalBlock.data); const frameSizes: number[] = []; @@ -819,7 +829,7 @@ export class MatroskaDemuxer extends Demuxer { const frameSize = frameSizes[i]!; const frameData = readBytes(slice, frameSize); - const blockDuration = originalBlock.duration || (frameCount * (track?.defaultDuration ?? 0)); + const blockDuration = originalBlock.duration || (frameCount * (track.defaultDuration ?? 0)); // Distribute timestamps evenly across the block duration const frameTimestamp = originalBlock.timestamp + (blockDuration * i / frameCount); @@ -832,6 +842,7 @@ export class MatroskaDemuxer extends Demuxer { referencedTimestamps: originalBlock.referencedTimestamps, data: frameData, lacing: BlockLacing.None, + decoded: true, }); } @@ -1331,18 +1342,17 @@ export class MatroskaDemuxer extends Demuxer { const trackNumber = readVarInt(slice); if (trackNumber === null) break; + const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber); + if (!trackData) break; // Not a track we care about + const relativeTimestamp = readI16Be(slice); const flags = readU8(slice); const isKeyFrame = !!(flags & 0x80); const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later - const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber); - let blockData = readBytes(slice, size - (slice.filePos - dataStartPos)); - - if (trackData.track) { - blockData = this.decodeBlockData(trackData.track, blockData); - } + const blockData = readBytes(slice, size - (slice.filePos - dataStartPos)); + const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0; trackData.blocks.push({ timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later @@ -1351,6 +1361,7 @@ export class MatroskaDemuxer extends Demuxer { referencedTimestamps: [], data: blockData, lacing, + decoded: !hasDecodingInstructions, }); }; break; @@ -1374,17 +1385,16 @@ export class MatroskaDemuxer extends Demuxer { const trackNumber = readVarInt(slice); if (trackNumber === null) break; + const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber); + if (!trackData) break; + const relativeTimestamp = readI16Be(slice); const flags = readU8(slice); const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later - const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber); - let blockData = readBytes(slice, size - (slice.filePos - dataStartPos)); - - if (trackData.track) { - blockData = this.decodeBlockData(trackData.track, blockData); - } + const blockData = readBytes(slice, size - (slice.filePos - dataStartPos)); + const hasDecodingInstructions = trackData.track.decodingInstructions.length > 0; this.currentBlock = { timestamp: relativeTimestamp, // We'll add the cluster's timestamp to this later @@ -1393,6 +1403,7 @@ export class MatroskaDemuxer extends Demuxer { referencedTimestamps: [], data: blockData, lacing, + decoded: !hasDecodingInstructions, }; trackData.blocks.push(this.currentBlock); }; break; @@ -1600,24 +1611,26 @@ export class MatroskaDemuxer extends Demuxer { } decodeBlockData(track: InternalTrack, rawData: Uint8Array) { + assert(track.decodingInstructions.length > 0); // This method shouldn't be called otherwise + let currentData = rawData; - // In the vast number of cases there are exactly zero decoding instructions - for (let i = 0; i < track.decodingInstructions.length; i++) { - const instruction = track.decodingInstructions[i]!; + for (const instruction of track.decodingInstructions) { assert(instruction.data); switch (instruction.data.type) { case 'decompress': { switch (instruction.data.algorithm) { case ContentCompAlgo.HeaderStripping: { - const prefix = instruction.data.settings ?? new Uint8Array(0); - const newData = new Uint8Array(prefix.length + currentData.length); + if (instruction.data.settings && instruction.data.settings.length > 0) { + const prefix = instruction.data.settings; + const newData = new Uint8Array(prefix.length + currentData.length); - newData.set(prefix, 0); - newData.set(currentData, prefix.length); + newData.set(prefix, 0); + newData.set(currentData, prefix.length); - currentData = newData; + currentData = newData; + } }; break; default: { @@ -1959,6 +1972,12 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const block = trackData.blocks[blockIndex]; assert(block); + // Perform lazy decoding if needed + if (!block.decoded) { + block.data = this.internalTrack.demuxer.decodeBlockData(this.internalTrack, block.data); + block.decoded = true; + } + const data = options.metadataOnly ? PLACEHOLDER_DATA : block.data; const timestamp = block.timestamp / this.internalTrack.segment.timestampFactor; const duration = block.duration / this.internalTrack.segment.timestampFactor; diff --git a/src/reader.ts b/src/reader.ts index b0104f0..687e9bc 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { assert, clamp, MaybePromise, toDataView } from './misc'; +import { assert, clamp, getUint24, MaybePromise, toDataView } from './misc'; import { Source } from './source'; export class Reader { @@ -157,9 +157,10 @@ export const readU16Be = (slice: FileSlice) => { }; export const readU24Be = (slice: FileSlice) => { - const high = readU16Be(slice); - const low = readU8(slice); - return high * 0x100 + low; + const value = getUint24(slice.view, slice.bufferPos, false); + slice.bufferPos += 3; + + return value; }; export const readI16Be = (slice: FileSlice) => {