mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 05:13:50 +02:00
Make cluster/fragment lookup operations almost stateless, fix high Matroska memory, allow more maxCacheSizes
This commit is contained in:
+35
-5
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user