diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index eac99a4..6c7de90 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -2651,122 +2651,118 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { ): Promise { const demuxer = this.internalTrack.demuxer; - try { - let currentFragment: Fragment | null = null; - let bestFragment: Fragment | null = null; - let bestSampleIndex = -1; + let currentFragment: Fragment | null = null; + let bestFragment: Fragment | null = null; + let bestSampleIndex = -1; - if (startFragment) { - const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment); + if (startFragment) { + const { sampleIndex, correctSampleFound } = getMatchInFragment(startFragment); - if (correctSampleFound) { - return this.fetchPacketInFragment(startFragment, sampleIndex, options); + 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 = binarySearchLessOrEqual( + this.internalTrack.fragmentLookupTable, + searchTimestamp, + x => x.timestamp, + ); + const lookupEntry = lookupEntryIndex !== -1 + ? 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; + + if (!startFragment) { + currentPos = lookupEntryPosition ?? 0; + } else { + if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) { + currentPos = startFragment.moofOffset + startFragment.moofSize; + currentFragment = startFragment; + } else { + // Use the lookup entry + currentPos = lookupEntryPosition; + } + } + + while (true) { + 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; } + } + // Load the header + let slice = demuxer.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; + } + + if (boxInfo.name === 'moof') { + currentFragment = await demuxer.readFragment(boxStartPos); + const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment); + if (correctSampleFound) { + return this.fetchPacketInFragment(currentFragment, sampleIndex, options); + } if (sampleIndex !== -1) { - bestFragment = startFragment; + bestFragment = currentFragment; 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 = binarySearchLessOrEqual( - this.internalTrack.fragmentLookupTable, - searchTimestamp, - x => x.timestamp, - ); - const lookupEntry = lookupEntryIndex !== -1 - ? 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; - - if (!startFragment) { - currentPos = lookupEntryPosition ?? 0; - } else { - if (lookupEntryPosition === null || startFragment.moofOffset >= lookupEntryPosition) { - currentPos = startFragment.moofOffset + startFragment.moofSize; - currentFragment = startFragment; - } else { - // Use the lookup entry - currentPos = lookupEntryPosition; - } - } - - while (true) { - 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; - } - } - - // Load the header - let slice = demuxer.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; - } - - if (boxInfo.name === 'moof') { - currentFragment = await demuxer.readFragment(boxStartPos); - const { sampleIndex, correctSampleFound } = getMatchInFragment(currentFragment); - if (correctSampleFound) { - return this.fetchPacketInFragment(currentFragment, sampleIndex, options); - } - if (sampleIndex !== -1) { - bestFragment = currentFragment; - bestSampleIndex = sampleIndex; - } - } - - currentPos = boxStartPos + boxInfo.totalSize; - } - - // 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 newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity; - return this.performFragmentedLookup( - null, - getMatchInFragment, - newSearchTimestamp, - latestTimestamp, - options, - ); - } - - if (bestFragment) { - // If we finished looping but didn't find a perfect match, still return the best match we found - return this.fetchPacketInFragment(bestFragment, bestSampleIndex, options); - } - - return null; - } finally { - // release(); + currentPos = boxStartPos + boxInfo.totalSize; } + + // 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 newSearchTimestamp = previousLookupEntry?.timestamp ?? -Infinity; + return this.performFragmentedLookup( + null, + getMatchInFragment, + newSearchTimestamp, + latestTimestamp, + options, + ); + } + + if (bestFragment) { + // If we finished looping but didn't find a perfect match, still return the best match we found + return this.fetchPacketInFragment(bestFragment, bestSampleIndex, options); + } + + return null; } } diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index 45e0726..865745f 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -2077,182 +2077,178 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ): Promise { const { demuxer, segment } = this.internalTrack; - try { - let currentCluster: Cluster | null = null; - let bestCluster: Cluster | null = null; - let bestBlockIndex = -1; + let currentCluster: Cluster | null = null; + let bestCluster: Cluster | null = null; + let bestBlockIndex = -1; - if (startCluster) { - const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster); + 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). + const cuePointIndex = binarySearchLessOrEqual( + this.internalTrack.cuePoints, + searchTimestamp, + x => x.time, + ); + 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; + + if (!startCluster) { + currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos; + } else { + if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) { + currentPos = startCluster.elementEndPos; + currentCluster = startCluster; + } else { + // Use the lookup entry + currentPos = lookupEntryPosition; + } + } + + while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) { + 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; + } + } + + // Load the header + let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + + const elementStartPos = currentPos; + const elementHeader = readElementHeader(slice); + + if ( + !elementHeader + || (!LEVEL_1_EBML_IDS.includes(elementHeader.id) && elementHeader.id !== EBMLId.Void) + ) { + // There's an element here that shouldn't be here. Might be garbage. In this case, let's + // try and resync to the next valid element. + const nextPos = await resync( + demuxer.reader, + elementStartPos, + LEVEL_1_EBML_IDS, + Math.min(segment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH), + ); + + if (nextPos) { + currentPos = nextPos; + continue; + } else { + break; // Resync failed + } + } + + const id = elementHeader.id; + let size = elementHeader.size; + const dataStartPos = slice.filePos; + + if (id === EBMLId.Cluster) { + currentCluster = await demuxer.readCluster(elementStartPos, segment); + + const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster); if (correctBlockFound) { - return this.fetchPacketInCluster(startCluster, blockIndex, options); + return this.fetchPacketInCluster(currentCluster, blockIndex, options); } if (blockIndex !== -1) { - bestCluster = startCluster; + bestCluster = currentCluster; 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). - const cuePointIndex = binarySearchLessOrEqual( - this.internalTrack.cuePoints, - searchTimestamp, - x => x.time, - ); - 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; - - if (!startCluster) { - currentPos = lookupEntryPosition ?? segment.clusterSeekStartPos; - } else { - if (lookupEntryPosition === null || startCluster.elementStartPos >= lookupEntryPosition) { - currentPos = startCluster.elementEndPos; - currentCluster = startCluster; - } else { - // Use the lookup entry - currentPos = lookupEntryPosition; - } - } - - while (segment.elementEndPos === null || currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) { - 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; - } - } - - // Load the header - let slice = demuxer.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); - if (slice instanceof Promise) slice = await slice; - if (!slice) break; - - const elementStartPos = currentPos; - const elementHeader = readElementHeader(slice); - - if ( - !elementHeader - || (!LEVEL_1_EBML_IDS.includes(elementHeader.id) && elementHeader.id !== EBMLId.Void) - ) { - // There's an element here that shouldn't be here. Might be garbage. In this case, let's - // try and resync to the next valid element. - const nextPos = await resync( - demuxer.reader, - elementStartPos, - LEVEL_1_EBML_IDS, - Math.min(segment.elementEndPos ?? Infinity, elementStartPos + MAX_RESYNC_LENGTH), - ); - - if (nextPos) { - currentPos = nextPos; - continue; - } else { - break; // Resync failed - } - } - - const id = elementHeader.id; - let size = elementHeader.size; - const dataStartPos = slice.filePos; + if (size === null) { + // Undefined element size (can happen in livestreamed files). In this case, we need to do some + // searching to determine the actual size of the element. if (id === EBMLId.Cluster) { - currentCluster = await demuxer.readCluster(elementStartPos, segment); + // The cluster should have already computed its length, we can just copy that result + assert(currentCluster); + size = currentCluster.elementEndPos - dataStartPos; + } else { + // Search for the next element at level 0 or 1 + const nextElementPos = await searchForNextElementId( + demuxer.reader, + dataStartPos, + LEVEL_0_AND_1_EBML_IDS, + segment.elementEndPos, + ); - const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster); - if (correctBlockFound) { - return this.fetchPacketInCluster(currentCluster, blockIndex, options); - } - - if (blockIndex !== -1) { - bestCluster = currentCluster; - bestBlockIndex = blockIndex; - } + size = nextElementPos.pos - dataStartPos; } - if (size === null) { - // Undefined element size (can happen in livestreamed files). In this case, we need to do some - // searching to determine the actual size of the element. + const endPos = dataStartPos + size; + if (segment.elementEndPos !== null && endPos > segment.elementEndPos - MIN_HEADER_SIZE) { + // No more elements fit in this segment + break; + } else { + // Check the next element. If it's a new segment, we know this segment ends here. The new + // segment is just ignored, since we're likely in a livestreamed file and thus only care about + // the first segment. - if (id === EBMLId.Cluster) { - // The cluster should have already computed its length, we can just copy that result - assert(currentCluster); - size = currentCluster.elementEndPos - dataStartPos; - } else { - // Search for the next element at level 0 or 1 - const nextElementPos = await searchForNextElementId( - demuxer.reader, - dataStartPos, - LEVEL_0_AND_1_EBML_IDS, - segment.elementEndPos, - ); + let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; - size = nextElementPos.pos - dataStartPos; - } - - const endPos = dataStartPos + size; - if (segment.elementEndPos !== null && endPos > segment.elementEndPos - MIN_HEADER_SIZE) { - // No more elements fit in this segment + const elementId = readElementId(slice); + if (elementId === EBMLId.Segment) { + segment.elementEndPos = endPos; break; - } else { - // Check the next element. If it's a new segment, we know this segment ends here. The new - // segment is just ignored, since we're likely in a livestreamed file and thus only care about - // the first segment. - - let slice = demuxer.reader.requestSliceRange(endPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); - if (slice instanceof Promise) slice = await slice; - if (!slice) break; - - const elementId = readElementId(slice); - if (elementId === EBMLId.Segment) { - segment.elementEndPos = endPos; - break; - } } } - - currentPos = dataStartPos + size; } - // 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(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options); - } - - if (bestCluster) { - // If we finished looping but didn't find a perfect match, still return the best match we found - return this.fetchPacketInCluster(bestCluster, bestBlockIndex, options); - } - - return null; - } finally { - // release(); + currentPos = dataStartPos + size; } + + // 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(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options); + } + + if (bestCluster) { + // If we finished looping but didn't find a perfect match, still return the best match we found + return this.fetchPacketInCluster(bestCluster, bestBlockIndex, options); + } + + return null; } }