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();