diff --git a/package-lock.json b/package-lock.json index f2a5af9..6535b63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediabunny", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediabunny", - "version": "1.4.3", + "version": "1.4.4", "license": "MPL-2.0", "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", diff --git a/package.json b/package.json index 6d9d8d3..72a0ee1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mediabunny", "author": "Vanilagy", - "version": "1.4.3", + "version": "1.4.4", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "type": "module", "main": "./dist/bundles/mediabunny.cjs", diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 2a99e8a..a3c3379 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -1941,44 +1941,46 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { } async getFirstPacket(options: PacketRetrievalOptions) { - if (this.internalTrack.demuxer.isFragmented) { - 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; - } - } - - return { - fragmentIndex: -1, - sampleIndex: -1, - correctSampleFound: false, - }; - }, - -Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries - Infinity, - options, - ); + const regularPacket = await this.fetchPacketForSampleIndex(0, options); + if (regularPacket || !this.internalTrack.demuxer.isFragmented) { + // If there's a non-fragmented packet, always prefer that + return regularPacket; } - return this.fetchPacketForSampleIndex(0, options); + 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; + } + } + + return { + fragmentIndex: -1, + sampleIndex: -1, + correctSampleFound: false, + }; + }, + -Infinity, // Use -Infinity as a search timestamp to avoid using the lookup entries + Infinity, + options, + ); } private mapTimestampIntoTimescale(timestamp: number) { @@ -1991,185 +1993,188 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { async getPacket(timestamp: number, options: PacketRetrievalOptions) { const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp); - if (this.internalTrack.demuxer.isFragmented) { - return this.performFragmentedLookup( - () => this.findSampleInFragmentsForTimestamp(timestampInTimescale), - timestampInTimescale, - timestampInTimescale, - options, - ); - } else { - const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); - const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale); - return this.fetchPacketForSampleIndex(sampleIndex, options); + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale); + const regularPacket = await this.fetchPacketForSampleIndex(sampleIndex, options); + + if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) { + // Prefer the non-fragmented packet + return regularPacket; } + + return this.performFragmentedLookup( + () => this.findSampleInFragmentsForTimestamp(timestampInTimescale), + timestampInTimescale, + timestampInTimescale, + options, + ); } async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) { - if (this.internalTrack.demuxer.isFragmented) { - const locationInFragment = this.packetToFragmentLocation.get(packet); - if (locationInFragment === undefined) { - throw new Error('Packet was not created from this track.'); - } + const regularSampleIndex = this.packetToSampleIndex.get(packet); - const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; - const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!; - - 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, - }; - } 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, - }; - } - } - - return { - fragmentIndex, - sampleIndex: -1, - correctSampleFound: false, - }; - } - }, - fragmentSample.presentationTimestamp, - Infinity, - options, - ); + if (regularSampleIndex !== undefined) { + // Prefer the non-fragmented packet + return this.fetchPacketForSampleIndex(regularSampleIndex + 1, options); } - const sampleIndex = this.packetToSampleIndex.get(packet); - if (sampleIndex === undefined) { + const locationInFragment = this.packetToFragmentLocation.get(packet); + if (locationInFragment === undefined) { throw new Error('Packet was not created from this track.'); } - return this.fetchPacketForSampleIndex(sampleIndex + 1, options); + + const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; + const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!; + + 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, + }; + } 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, + }; + } + } + + return { + fragmentIndex, + sampleIndex: -1, + correctSampleFound: false, + }; + } + }, + fragmentSample.presentationTimestamp, + Infinity, + options, + ); } async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) { const timestampInTimescale = this.mapTimestampIntoTimescale(timestamp); - if (this.internalTrack.demuxer.isFragmented) { - return this.performFragmentedLookup( - () => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale), - timestampInTimescale, - timestampInTimescale, - options, - ); - } - const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale); const keyFrameSampleIndex = sampleIndex === -1 ? -1 : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex); - return this.fetchPacketForSampleIndex(keyFrameSampleIndex, options); + const regularPacket = await this.fetchPacketForSampleIndex(keyFrameSampleIndex, options); + + if (!sampleTableIsEmpty(sampleTable) || !this.internalTrack.demuxer.isFragmented) { + // Prefer the non-fragmented packet + return regularPacket; + } + + return this.performFragmentedLookup( + () => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale), + timestampInTimescale, + timestampInTimescale, + options, + ); } async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) { - if (this.internalTrack.demuxer.isFragmented) { - const locationInFragment = this.packetToFragmentLocation.get(packet); - if (locationInFragment === undefined) { - throw new Error('Packet was not created from this track.'); - } - - const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; - const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!; - - 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, - ); - - 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, - }; - } - } - - return { - fragmentIndex, - sampleIndex: -1, - correctSampleFound: false, - }; - } - }, - fragmentSample.presentationTimestamp, - Infinity, - options, - ); + const regularSampleIndex = this.packetToSampleIndex.get(packet); + if (regularSampleIndex !== undefined) { + // Prefer the non-fragmented packet + const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); + const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, regularSampleIndex); + return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options); } - const sampleIndex = this.packetToSampleIndex.get(packet); - if (sampleIndex === undefined) { + const locationInFragment = this.packetToFragmentLocation.get(packet); + if (locationInFragment === undefined) { throw new Error('Packet was not created from this track.'); } - const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack); - const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex); - return this.fetchPacketForSampleIndex(nextKeyFrameSampleIndex, options); + + const trackData = locationInFragment.fragment.trackData.get(this.internalTrack.id)!; + const fragmentSample = trackData.samples[locationInFragment.sampleIndex]!; + + 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, + ); + + 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, + }; + } + } + + return { + fragmentIndex, + sampleIndex: -1, + correctSampleFound: false, + }; + } + }, + fragmentSample.presentationTimestamp, + Infinity, + options, + ); } private async fetchPacketForSampleIndex(sampleIndex: number, options: PacketRetrievalOptions) { @@ -2701,3 +2706,7 @@ const extractRotationFromMatrix = (matrix: TransformationMatrix) => { // Invert the rotation because matrices are post-multiplied in ISOBMFF return -Math.atan2(sinTheta, cosTheta) * (180 / Math.PI); }; + +const sampleTableIsEmpty = (sampleTable: SampleTable) => { + return sampleTable.sampleSizes.length === 0; +};