Make cluster/fragment lookup operations almost stateless, fix high Matroska memory, allow more maxCacheSizes

This commit is contained in:
Vanilagy
2025-09-29 20:36:37 +02:00
parent 0185383794
commit 198928088b
6 changed files with 465 additions and 666 deletions
+35 -5
View File
@@ -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);
+229 -380
View File
@@ -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<InternalTrack['id'], FragmentTrackData>;
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<Fragment> {
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<EncodedPacket | null> {
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();
}
}
}
+184 -270
View File
@@ -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<number, ClusterTrackData>;
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<EncodedPacket | null> {
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();
}
}
}
+2 -1
View File
@@ -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
}
};
+4
View File
@@ -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);
};
+11 -10
View File
@@ -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();