diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index db421b6..1135cf6 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -42,10 +42,12 @@ import { last, MATRIX_COEFFICIENTS_MAP_INVERSE, normalizeRotation, + ResultValue, Rotation, roundIfAlmostInteger, TRANSFER_CHARACTERISTICS_MAP_INVERSE, UNDETERMINED_LANGUAGE, + Yo, } from '../misc'; import { EncodedPacket, EncodedPacketSideData, PLACEHOLDER_DATA } from '../packet'; import { @@ -265,12 +267,6 @@ export class MatroskaDemuxer extends Demuxer { this.reader = input._reader; } - override async computeDuration() { - const tracks = await this.getTracks(); - const trackDurations = await Promise.all(tracks.map(x => x.computeDuration())); - return Math.max(0, ...trackDurations); - } - async getTracks() { await this.readMetadata(); return this.segments.flatMap(segment => segment.tracks.map(track => track.inputTrack!)); @@ -587,9 +583,9 @@ export class MatroskaDemuxer extends Demuxer { this.currentSegment = null; } - async readCluster(startPos: number, segment: Segment) { + async readCluster(res: ResultValue, startPos: number, segment: Segment): Promise { if (segment.lastReadCluster?.elementStartPos === startPos) { - return segment.lastReadCluster; + return res.set(segment.lastReadCluster); } let headerSlice = this.reader.requestSliceRange(startPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); @@ -719,7 +715,7 @@ export class MatroskaDemuxer extends Demuxer { } segment.lastReadCluster = cluster; - return cluster; + return res.set(cluster); } getTrackDataInCluster(cluster: Cluster, trackNumber: number) { @@ -1838,11 +1834,6 @@ export class MatroskaDemuxer extends Demuxer { } abstract class MatroskaTrackBacking implements InputTrackBacking { - packetToClusterLocation = new WeakMap(); - constructor(public internalTrack: InternalTrack) {} getId() { @@ -1857,11 +1848,6 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.internalTrack.codecId; } - async computeDuration() { - const lastPacket = await this.getPacket(Infinity, { metadataOnly: true }); - return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0); - } - getName() { return this.internalTrack.name; } @@ -1870,11 +1856,6 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.internalTrack.languageCode; } - async getFirstTimestamp() { - const firstPacket = await this.getFirstPacket({ metadataOnly: true }); - return firstPacket?.timestamp ?? 0; - } - getTimeResolution() { return this.internalTrack.segment.timestampFactor; } @@ -1883,8 +1864,9 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.internalTrack.disposition; } - async getFirstPacket(options: PacketRetrievalOptions) { + async getFirstPacket(res: ResultValue, options: PacketRetrievalOptions): Promise { return this.performClusterLookup( + res, null, (cluster) => { const trackData = cluster.trackData.get(this.internalTrack.id); @@ -1913,10 +1895,15 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return roundIfAlmostInteger(timestamp * this.internalTrack.segment.timestampFactor); } - async getPacket(timestamp: number, options: PacketRetrievalOptions) { + async getPacket( + res: ResultValue, + timestamp: number, + options: PacketRetrievalOptions, + ): Promise { const timestampInTimescale = this.intoTimescale(timestamp); return this.performClusterLookup( + res, null, (cluster) => { const trackData = cluster.trackData.get(this.internalTrack.id); @@ -1941,21 +1928,31 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ); } - async getNextPacket(packet: EncodedPacket, options: PacketRetrievalOptions) { - const locationInCluster = this.packetToClusterLocation.get(packet); - if (locationInCluster === undefined) { + async getNextPacket( + res: ResultValue, + packet: EncodedPacket, + options: PacketRetrievalOptions, + ): Promise { + const clusterStartPos = packet._internal; + if (clusterStartPos === undefined) { throw new Error('Packet was not created from this track.'); } + assert(typeof clusterStartPos === 'number'); + + // sequenceNumber = clusterStartPos + blockIndex, so: + const blockIndex = packet.sequenceNumber - clusterStartPos; + return this.performClusterLookup( - locationInCluster.cluster, + res, + clusterStartPos, (cluster) => { - if (cluster === locationInCluster.cluster) { + if (cluster.elementStartPos === clusterStartPos) { const trackData = cluster.trackData.get(this.internalTrack.id)!; - if (locationInCluster.blockIndex + 1 < trackData.blocks.length) { + if (blockIndex + 1 < trackData.blocks.length) { // We can simply take the next block in the cluster return { - blockIndex: locationInCluster.blockIndex + 1, + blockIndex: blockIndex + 1, correctBlockFound: true, }; } @@ -1980,10 +1977,15 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ); } - async getKeyPacket(timestamp: number, options: PacketRetrievalOptions) { + async getKeyPacket( + res: ResultValue, + timestamp: number, + options: PacketRetrievalOptions, + ): Promise { const timestampInTimescale = this.intoTimescale(timestamp); return this.performClusterLookup( + res, null, (cluster) => { const trackData = cluster.trackData.get(this.internalTrack.id); @@ -2007,19 +2009,29 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ); } - async getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) { - const locationInCluster = this.packetToClusterLocation.get(packet); - if (locationInCluster === undefined) { + async getNextKeyPacket( + res: ResultValue, + packet: EncodedPacket, + options: PacketRetrievalOptions, + ): Promise { + const clusterStartPos = packet._internal; + if (clusterStartPos === undefined) { throw new Error('Packet was not created from this track.'); } + assert(typeof clusterStartPos === 'number'); + + // sequenceNumber = clusterStartPos + blockIndex, so: + const blockIndex = packet.sequenceNumber - clusterStartPos; + return this.performClusterLookup( - locationInCluster.cluster, + res, + clusterStartPos, (cluster) => { - if (cluster === locationInCluster.cluster) { + if (cluster.elementStartPos === clusterStartPos) { const trackData = cluster.trackData.get(this.internalTrack.id)!; const nextKeyFrameIndex = trackData.blocks.findIndex( - (x, i) => x.isKeyFrame && i > locationInCluster.blockIndex, + (x, i) => x.isKeyFrame && i > blockIndex, ); if (nextKeyFrameIndex !== -1) { @@ -2053,9 +2065,14 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ); } - private async fetchPacketInCluster(cluster: Cluster, blockIndex: number, options: PacketRetrievalOptions) { + private fetchPacketInCluster( + res: ResultValue, + cluster: Cluster, + blockIndex: number, + options: PacketRetrievalOptions, + ): Yo { if (blockIndex === -1) { - return null; + return res.set(null); } const trackData = cluster.trackData.get(this.internalTrack.id)!; @@ -2083,20 +2100,21 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { block.isKeyFrame ? 'key' : 'delta', timestamp, duration, - cluster.dataStartPos + blockIndex, + cluster.elementStartPos + blockIndex, block.data.byteLength, sideData, ); - this.packetToClusterLocation.set(packet, { cluster, blockIndex }); + packet._internal = cluster.elementStartPos; - return packet; + return res.set(packet); } /** Looks for a packet in the clusters while trying to load as few clusters as possible to retrieve it. */ private async performClusterLookup( - // The cluster where we start looking - startCluster: Cluster | null, + res: ResultValue, + // The position where we'll start the lookup + startOffset: number | 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 @@ -2104,20 +2122,25 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { // The timestamp for which we know the correct block will not come after it latestTimestamp: number, options: PacketRetrievalOptions, - ): Promise { + ): Promise { const { demuxer, segment } = this.internalTrack; + let currentPos = startOffset ?? 0; let currentCluster: Cluster | null = null; let bestCluster: Cluster | null = null; let bestBlockIndex = -1; - if (startCluster) { + if (startOffset !== null && segment.lastReadCluster?.elementStartPos === startOffset) { + const startCluster = segment.lastReadCluster; const { blockIndex, correctBlockFound } = getMatchInCluster(startCluster); if (correctBlockFound) { - return this.fetchPacketInCluster(startCluster, blockIndex, options); + return this.fetchPacketInCluster(res, startCluster, blockIndex, options); } + currentPos = startCluster.elementEndPos; // Start reading from the next cluster + currentCluster = startCluster; + if (blockIndex !== -1) { bestCluster = startCluster; bestBlockIndex = blockIndex; @@ -2150,18 +2173,14 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { positionCacheEntry?.elementStartPos ?? 0, ) || null; - let currentPos: number; + if (lookupEntryPosition !== null && lookupEntryPosition > currentPos) { + // Use the lookup entry + currentPos = lookupEntryPosition; + currentCluster = null; + } - if (!startCluster) { + if (startOffset === null) { 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) { @@ -2207,13 +2226,18 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const dataStartPos = slice.filePos; if (id === EBMLId.Cluster) { - currentCluster = await demuxer.readCluster(elementStartPos, segment); + const result = new ResultValue(); + const promise = demuxer.readCluster(result, elementStartPos, segment); + if (result.pending) await promise; + + currentCluster = result.value; + // readCluster computes the proper size even if it's undefined in the header, so let's use that instead size = currentCluster.elementEndPos - dataStartPos; const { blockIndex, correctBlockFound } = getMatchInCluster(currentCluster); if (correctBlockFound) { - return this.fetchPacketInCluster(currentCluster, blockIndex, options); + return this.fetchPacketInCluster(res, currentCluster, blockIndex, options); } if (blockIndex !== -1) { @@ -2267,15 +2291,17 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { assert(!previousCuePoint || previousCuePoint.time < cuePoint.time); const newSearchTimestamp = previousCuePoint?.time ?? -Infinity; - return this.performClusterLookup(null, getMatchInCluster, newSearchTimestamp, latestTimestamp, options); + return this.performClusterLookup( + res, 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 this.fetchPacketInCluster(res, bestCluster, bestBlockIndex, options); } - return null; + return res.set(null); } } @@ -2333,7 +2359,11 @@ class MatroskaVideoTrackBacking extends MatroskaTrackBacking implements InputVid || (this.internalTrack.info.codec === 'hevc' && !this.internalTrack.info.codecDescription); if (needsPacketForAdditionalInfo) { - firstPacket = await this.getFirstPacket({}); + const result = new ResultValue(); + const promise = this.getFirstPacket(result, {}); + if (result.pending) await promise; + + firstPacket = result.value; } return { diff --git a/src/reader.ts b/src/reader.ts index ebcda44..4b4a2e5 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -175,7 +175,7 @@ export const readBytes = (slice: FileSlice, length: number) => { export const readU8 = (slice: FileSlice) => { checkIsInRange(slice, 1); - return slice.view.getUint8(slice.bufferPos++); + return slice.bytes[slice.bufferPos++]!; }; export const readU16 = (slice: FileSlice, littleEndian: boolean) => { diff --git a/test/node/demux.test.ts b/test/node/demux.test.ts index 1eb79b7..89efc4f 100644 --- a/test/node/demux.test.ts +++ b/test/node/demux.test.ts @@ -3,7 +3,7 @@ import { Input } from '../../src/input.js'; import { BufferSource, FilePathSource } from '../../src/source.js'; import path from 'node:path'; import fs from 'node:fs'; -import { ALL_FORMATS, MP3, MP4, QTFF } from '../../src/input-format.js'; +import { ALL_FORMATS, MP3, MP4, QTFF, WEBM } from '../../src/input-format.js'; import { PacketReader } from '../../src/cursors.js'; import { InputAudioTrack, InputTrack } from '../../src/input-track.js'; import { assert } from '../../src/misc.js'; @@ -113,6 +113,38 @@ test('Fragmented ISOBMFF sync reading', async () => { expect(count).toBe(120); }); +test('Matroska demuxing', async () => { + using input = new Input({ + source: new FilePathSource(path.join(__dirname, '../public/trim-buck-bunny.webm')), + formats: ALL_FORMATS, + }); + + expect(await input.getFormat()).toBe(WEBM); + + const tracks = await input.getTracks(); + expect(tracks).toHaveLength(2); + + const videoTrack = await input.getPrimaryVideoTrack(); + assert(videoTrack); + + await testBasicPacketReading(videoTrack); + + expect(await input.computeDuration()).toBeCloseTo(5); +}); + +test('Matroska sync reading', async () => { + using input = new Input({ + source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/trim-buck-bunny.webm'))), + formats: ALL_FORMATS, + }); + + const videoTrack = await input.getPrimaryVideoTrack(); + assert(videoTrack); + + const count = testSyncPacketReading(videoTrack); + expect(count).toBe(120); +}); + test('MP3 demuxing', async () => { using input = new Input({ source: new FilePathSource(path.join(__dirname, '../public/AudacityTest1.mp3')), diff --git a/test/public/trim-buck-bunny.webm b/test/public/trim-buck-bunny.webm new file mode 100644 index 0000000..f0ee7dc Binary files /dev/null and b/test/public/trim-buck-bunny.webm differ