From 198928088b4218efffa06f9c9de59ba84c37a1c2 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:36:37 +0200 Subject: [PATCH] Make cluster/fragment lookup operations almost stateless, fix high Matroska memory, allow more maxCacheSizes --- dev/demux.html | 40 +- src/isobmff/isobmff-demuxer.ts | 609 ++++++++++++------------------- src/matroska/matroska-demuxer.ts | 454 ++++++++++------------- src/media-sink.ts | 3 +- src/misc.ts | 4 + src/source.ts | 21 +- 6 files changed, 465 insertions(+), 666 deletions(-) diff --git a/dev/demux.html b/dev/demux.html index daa726c..953fa06 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -14,13 +14,43 @@ source: new Mediabunny.BlobSource(file), }); - const audioTrack = await input.getPrimaryAudioTrack(); - - const sink = new Mediabunny.EncodedPacketSink(audioTrack); - for await (const packet of sink.packets()) { - console.log(packet) + let total = 0; + input.source.onread = (start, end) => { + total += end - start; + //console.log(total / file.size, end - start); } + const videoTrack = await input.getPrimaryVideoTrack(); + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + + //console.log(await sink.getPacket(0)); + /* + console.time() + console.log(await sink.getPacket(500)); + console.timeEnd() + + console.time() + console.log(await sink.getPacket(400)); + console.timeEnd() + */ + //console.log(await sink.getPacket(50)); + //console.log(await sink.getPacket(8000)); + + + + const stats = await videoTrack.computePacketStats(); + + /* + const sink = new Mediabunny.EncodedPacketSink(videoTrack); + for await (const packet of sink.packets()) { + //console.log(packet) + } + */ + //console.log(await videoTrack.computeDuration()); + console.log("Done", stats, total, file.size) + + console.log(input); + /* const videoTrack = await input.getPrimaryVideoTrack(); const sink = new Mediabunny.VideoSampleSink(videoTrack); diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 903e41f..eac99a4 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -41,13 +41,11 @@ import { import { PacketRetrievalOptions } from '../media-sink'; import { assert, - AsyncMutex, binarySearchExact, binarySearchLessOrEqual, Bitstream, COLOR_PRIMARIES_MAP_INVERSE, findLastIndex, - insertSorted, isIso639Dash2LanguageCode, last, MATRIX_COEFFICIENTS_MAP_INVERSE, @@ -103,10 +101,17 @@ type InternalTrack = { languageCode: string; sampleTableByteOffset: number; sampleTable: SampleTable | null; - fragmentLookupTable: FragmentLookupTableEntry[] | null; + fragmentLookupTable: FragmentLookupTableEntry[]; currentFragmentState: FragmentTrackState | null; - fragments: Fragment[]; - fragmentsWithKeyFrame: Fragment[]; + /** + * List of all encountered fragment offsets alongside their timestamps. This list never gets truncated, but memory + * consumption should be negligible. + */ + fragmentPositionCache: { + moofOffset: number; + startTimestamp: number; + endTimestamp: number; + }[]; /** The segment durations of all edit list entries leading up to the main one (from which the offset is taken.) */ editListPreviousSegmentDurations: number; /** The media time offset of the main edit list entry (with media time !== -1) */ @@ -198,6 +203,7 @@ type FragmentTrackState = { }; type FragmentTrackData = { + track: InternalTrack; startTimestamp: number; endTimestamp: number; firstKeyFrameTimestamp: number | null; @@ -222,10 +228,6 @@ type Fragment = { moofSize: number; implicitBaseDataOffset: number; trackData: Map; - dataStart: number; - dataEnd: number; - nextFragment: Fragment | null; - isKnownToBeFirstFragment: boolean; }; export class IsobmffDemuxer extends Demuxer { @@ -243,9 +245,12 @@ export class IsobmffDemuxer extends Demuxer { isFragmented = false; fragmentTrackDefaults: FragmentTrackDefaults[] = []; - fragments: Fragment[] = []; currentFragment: Fragment | null = null; - fragmentLookupMutex = new AsyncMutex(); + /** + * Caches the last fragment that was read. Based on the assumption that there will be multiple reads to the + * same fragment in quick succession. + */ + lastReadFragment: Fragment | null = null; constructor(input: Input) { super(input); @@ -502,6 +507,10 @@ export class IsobmffDemuxer extends Demuxer { } async readFragment(startPos: number): Promise { + if (this.lastReadFragment?.moofOffset === startPos) { + return this.lastReadFragment; + } + let headerSlice = this.reader.requestSliceRange(startPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE); if (headerSlice instanceof Promise) headerSlice = await headerSlice; assert(headerSlice); @@ -515,92 +524,65 @@ export class IsobmffDemuxer extends Demuxer { this.traverseBox(entireSlice); - const index = binarySearchExact(this.fragments, startPos, x => x.moofOffset); - assert(index !== -1); + const fragment = this.lastReadFragment; + assert(fragment && fragment.moofOffset === startPos); - const fragment = this.fragments[index]!; - assert(fragment.moofOffset === startPos); + for (const [, trackData] of fragment.trackData) { + const track = trackData.track; + const { fragmentPositionCache } = track; - // It may be that some tracks don't define the base decode time, i.e. when the fragment begins. This means the - // only other option is to sum up the duration of all previous fragments. - for (const [trackId, trackData] of fragment.trackData) { - if (trackData.startTimestampIsFinal) { - continue; - } + if (!trackData.startTimestampIsFinal) { + // It may be that some tracks don't define the base decode time, i.e. when the fragment begins. This + // we'll need to figure out the start timestamp another way. We'll compute the timestamp by accessing + // the lookup entries and fragment cache, which works out nicely with the lookup algorithm: If these + // exist, then the lookup will automatically start at the furthest possible point. If they don't, the + // lookup starts sequentially from the start, incrementally summing up all fragment durations. It's sort + // of implicit, but it ends up working nicely. - const internalTrack = this.tracks.find(x => x.id === trackId)!; - - let currentPos = 0; - let currentFragment: Fragment | null = null; - let lastFragment: Fragment | null = null; - - const index = binarySearchLessOrEqual( - internalTrack.fragments, - startPos - 1, - x => x.moofOffset, - ); - if (index !== -1) { - // Instead of starting at the start of the file, let's start at the previous fragment instead (which - // already has final timestamps). - currentFragment = internalTrack.fragments[index]!; - lastFragment = currentFragment; - currentPos = currentFragment.moofOffset + currentFragment.moofSize; - } - - let nextFragmentIsFirstFragment = currentPos === 0; - - while (currentPos <= startPos - MIN_BOX_HEADER_SIZE) { - if (currentFragment?.nextFragment) { - currentFragment = currentFragment.nextFragment; - currentPos = currentFragment.moofOffset + currentFragment.moofSize; + const lookupEntryIndex = binarySearchExact( + track.fragmentLookupTable, + fragment.moofOffset, + x => x.moofOffset, + ); + if (lookupEntryIndex !== -1) { + // There's a lookup entry, let's use its timestamp + const lookupEntry = track.fragmentLookupTable[lookupEntryIndex]!; + offsetFragmentTrackDataByTimestamp(trackData, lookupEntry.timestamp); } else { - let slice = this.reader.requestSliceRange(currentPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE); - if (slice instanceof Promise) slice = await slice; - if (!slice) break; - - const boxStartPos = currentPos; - const boxInfo = readBoxHeader(slice); - if (!boxInfo) { - break; + const lastCacheIndex = binarySearchLessOrEqual( + fragmentPositionCache, + fragment.moofOffset - 1, + x => x.moofOffset, + ); + if (lastCacheIndex !== -1) { + // Let's use the timestamp of the previous fragment in the cache + const lastCache = fragmentPositionCache[lastCacheIndex]!; + offsetFragmentTrackDataByTimestamp(trackData, lastCache.endTimestamp); + } else { + // We're the first fragment I guess, "offset by 0" } - - if (boxInfo.name === 'moof') { - const index = binarySearchExact(this.fragments, boxStartPos, x => x.moofOffset); - - let fragment: Fragment; - if (index === -1) { - fragment = await this.readFragment(boxStartPos); // Recursive call - } else { - // We already know this fragment - fragment = this.fragments[index]!; - } - - // Even if we already know the fragment, we might not yet know its predecessor; always do this - if (currentFragment) currentFragment.nextFragment = fragment; - currentFragment = fragment; - - if (nextFragmentIsFirstFragment) { - fragment.isKnownToBeFirstFragment = true; - nextFragmentIsFirstFragment = false; - } - } - - currentPos = boxStartPos + boxInfo.totalSize; } - if (currentFragment && currentFragment.trackData.has(trackId)) { - lastFragment = currentFragment; - } + trackData.startTimestampIsFinal = true; } - if (lastFragment) { - const otherTrackData = lastFragment.trackData.get(trackId)!; - assert(otherTrackData.startTimestampIsFinal); - - offsetFragmentTrackDataByTimestamp(trackData, otherTrackData.endTimestamp); + // Let's remember that a fragment with a given timestamp is here, speeding up future lookups if no + // lookup table exists + const insertionIndex = binarySearchLessOrEqual( + fragmentPositionCache, + trackData.startTimestamp, + x => x.startTimestamp, + ); + if ( + insertionIndex === -1 + || fragmentPositionCache[insertionIndex]!.moofOffset !== fragment.moofOffset + ) { + fragmentPositionCache.splice(insertionIndex + 1, 0, { + moofOffset: fragment.moofOffset, + startTimestamp: trackData.startTimestamp, + endTimestamp: trackData.endTimestamp, + }); } - - trackData.startTimestampIsFinal = true; } return fragment; @@ -683,10 +665,9 @@ export class IsobmffDemuxer extends Demuxer { languageCode: UNDETERMINED_LANGUAGE, sampleTableByteOffset: -1, sampleTable: null, - fragmentLookupTable: null, + fragmentLookupTable: [], currentFragmentState: null, - fragments: [], - fragmentsWithKeyFrame: [], + fragmentPositionCache: [], editListPreviousSegmentDurations: 0, editListOffset: 0, } satisfies InternalTrack as InternalTrack; @@ -1735,8 +1716,6 @@ export class IsobmffDemuxer extends Demuxer { break; } - track.fragmentLookupTable = []; - const word = readU32Be(slice); const lengthSizeOfTrafNum = (word & 0b110000) >> 4; @@ -1771,31 +1750,11 @@ export class IsobmffDemuxer extends Demuxer { moofSize: boxInfo.totalSize, implicitBaseDataOffset: startPos, trackData: new Map(), - dataStart: Infinity, - dataEnd: 0, - nextFragment: null, - isKnownToBeFirstFragment: false, }; this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); - insertSorted(this.fragments, this.currentFragment, x => x.moofOffset); - - // Compute the byte range of the sample data in this fragment, so we can load the whole fragment at once - for (const [, trackData] of this.currentFragment.trackData) { - const firstSample = trackData.samples[0]!; - const lastSample = last(trackData.samples)!; - - this.currentFragment.dataStart = Math.min( - this.currentFragment.dataStart, - firstSample.byteOffset, - ); - this.currentFragment.dataEnd = Math.max( - this.currentFragment.dataEnd, - lastSample.byteOffset + lastSample.byteSize, - ); - } - + this.lastReadFragment = this.currentFragment; this.currentFragment = null; }; break; @@ -1809,19 +1768,6 @@ export class IsobmffDemuxer extends Demuxer { if (this.currentTrack) { const trackData = this.currentFragment.trackData.get(this.currentTrack.id); if (trackData) { - // We know there is sample data for this track in this fragment, so let's add it to the - // track's fragments: - insertSorted(this.currentTrack.fragments, this.currentFragment, x => x.moofOffset); - - const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null; - if (hasKeyFrame) { - insertSorted( - this.currentTrack.fragmentsWithKeyFrame, - this.currentFragment, - x => x.moofOffset, - ); - } - const { currentFragmentState } = this.currentTrack; assert(currentFragmentState); @@ -1952,6 +1898,7 @@ export class IsobmffDemuxer extends Demuxer { let currentTimestamp = 0; const trackData: FragmentTrackData = { + track, startTimestamp: 0, endTimestamp: 0, firstKeyFrameTimestamp: null, @@ -2404,31 +2351,17 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { } return this.performFragmentedLookup( - () => { - const startFragment = this.internalTrack.demuxer.fragments[0] ?? null; - if (startFragment?.isKnownToBeFirstFragment) { - // Walk from the very first fragment in the file until we find one with our track in it - let currentFragment: Fragment | null = startFragment; - while (currentFragment) { - const trackData = currentFragment.trackData.get(this.internalTrack.id); - if (trackData) { - return { - fragmentIndex: binarySearchExact( - this.internalTrack.fragments, - currentFragment.moofOffset, - x => x.moofOffset, - ), - sampleIndex: 0, - correctSampleFound: true, - }; - } - - currentFragment = currentFragment.nextFragment; - } + null, + (fragment) => { + const trackData = fragment.trackData.get(this.internalTrack.id); + if (trackData) { + return { + sampleIndex: 0, + correctSampleFound: true, + }; } return { - fragmentIndex: -1, sampleIndex: -1, correctSampleFound: false, }; @@ -2459,7 +2392,24 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { } return this.performFragmentedLookup( - () => this.findSampleInFragmentsForTimestamp(timestampInTimescale), + null, + (fragment) => { + const trackData = fragment.trackData.get(this.internalTrack.id); + if (!trackData) { + return { sampleIndex: -1, correctSampleFound: false }; + } + + const index = binarySearchLessOrEqual( + trackData.presentationTimestamps, + timestampInTimescale, + x => x.presentationTimestamp, + ); + + const sampleIndex = index !== -1 ? trackData.presentationTimestamps[index]!.sampleIndex : -1; + const correctSampleFound = index !== -1 && timestampInTimescale < trackData.endTimestamp; + + return { sampleIndex, correctSampleFound }; + }, timestampInTimescale, timestampInTimescale, options, @@ -2479,53 +2429,32 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { throw new Error('Packet was not created from this track.'); } - const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; - - const fragmentIndex = binarySearchExact( - this.internalTrack.fragments, - locationInFragment.fragment.moofOffset, - x => x.moofOffset, - ); - assert(fragmentIndex !== -1); - return this.performFragmentedLookup( - () => { - if (locationInFragment.sampleIndex + 1 < trackData.samples.length) { - // We can simply take the next sample in the fragment - return { - fragmentIndex, - sampleIndex: locationInFragment.sampleIndex + 1, - correctSampleFound: true, - }; + locationInFragment.fragment, + (fragment) => { + if (fragment === locationInFragment.fragment) { + const trackData = fragment.trackData.get(this.internalTrack.id)!; + if (locationInFragment.sampleIndex + 1 < trackData.samples.length) { + // We can simply take the next sample in the fragment + return { + sampleIndex: locationInFragment.sampleIndex + 1, + correctSampleFound: true, + }; + } } else { - // Walk the list of fragments until we find the next fragment for this track - let currentFragment = locationInFragment.fragment; - while (currentFragment.nextFragment) { - currentFragment = currentFragment.nextFragment; - - const trackData = currentFragment.trackData.get(this.internalTrack.id); - if (trackData) { - const fragmentIndex = binarySearchExact( - this.internalTrack.fragments, - currentFragment.moofOffset, - x => x.moofOffset, - ); - assert(fragmentIndex !== -1); - - return { - fragmentIndex, - sampleIndex: 0, - correctSampleFound: true, - }; - } + const trackData = fragment.trackData.get(this.internalTrack.id); + if (trackData) { + return { + sampleIndex: 0, + correctSampleFound: true, + }; } - - return { - fragmentIndex, - sampleIndex: -1, - correctSampleFound: false, - }; } + + return { + sampleIndex: -1, + correctSampleFound: false, + }; }, -Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries Infinity, @@ -2549,7 +2478,23 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { } return this.performFragmentedLookup( - () => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale), + null, + (fragment) => { + const trackData = fragment.trackData.get(this.internalTrack.id); + if (!trackData) { + return { sampleIndex: -1, correctSampleFound: false }; + } + + const index = findLastIndex(trackData.presentationTimestamps, (x) => { + const sample = trackData.samples[x.sampleIndex]!; + return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale; + }); + + const sampleIndex = index !== -1 ? trackData.presentationTimestamps[index]!.sampleIndex : -1; + const correctSampleFound = index !== -1 && timestampInTimescale < trackData.endTimestamp; + + return { sampleIndex, correctSampleFound }; + }, timestampInTimescale, timestampInTimescale, options, @@ -2570,60 +2515,39 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { throw new Error('Packet was not created from this track.'); } - const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; - - const fragmentIndex = binarySearchExact( - this.internalTrack.fragments, - locationInFragment.fragment.moofOffset, - x => x.moofOffset, - ); - assert(fragmentIndex !== -1); - return this.performFragmentedLookup( - () => { - const nextKeyFrameIndex = trackData.samples.findIndex( - (x, i) => x.isKeyFrame && i > locationInFragment.sampleIndex, - ); + locationInFragment.fragment, + (fragment) => { + if (fragment === locationInFragment.fragment) { + const trackData = fragment.trackData.get(this.internalTrack.id)!; + const nextKeyFrameIndex = trackData.samples.findIndex( + (x, i) => x.isKeyFrame && i > locationInFragment.sampleIndex, + ); - if (nextKeyFrameIndex !== -1) { - // We can simply take the next key frame in the fragment - return { - fragmentIndex, - sampleIndex: nextKeyFrameIndex, - correctSampleFound: true, - }; - } else { - // Walk the list of fragments until we find the next fragment for this track with a key frame - let currentFragment = locationInFragment.fragment; - while (currentFragment.nextFragment) { - currentFragment = currentFragment.nextFragment; - - const trackData = currentFragment.trackData.get(this.internalTrack.id); - if (trackData && trackData.firstKeyFrameTimestamp !== null) { - const fragmentIndex = binarySearchExact( - this.internalTrack.fragments, - currentFragment.moofOffset, - x => x.moofOffset, - ); - assert(fragmentIndex !== -1); - - const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame); - assert(keyFrameIndex !== -1); // There must be one - - return { - fragmentIndex, - sampleIndex: keyFrameIndex, - correctSampleFound: true, - }; - } + if (nextKeyFrameIndex !== -1) { + // We can simply take the next key frame in the fragment + return { + sampleIndex: nextKeyFrameIndex, + correctSampleFound: true, + }; } + } else { + const trackData = fragment.trackData.get(this.internalTrack.id); + if (trackData && trackData.firstKeyFrameTimestamp !== null) { + const keyFrameIndex = trackData.samples.findIndex(x => x.isKeyFrame); + assert(keyFrameIndex !== -1); // There must be one - return { - fragmentIndex, - sampleIndex: -1, - correctSampleFound: false, - }; + return { + sampleIndex: keyFrameIndex, + correctSampleFound: true, + }; + } } + + return { + sampleIndex: -1, + correctSampleFound: false, + }; }, -Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries Infinity, @@ -2713,77 +2637,12 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { return packet; } - private findSampleInFragmentsForTimestamp(timestampInTimescale: number) { - const fragmentIndex = binarySearchLessOrEqual( - // This array is technically not sorted by start timestamp, but for any reasonable file, it basically is. - this.internalTrack.fragments, - timestampInTimescale, - x => x.trackData.get(this.internalTrack.id)!.startTimestamp, - ); - let sampleIndex = -1; - let correctSampleFound = false; - - if (fragmentIndex !== -1) { - const fragment = this.internalTrack.fragments[fragmentIndex]!; - const trackData = fragment.trackData.get(this.internalTrack.id)!; - - const index = binarySearchLessOrEqual( - trackData.presentationTimestamps, - timestampInTimescale, - x => x.presentationTimestamp, - ); - assert(index !== -1); - - sampleIndex = trackData.presentationTimestamps[index]!.sampleIndex; - correctSampleFound = timestampInTimescale < trackData.endTimestamp; - } - - return { fragmentIndex, sampleIndex, correctSampleFound }; - } - - private findKeySampleInFragmentsForTimestamp(timestampInTimescale: number) { - const indexInKeyFrameFragments = binarySearchLessOrEqual( - // This array is technically not sorted by start timestamp, but for any reasonable file, it basically is. - this.internalTrack.fragmentsWithKeyFrame, - timestampInTimescale, - x => x.trackData.get(this.internalTrack.id)!.startTimestamp, - ); - - let fragmentIndex = -1; - let sampleIndex = -1; - let correctSampleFound = false; - - if (indexInKeyFrameFragments !== -1) { - const fragment = this.internalTrack.fragmentsWithKeyFrame[indexInKeyFrameFragments]!; - - // Now, let's find the actual index of the fragment in the list of ALL fragments, not just key frame ones - fragmentIndex = binarySearchExact( - this.internalTrack.fragments, - fragment.moofOffset, - x => x.moofOffset, - ); - assert(fragmentIndex !== -1); - - const trackData = fragment.trackData.get(this.internalTrack.id)!; - const index = findLastIndex(trackData.presentationTimestamps, (x) => { - const sample = trackData.samples[x.sampleIndex]!; - return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale; - }); - assert(index !== -1); // It's a key frame fragment, so there must be a key frame - - const entry = trackData.presentationTimestamps[index]!; - sampleIndex = entry.sampleIndex; - correctSampleFound = timestampInTimescale < trackData.endTimestamp; - } - - return { fragmentIndex, sampleIndex, correctSampleFound }; - } - /** Looks for a packet in the fragments while trying to load as few fragments as possible to retrieve it. */ private async performFragmentedLookup( - // This function returns the best-matching sample that is currently loaded. Based on this information, we know - // which fragments we need to load to find the actual match. - getBestMatch: () => { fragmentIndex: number; sampleIndex: number; correctSampleFound: boolean }, + // The fragment where we start looking + startFragment: Fragment | null, + // This function returns the best-matching sample in a given fragment + getMatchInFragment: (fragment: Fragment) => { sampleIndex: number; correctSampleFound: boolean }, // The timestamp with which we can search the lookup table searchTimestamp: number, // The timestamp for which we know the correct sample will not come after it @@ -2791,65 +2650,71 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { options: PacketRetrievalOptions, ): Promise { const demuxer = this.internalTrack.demuxer; - const release = await demuxer.fragmentLookupMutex.acquire(); // The algorithm requires exclusivity try { - const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch(); - if (correctSampleFound) { - // The correct sample already exists, easy path. - const fragment = this.internalTrack.fragments[fragmentIndex]!; - return this.fetchPacketInFragment(fragment, sampleIndex, options); - } + let currentFragment: Fragment | null = null; + let bestFragment: Fragment | null = null; + let bestSampleIndex = -1; - let prevFragment: Fragment | null = null; - let bestFragmentIndex = fragmentIndex; - let bestSampleIndex = sampleIndex; + if (startFragment) { + const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment); + + if (correctSampleFound) { + return this.fetchPacketInFragment(startFragment, sampleIndex, options); + } + + if (sampleIndex !== -1) { + bestFragment = startFragment; + bestSampleIndex = sampleIndex; + } + } // Search for a lookup entry; this way, we won't need to start searching from the start of the file // but can jump right into the correct fragment (or at least nearby). - const lookupEntryIndex = this.internalTrack.fragmentLookupTable - ? binarySearchLessOrEqual( - this.internalTrack.fragmentLookupTable, - searchTimestamp, - x => x.timestamp, - ) - : -1; + const lookupEntryIndex = binarySearchLessOrEqual( + this.internalTrack.fragmentLookupTable, + searchTimestamp, + x => x.timestamp, + ); const lookupEntry = lookupEntryIndex !== -1 - ? this.internalTrack.fragmentLookupTable![lookupEntryIndex]! + ? this.internalTrack.fragmentLookupTable[lookupEntryIndex]! : null; + const positionCacheIndex = binarySearchLessOrEqual( + this.internalTrack.fragmentPositionCache, + searchTimestamp, + x => x.startTimestamp, + ); + const positionCacheEntry = positionCacheIndex !== -1 + ? this.internalTrack.fragmentPositionCache[positionCacheIndex]! + : null; + + const lookupEntryPosition = Math.max( + lookupEntry?.moofOffset ?? 0, + positionCacheEntry?.moofOffset ?? 0, + ) || null; + let currentPos: number; - let nextFragmentIsFirstFragment = false; - if (fragmentIndex === -1) { - currentPos = lookupEntry?.moofOffset ?? 0; - nextFragmentIsFirstFragment = currentPos === 0; + if (!startFragment) { + currentPos = lookupEntryPosition ?? 0; } else { - const fragment = this.internalTrack.fragments[fragmentIndex]!; - - if (!lookupEntry || fragment.moofOffset >= lookupEntry.moofOffset) { - currentPos = fragment.moofOffset + fragment.moofSize; - prevFragment = fragment; + if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) { + currentPos = startFragment.moofOffset + startFragment.moofSize; + currentFragment = startFragment; } else { // Use the lookup entry - currentPos = lookupEntry.moofOffset; + currentPos = lookupEntryPosition; } } while (true) { - if (prevFragment) { - const trackData = prevFragment.trackData.get(this.internalTrack.id); + if (currentFragment) { + const trackData = currentFragment.trackData.get(this.internalTrack.id); if (trackData && trackData.startTimestamp > latestTimestamp) { // We're already past the upper bound, no need to keep searching break; } - - if (prevFragment.nextFragment) { - // Skip ahead quickly without needing to read the file again - currentPos = prevFragment.nextFragment.moofOffset + prevFragment.nextFragment.moofSize; - prevFragment = prevFragment.nextFragment; - continue; - } } // Load the header @@ -2857,56 +2722,40 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { if (slice instanceof Promise) slice = await slice; if (!slice) break; - const startPos = currentPos; + const boxStartPos = currentPos; const boxInfo = readBoxHeader(slice); if (!boxInfo) { break; } if (boxInfo.name === 'moof') { - const index = binarySearchExact(demuxer.fragments, startPos, x => x.moofOffset); - - let fragment: Fragment; - if (index === -1) { - // This is the first time we've seen this fragment - fragment = await demuxer.readFragment(startPos); - } else { - // We already know this fragment - fragment = demuxer.fragments[index]!; - } - - // Even if we already know the fragment, we might not yet know its predecessor, so always do this - if (prevFragment) prevFragment.nextFragment = fragment; - prevFragment = fragment; - - if (nextFragmentIsFirstFragment) { - fragment.isKnownToBeFirstFragment = true; - nextFragmentIsFirstFragment = false; - } - - const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch(); + currentFragment = await demuxer.readFragment(boxStartPos); + const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment); if (correctSampleFound) { - const fragment = this.internalTrack.fragments[fragmentIndex]!; - return this.fetchPacketInFragment(fragment, sampleIndex, options); + return this.fetchPacketInFragment(currentFragment, sampleIndex, options); } - if (fragmentIndex !== -1) { - bestFragmentIndex = fragmentIndex; + if (sampleIndex !== -1) { + bestFragment = currentFragment; bestSampleIndex = sampleIndex; } } - currentPos = startPos + boxInfo.totalSize; + currentPos = boxStartPos + boxInfo.totalSize; } - const bestFragment = bestFragmentIndex !== -1 ? this.internalTrack.fragments[bestFragmentIndex]! : null; - // Catch faulty lookup table entries if (lookupEntry && (!bestFragment || bestFragment.moofOffset < lookupEntry.moofOffset)) { // The lookup table entry lied to us! We found a lookup entry but no fragment there that satisfied // the match. In this case, let's search again but using the lookup entry before that. - const previousLookupEntry = this.internalTrack.fragmentLookupTable![lookupEntryIndex - 1]; + const previousLookupEntry = this.internalTrack.fragmentLookupTable[lookupEntryIndex - 1]; const newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity; - return this.performFragmentedLookup(getBestMatch, newSearchTimestamp, latestTimestamp, options); + return this.performFragmentedLookup( + null, + getMatchInFragment, + newSearchTimestamp, + latestTimestamp, + options, + ); } if (bestFragment) { @@ -2916,7 +2765,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { return null; } finally { - release(); + // release(); } } } diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index e287652..45e0726 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -35,12 +35,9 @@ import { AttachedFile, MetadataTags } from '../tags'; import { PacketRetrievalOptions } from '../media-sink'; import { assert, - AsyncMutex, - binarySearchExact, binarySearchLessOrEqual, COLOR_PRIMARIES_MAP_INVERSE, findLastIndex, - insertSorted, isIso639Dash2LanguageCode, last, MATRIX_COEFFICIENTS_MAP_INVERSE, @@ -93,8 +90,11 @@ type Segment = { elementEndPos: number | null; clusterSeekStartPos: number; - clusters: Cluster[]; - clusterLookupMutex: AsyncMutex; + /** + * Caches the last cluster that was read. Based on the assumption that there will be multiple reads to the + * same cluster in quick succession. + */ + lastReadCluster: Cluster | null; metadataTags: MetadataTags; metadataTagsCollected: boolean; @@ -112,8 +112,6 @@ type Cluster = { dataStartPos: number; timestamp: number; trackData: Map; - nextCluster: Cluster | null; - isKnownToBeFirstCluster: boolean; }; type ClusterTrackData = { @@ -182,8 +180,14 @@ type InternalTrack = { id: number; demuxer: MatroskaDemuxer; segment: Segment; - clusters: Cluster[]; - clustersWithKeyFrame: Cluster[]; + /** + * List of all encountered cluster offsets alongside their timestamps. This list never gets truncated, but memory + * consumption should be negligible. + */ + clusterPositionCache: { + elementStartPos: number; + startTimestamp: number; + }[]; cuePoints: CuePoint[]; isDefault: boolean; @@ -409,8 +413,7 @@ export class MatroskaDemuxer extends Demuxer { : segmentDataStart + dataSize, clusterSeekStartPos: segmentDataStart, - clusters: [], - clusterLookupMutex: new AsyncMutex(), + lastReadCluster: null, metadataTags: {}, metadataTagsCollected: false, @@ -591,6 +594,10 @@ export class MatroskaDemuxer extends Demuxer { } async readCluster(startPos: number, segment: Segment) { + if (segment.lastReadCluster?.elementStartPos === startPos) { + return segment.lastReadCluster; + } + let headerSlice = this.reader.requestSliceRange(startPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); if (headerSlice instanceof Promise) headerSlice = await headerSlice; assert(headerSlice); @@ -600,6 +607,8 @@ export class MatroskaDemuxer extends Demuxer { assert(elementHeader); const id = elementHeader.id; + assert(id === EBMLId.Cluster); + let size = elementHeader.size; const dataStartPos = headerSlice.filePos; @@ -617,8 +626,6 @@ export class MatroskaDemuxer extends Demuxer { size = nextElementPos.pos - dataStartPos; } - assert(id === EBMLId.Cluster); - // Load the entire cluster let dataSlice = this.reader.requestSlice(dataStartPos, size); if (dataSlice instanceof Promise) dataSlice = await dataSlice; @@ -630,8 +637,6 @@ export class MatroskaDemuxer extends Demuxer { dataStartPos, timestamp: -1, trackData: new Map(), - nextCluster: null, - isKnownToBeFirstCluster: false, }; this.currentCluster = cluster; @@ -705,17 +710,24 @@ export class MatroskaDemuxer extends Demuxer { trackData.startTimestamp = firstBlock.timestamp; trackData.endTimestamp = lastBlock.timestamp + lastBlock.duration; - insertSorted(track.clusters, cluster, x => x.elementStartPos); - - const hasKeyFrame = trackData.firstKeyFrameTimestamp !== null; - if (hasKeyFrame) { - insertSorted(track.clustersWithKeyFrame, cluster, x => x.elementStartPos); + // Let's remember that a cluster with a given timestamp is here, speeding up future lookups if no cues exist + const insertionIndex = binarySearchLessOrEqual( + track.clusterPositionCache, + trackData.startTimestamp, + x => x.startTimestamp, + ); + if ( + insertionIndex === -1 + || track.clusterPositionCache[insertionIndex]!.elementStartPos !== elementStartPos + ) { + track.clusterPositionCache.splice(insertionIndex + 1, 0, { + elementStartPos: cluster.elementStartPos, + startTimestamp: trackData.startTimestamp, + }); } } - insertSorted(segment.clusters, cluster, x => x.elementStartPos); - this.currentCluster = null; - + segment.lastReadCluster = cluster; return cluster; } @@ -977,8 +989,7 @@ export class MatroskaDemuxer extends Demuxer { id: -1, segment: this.currentSegment, demuxer: this, - clusters: [], - clustersWithKeyFrame: [], + clusterPositionCache: [], cuePoints: [], isDefault: false, @@ -1844,31 +1855,17 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { async getFirstPacket(options: PacketRetrievalOptions) { return this.performClusterLookup( - () => { - const startCluster = this.internalTrack.segment.clusters[0] ?? null; - if (startCluster?.isKnownToBeFirstCluster) { - // Walk from the very first cluster in the file until we find one with our track in it - let currentCluster: Cluster | null = startCluster; - while (currentCluster) { - const trackData = currentCluster.trackData.get(this.internalTrack.id); - if (trackData) { - return { - clusterIndex: binarySearchExact( - this.internalTrack.clusters, - currentCluster.elementStartPos, - x => x.elementStartPos, - ), - blockIndex: 0, - correctBlockFound: true, - }; - } - - currentCluster = currentCluster.nextCluster; - } + null, + (cluster) => { + const trackData = cluster.trackData.get(this.internalTrack.id); + if (trackData) { + return { + blockIndex: 0, + correctBlockFound: true, + }; } return { - clusterIndex: -1, blockIndex: -1, correctBlockFound: false, }; @@ -1890,7 +1887,24 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const timestampInTimescale = this.intoTimescale(timestamp); return this.performClusterLookup( - () => this.findBlockInClustersForTimestamp(timestampInTimescale), + null, + (cluster) => { + const trackData = cluster.trackData.get(this.internalTrack.id); + if (!trackData) { + return { blockIndex: -1, correctBlockFound: false }; + } + + const index = binarySearchLessOrEqual( + trackData.presentationTimestamps, + timestampInTimescale, + x => x.timestamp, + ); + + const blockIndex = index !== -1 ? trackData.presentationTimestamps[index]!.blockIndex : -1; + const correctBlockFound = index !== -1 && timestampInTimescale < trackData.endTimestamp; + + return { blockIndex, correctBlockFound }; + }, timestampInTimescale, timestampInTimescale, options, @@ -1903,53 +1917,32 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { throw new Error('Packet was not created from this track.'); } - const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id)!; - - const clusterIndex = binarySearchExact( - this.internalTrack.clusters, - locationInCluster.cluster.elementStartPos, - x => x.elementStartPos, - ); - assert(clusterIndex !== -1); - return this.performClusterLookup( - () => { - if (locationInCluster.blockIndex + 1 < trackData.blocks.length) { - // We can simply take the next block in the cluster - return { - clusterIndex, - blockIndex: locationInCluster.blockIndex + 1, - correctBlockFound: true, - }; + locationInCluster.cluster, + (cluster) => { + if (cluster === locationInCluster.cluster) { + const trackData = cluster.trackData.get(this.internalTrack.id)!; + if (locationInCluster.blockIndex + 1 < trackData.blocks.length) { + // We can simply take the next block in the cluster + return { + blockIndex: locationInCluster.blockIndex + 1, + correctBlockFound: true, + }; + } } else { - // Walk the list of clusters until we find the next cluster for this track - let currentCluster = locationInCluster.cluster; - while (currentCluster.nextCluster) { - currentCluster = currentCluster.nextCluster; - - const trackData = currentCluster.trackData.get(this.internalTrack.id); - if (trackData) { - const clusterIndex = binarySearchExact( - this.internalTrack.clusters, - currentCluster.elementStartPos, - x => x.elementStartPos, - ); - assert(clusterIndex !== -1); - - return { - clusterIndex, - blockIndex: 0, - correctBlockFound: true, - }; - } + const trackData = cluster.trackData.get(this.internalTrack.id); + if (trackData) { + return { + blockIndex: 0, + correctBlockFound: true, + }; } - - return { - clusterIndex, - blockIndex: -1, - correctBlockFound: false, - }; } + + return { + blockIndex: -1, + correctBlockFound: false, + }; }, -Infinity, // Use -Infinity as a search timestamp to avoid using the cues Infinity, @@ -1961,7 +1954,23 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const timestampInTimescale = this.intoTimescale(timestamp); return this.performClusterLookup( - () => this.findKeyBlockInClustersForTimestamp(timestampInTimescale), + null, + (cluster) => { + const trackData = cluster.trackData.get(this.internalTrack.id); + if (!trackData) { + return { blockIndex: -1, correctBlockFound: false }; + } + + const index = findLastIndex(trackData.presentationTimestamps, (x) => { + const block = trackData.blocks[x.blockIndex]!; + return block.isKeyFrame && x.timestamp <= timestampInTimescale; + }); + + const blockIndex = index !== -1 ? trackData.presentationTimestamps[index]!.blockIndex : -1; + const correctBlockFound = index !== -1 && timestampInTimescale < trackData.endTimestamp; + + return { blockIndex, correctBlockFound }; + }, timestampInTimescale, timestampInTimescale, options, @@ -1974,60 +1983,39 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { throw new Error('Packet was not created from this track.'); } - const trackData = locationInCluster.cluster.trackData.get(this.internalTrack.id)!; - - const clusterIndex = binarySearchExact( - this.internalTrack.clusters, - locationInCluster.cluster.elementStartPos, - x => x.elementStartPos, - ); - assert(clusterIndex !== -1); - return this.performClusterLookup( - () => { - const nextKeyFrameIndex = trackData.blocks.findIndex( - (x, i) => x.isKeyFrame && i > locationInCluster.blockIndex, - ); + locationInCluster.cluster, + (cluster) => { + if (cluster === locationInCluster.cluster) { + const trackData = cluster.trackData.get(this.internalTrack.id)!; + const nextKeyFrameIndex = trackData.blocks.findIndex( + (x, i) => x.isKeyFrame && i > locationInCluster.blockIndex, + ); - if (nextKeyFrameIndex !== -1) { - // We can simply take the next key frame in the cluster - return { - clusterIndex, - blockIndex: nextKeyFrameIndex, - correctBlockFound: true, - }; - } else { - // Walk the list of clusters until we find the next cluster for this track with a key frame - let currentCluster = locationInCluster.cluster; - while (currentCluster.nextCluster) { - currentCluster = currentCluster.nextCluster; - - const trackData = currentCluster.trackData.get(this.internalTrack.id); - if (trackData && trackData.firstKeyFrameTimestamp !== null) { - const clusterIndex = binarySearchExact( - this.internalTrack.clusters, - currentCluster.elementStartPos, - x => x.elementStartPos, - ); - assert(clusterIndex !== -1); - - const keyFrameIndex = trackData.blocks.findIndex(x => x.isKeyFrame); - assert(keyFrameIndex !== -1); // There must be one - - return { - clusterIndex, - blockIndex: keyFrameIndex, - correctBlockFound: true, - }; - } + if (nextKeyFrameIndex !== -1) { + // We can simply take the next key frame in the cluster + return { + blockIndex: nextKeyFrameIndex, + correctBlockFound: true, + }; } + } else { + const trackData = cluster.trackData.get(this.internalTrack.id); + if (trackData && trackData.firstKeyFrameTimestamp !== null) { + const keyFrameIndex = trackData.blocks.findIndex(x => x.isKeyFrame); + assert(keyFrameIndex !== -1); // There must be one - return { - clusterIndex, - blockIndex: -1, - correctBlockFound: false, - }; + return { + blockIndex: keyFrameIndex, + correctBlockFound: true, + }; + } } + + return { + blockIndex: -1, + correctBlockFound: false, + }; }, -Infinity, // Use -Infinity as a search timestamp to avoid using the cues Infinity, @@ -2075,77 +2063,12 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return packet; } - private findBlockInClustersForTimestamp(timestampInTimescale: number) { - const clusterIndex = binarySearchLessOrEqual( - // This array is technically not sorted by start timestamp, but for any reasonable file, it basically is. - this.internalTrack.clusters, - timestampInTimescale, - x => x.trackData.get(this.internalTrack.id)!.startTimestamp, - ); - let blockIndex = -1; - let correctBlockFound = false; - - if (clusterIndex !== -1) { - const cluster = this.internalTrack.clusters[clusterIndex]!; - const trackData = cluster.trackData.get(this.internalTrack.id)!; - - const index = binarySearchLessOrEqual( - trackData.presentationTimestamps, - timestampInTimescale, - x => x.timestamp, - ); - assert(index !== -1); - - blockIndex = trackData.presentationTimestamps[index]!.blockIndex; - correctBlockFound = timestampInTimescale < trackData.endTimestamp; - } - - return { clusterIndex, blockIndex, correctBlockFound }; - } - - private findKeyBlockInClustersForTimestamp(timestampInTimescale: number) { - const indexInKeyFrameClusters = binarySearchLessOrEqual( - // This array is technically not sorted by start timestamp, but for any reasonable file, it basically is. - this.internalTrack.clustersWithKeyFrame, - timestampInTimescale, - x => x.trackData.get(this.internalTrack.id)!.firstKeyFrameTimestamp!, - ); - - let clusterIndex = -1; - let blockIndex = -1; - let correctBlockFound = false; - - if (indexInKeyFrameClusters !== -1) { - const cluster = this.internalTrack.clustersWithKeyFrame[indexInKeyFrameClusters]!; - - // Now, let's find the actual index of the cluster in the list of ALL clusters, not just key frame ones - clusterIndex = binarySearchExact( - this.internalTrack.clusters, - cluster.elementStartPos, - x => x.elementStartPos, - ); - assert(clusterIndex !== -1); - - const trackData = cluster.trackData.get(this.internalTrack.id)!; - const index = findLastIndex(trackData.presentationTimestamps, (x) => { - const block = trackData.blocks[x.blockIndex]!; - return block.isKeyFrame && x.timestamp <= timestampInTimescale; - }); - assert(index !== -1); // It's a key frame cluster, so there must be a key frame - - const entry = trackData.presentationTimestamps[index]!; - blockIndex = entry.blockIndex; - correctBlockFound = timestampInTimescale < trackData.endTimestamp; - } - - return { clusterIndex, blockIndex, correctBlockFound }; - } - /** Looks for a packet in the clusters while trying to load as few clusters as possible to retrieve it. */ private async performClusterLookup( - // This function returns the best-matching block that is currently loaded. Based on this information, we know - // which clusters we need to load to find the actual match. - getBestMatch: () => { clusterIndex: number; blockIndex: number; correctBlockFound: boolean }, + // The cluster where we start looking + startCluster: Cluster | null, + // This function returns the best-matching block in a given cluster + getMatchInCluster: (cluster: Cluster) => { blockIndex: number; correctBlockFound: boolean }, // The timestamp with which we can search the lookup table searchTimestamp: number, // The timestamp for which we know the correct block will not come after it @@ -2153,19 +2076,24 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { options: PacketRetrievalOptions, ): Promise { const { demuxer, segment } = this.internalTrack; - const release = await segment.clusterLookupMutex.acquire(); // The algorithm requires exclusivity try { - const { clusterIndex, blockIndex, correctBlockFound } = getBestMatch(); - if (correctBlockFound) { - // The correct block already exists, easy path. - const cluster = this.internalTrack.clusters[clusterIndex]!; - return this.fetchPacketInCluster(cluster, blockIndex, options); - } + let currentCluster: Cluster | null = null; + let bestCluster: Cluster | null = null; + let bestBlockIndex = -1; - let prevCluster: Cluster | null = null; - let bestClusterIndex = clusterIndex; - let bestBlockIndex = blockIndex; + if (startCluster) { + const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster); + + if (correctBlockFound) { + return this.fetchPacketInCluster(startCluster, blockIndex, options); + } + + if (blockIndex !== -1) { + bestCluster = startCluster; + bestBlockIndex = blockIndex; + } + } // Search for a cue point; this way, we won't need to start searching from the start of the file // but can jump right into the correct cluster (or at least nearby). @@ -2174,40 +2102,46 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { searchTimestamp, x => x.time, ); - const cuePoint = cuePointIndex !== -1 ? this.internalTrack.cuePoints[cuePointIndex]! : null; + const cuePoint = cuePointIndex !== -1 + ? this.internalTrack.cuePoints[cuePointIndex]! + : null; + + // Also check the position cache + const positionCacheIndex = binarySearchLessOrEqual( + this.internalTrack.clusterPositionCache, + searchTimestamp, + x => x.startTimestamp, + ); + const positionCacheEntry = positionCacheIndex !== -1 + ? this.internalTrack.clusterPositionCache[positionCacheIndex]! + : null; + + const lookupEntryPosition = Math.max( + cuePoint?.clusterPosition ?? 0, + positionCacheEntry?.elementStartPos ?? 0, + ) || null; let currentPos: number; - let nextClusterIsFirstCluster = false; - if (clusterIndex === -1) { - currentPos = cuePoint?.clusterPosition ?? segment.clusterSeekStartPos; - nextClusterIsFirstCluster = currentPos === segment.clusterSeekStartPos; + if (!startCluster) { + currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos; } else { - const cluster = this.internalTrack.clusters[clusterIndex]!; - - if (!cuePoint || cluster.elementStartPos >= cuePoint.clusterPosition) { - currentPos = cluster.elementEndPos; - prevCluster = cluster; + if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) { + currentPos = startCluster.elementEndPos; + currentCluster = startCluster; } else { // Use the lookup entry - currentPos = cuePoint.clusterPosition; + currentPos = lookupEntryPosition; } } while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) { - if (prevCluster) { - const trackData = prevCluster.trackData.get(this.internalTrack.id); + if (currentCluster) { + const trackData = currentCluster.trackData.get(this.internalTrack.id); if (trackData && trackData.startTimestamp > latestTimestamp) { // We're already past the upper bound, no need to keep searching break; } - - if (prevCluster.nextCluster) { - // Skip ahead quickly without needing to read the file again - currentPos = prevCluster.nextCluster.elementEndPos; - prevCluster = prevCluster.nextCluster; - continue; - } } // Load the header @@ -2244,33 +2178,15 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const dataStartPos = slice.filePos; if (id === EBMLId.Cluster) { - const index = binarySearchExact(segment.clusters, elementStartPos, x => x.elementStartPos); + currentCluster = await demuxer.readCluster(elementStartPos, segment); - let cluster: Cluster; - if (index === -1) { - // This is the first time we've seen this cluster - cluster = await demuxer.readCluster(elementStartPos, segment); - } else { - // We already know this cluster - cluster = segment.clusters[index]!; - } - - // Even if we already know the cluster, we might not yet know its predecessor, so always do this - if (prevCluster) prevCluster.nextCluster = cluster; - prevCluster = cluster; - - if (nextClusterIsFirstCluster) { - cluster.isKnownToBeFirstCluster = true; - nextClusterIsFirstCluster = false; - } - - const { clusterIndex, blockIndex, correctBlockFound } = getBestMatch(); + const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster); if (correctBlockFound) { - const cluster = this.internalTrack.clusters[clusterIndex]!; - return this.fetchPacketInCluster(cluster, blockIndex, options); + return this.fetchPacketInCluster(currentCluster, blockIndex, options); } - if (clusterIndex !== -1) { - bestClusterIndex = clusterIndex; + + if (blockIndex !== -1) { + bestCluster = currentCluster; bestBlockIndex = blockIndex; } } @@ -2281,8 +2197,8 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { if (id === EBMLId.Cluster) { // The cluster should have already computed its length, we can just copy that result - assert(prevCluster); - size = prevCluster.elementEndPos - dataStartPos; + assert(currentCluster); + size = currentCluster.elementEndPos - dataStartPos; } else { // Search for the next element at level 0 or 1 const nextElementPos = await searchForNextElementId( @@ -2319,15 +2235,13 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { currentPos = dataStartPos + size; } - const bestCluster = bestClusterIndex !== -1 ? this.internalTrack.clusters[bestClusterIndex]! : null; - // Catch faulty cue points if (cuePoint && (!bestCluster || bestCluster.elementStartPos < cuePoint.clusterPosition)) { // The cue point lied to us! We found a cue point but no cluster there that satisfied the match. In this // case, let's search again but using the cue point before that. const previousCuePoint = this.internalTrack.cuePoints[cuePointIndex - 1]; const newSearchTimestamp = previousCuePoint?.time ?? -Infinity; - return this.performClusterLookup(getBestMatch, newSearchTimestamp, latestTimestamp, options); + return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options); } if (bestCluster) { @@ -2337,7 +2251,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return null; } finally { - release(); + // release(); } } } diff --git a/src/media-sink.ts b/src/media-sink.ts index 793b0b7..1d0665a 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -25,6 +25,7 @@ import { getUint24, insertSorted, isFirefox, + isNumber, isSafari, last, mapAsyncGenerator, @@ -75,7 +76,7 @@ const validatePacketRetrievalOptions = (options: PacketRetrievalOptions) => { }; const validateTimestamp = (timestamp: number) => { - if (typeof timestamp !== 'number' || Number.isNaN(timestamp)) { + if (!isNumber(timestamp)) { throw new TypeError('timestamp must be a number.'); // It can be non-finite, that's fine } }; diff --git a/src/misc.ts b/src/misc.ts index c39de08..ba26c5d 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -786,3 +786,7 @@ export const polyfillSymbolDispose = () => { // @ts-expect-error Readonly Symbol.dispose ??= Symbol('Symbol.dispose'); }; + +export const isNumber = (x: unknown) => { + return typeof x === 'number' && !Number.isNaN(x); +}; diff --git a/src/source.ts b/src/source.ts index 594393a..21ee13f 100644 --- a/src/source.ts +++ b/src/source.ts @@ -11,6 +11,7 @@ import { assert, binarySearchLessOrEqual, closedIntervalsOverlap, + isNumber, MaybePromise, mergeRequestInit, promiseWithResolvers, @@ -172,9 +173,9 @@ export class BlobSource extends Source { } if ( options.maxCacheSize !== undefined - && (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0) + && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0) ) { - throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.'); + throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.'); } super(); @@ -331,9 +332,9 @@ export class UrlSource extends Source { } if ( options.maxCacheSize !== undefined - && (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0) + && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0) ) { - throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.'); + throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.'); } if (options.fetchFn !== undefined && typeof options.fetchFn !== 'function') { throw new TypeError('options.fetchFn, when provided, must be a function.'); @@ -591,9 +592,9 @@ export class FilePathSource extends Source { } if ( options.maxCacheSize !== undefined - && (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0) + && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0) ) { - throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.'); + throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.'); } super(); @@ -704,9 +705,9 @@ export class StreamSource extends Source { } if ( options.maxCacheSize !== undefined - && (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0) + && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0) ) { - throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.'); + throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.'); } if (options.prefetchProfile && !['none', 'fileSystem', 'network'].includes(options.prefetchProfile)) { throw new TypeError( @@ -884,9 +885,9 @@ export class ReadableStreamSource extends Source { } if ( options.maxCacheSize !== undefined - && (!Number.isInteger(options.maxCacheSize) || options.maxCacheSize < 0) + && (!isNumber(options.maxCacheSize) || options.maxCacheSize < 0) ) { - throw new TypeError('options.maxCacheSize, when provided, must be a non-negative integer.'); + throw new TypeError('options.maxCacheSize, when provided, must be a non-negative number.'); } super();