From 641cdd02e33f14fa06c58c691925aead049c4dbd Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 28 Aug 2025 17:24:31 +0200 Subject: [PATCH] Add new slice-based reader system, port all demuxers to new reader system, add optimized BlobSource --- dev/demux.html | 21 + shared/mp3-misc.ts | 56 +-- src/adts/adts-demuxer.ts | 124 ++--- src/adts/adts-reader.ts | 127 +++--- src/input-format.ts | 153 +++---- src/input.ts | 4 + src/isobmff/isobmff-demuxer.ts | 714 +++++++++++++++-------------- src/isobmff/isobmff-reader.ts | 147 ++---- src/matroska/ebml.ts | 395 +++++++--------- src/matroska/matroska-demuxer.ts | 363 ++++++++------- src/mp3/mp3-demuxer.ts | 143 +++--- src/mp3/mp3-reader.ts | 106 ++--- src/ogg/ogg-demuxer.ts | 760 +++++++++++++++---------------- src/ogg/ogg-reader.ts | 150 ++---- src/reader2.ts | 294 ++++++++++++ src/source.ts | 357 ++++++++++++++- src/wave/wave-demuxer.ts | 88 ++-- 17 files changed, 2230 insertions(+), 1772 deletions(-) create mode 100644 src/reader2.ts diff --git a/dev/demux.html b/dev/demux.html index 29f4027..9cb536d 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -16,11 +16,32 @@ source }); + const audioTrack = await input.getPrimaryAudioTrack(); + + for (let i = 0; i < 10; i++) { + console.time() + const stats = await audioTrack.computePacketStats(); + console.log(stats) + console.timeEnd() + } + //console.log(stats); + + /* const videoTrack = await input.getPrimaryVideoTrack(); console.log(await videoTrack.getFirstTimestamp(), await videoTrack.computeDuration()); console.log(videoTrack.name); console.log(videoTrack.internalCodecId); + */ + + /* + + const sink = new Mediabunny.AudioSampleSink(await input.getPrimaryAudioTrack()); + for await (const sample of sink.samples()) { + console.log(sample) + break; + } + */ /* const sink = new Mediabunny.EncodedPacketSink(videoTrack); diff --git a/shared/mp3-misc.ts b/shared/mp3-misc.ts index fdd9554..67358f9 100644 --- a/shared/mp3-misc.ts +++ b/shared/mp3-misc.ts @@ -40,7 +40,6 @@ export const XING = 0x58696e67; export const INFO = 0x496e666f; export type FrameHeader = { - startPos: number; totalSize: number; mpegVersionId: number; layer: number; @@ -70,27 +69,28 @@ export const getXingOffset = (mpegVersionId: number, channel: number) => { : (channel === 3 ? 13 : 21); }; -export const readFrameHeader = (word: number, reader: { pos: number; fileSize: number | null }): FrameHeader | null => { - const startPos = reader.pos; - +export const readFrameHeader = (word: number, remainingBytes: number | null): { + header: FrameHeader | null; + bytesAdvanced: number; +} => { const firstByte = word >>> 24; const secondByte = (word >>> 16) & 0xff; const thirdByte = (word >>> 8) & 0xff; const fourthByte = word & 0xff; if (firstByte !== 0xff && secondByte !== 0xff && thirdByte !== 0xff && fourthByte !== 0xff) { - reader.pos += 4; - return null; + return { + header: null, + bytesAdvanced: 4, + }; } - reader.pos += 1; - if (firstByte !== 0xff) { - return null; + return { header: null, bytesAdvanced: 1 }; } if ((secondByte & 0xe0) !== 0xe0) { - return null; + return { header: null, bytesAdvanced: 1 }; } const mpegVersionId = (secondByte >> 3) & 0x3; @@ -110,21 +110,21 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n ? MPEG_V1_BITRATES[layer]?.[bitrateIndex] : MPEG_V2_BITRATES[layer]?.[bitrateIndex]; if (!kilobitRate || kilobitRate === -1) { - return null; + return { header: null, bytesAdvanced: 1 }; } const bitrate = kilobitRate * 1000; const sampleRate = SAMPLING_RATES[mpegVersionId]?.[frequencyIndex]; if (!sampleRate || sampleRate === -1) { - return null; + return { header: null, bytesAdvanced: 1 }; } const frameLength = computeMp3FrameSize(layer, bitrate, sampleRate, padding); - if (reader.fileSize !== null && reader.fileSize - startPos < frameLength) { + if (remainingBytes !== null && remainingBytes < frameLength) { // The frame doesn't fit into the rest of the file - return null; + return { header: null, bytesAdvanced: 1 }; } let audioSamplesInFrame: number; @@ -141,18 +141,20 @@ export const readFrameHeader = (word: number, reader: { pos: number; fileSize: n } return { - startPos: startPos, - totalSize: frameLength, - mpegVersionId, - layer, - bitrate, - frequencyIndex, - sampleRate, - channel, - modeExtension, - copyright, - original, - emphasis, - audioSamplesInFrame, + header: { + totalSize: frameLength, + mpegVersionId, + layer, + bitrate, + frequencyIndex, + sampleRate, + channel, + modeExtension, + copyright, + original, + emphasis, + audioSamplesInFrame, + }, + bytesAdvanced: 1, }; }; diff --git a/src/adts/adts-demuxer.ts b/src/adts/adts-demuxer.ts index e6d6f32..61b8d82 100644 --- a/src/adts/adts-demuxer.ts +++ b/src/adts/adts-demuxer.ts @@ -20,7 +20,8 @@ import { UNDETERMINED_LANGUAGE, } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; -import { AdtsReader, FrameHeader, MAX_FRAME_HEADER_SIZE } from './adts-reader'; +import { readBytes, Reader2 } from '../reader2'; +import { FrameHeader, MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts-reader'; const SAMPLES_PER_AAC_FRAME = 1024; @@ -32,15 +33,16 @@ type Sample = { }; export class AdtsDemuxer extends Demuxer { - reader: AdtsReader; + reader: Reader2; metadataPromise: Promise | null = null; firstFrameHeader: FrameHeader | null = null; - loadedSamples: Sample[] = []; // All samples from the start of the file to lastLoadedPos + loadedSamples: Sample[] = []; tracks: InputAudioTrack[] = []; readingMutex = new AsyncMutex(); + lastSampleLoaded = false; lastLoadedPos = 0; fileSize = 0; nextTimestampInSamples = 0; @@ -48,14 +50,19 @@ export class AdtsDemuxer extends Demuxer { constructor(input: Input) { super(input); - this.reader = new AdtsReader(input._mainReader); + this.reader = input._reader2; } async readMetadata() { return this.metadataPromise ??= (async () => { - this.fileSize = await this.input.source.getSize(); + let fileSize = this.reader.requestSize(); + if (fileSize instanceof Promise) fileSize = await fileSize; + this.fileSize = fileSize; - await this.loadNextChunk(); + // Keep loading until we find the first frame header + while (!this.firstFrameHeader && !this.lastSampleLoaded) { + await this.advanceReader(); + } // There has to be a frame if this demuxer got selected assert(this.firstFrameHeader); @@ -65,55 +72,45 @@ export class AdtsDemuxer extends Demuxer { })(); } - async loadNextChunk() { - assert(this.lastLoadedPos < this.fileSize); - - const chunkSize = 0.5 * 1024 * 1024; // 0.5 MiB - const endPos = Math.min(this.lastLoadedPos + chunkSize, this.fileSize); - await this.reader.reader.loadRange(this.lastLoadedPos, endPos); - - this.lastLoadedPos = endPos; - assert(this.lastLoadedPos <= this.fileSize); - - this.parseFramesFromLoadedData(); - } - - private parseFramesFromLoadedData() { - while (this.reader.pos <= this.fileSize - MAX_FRAME_HEADER_SIZE) { - const startPos = this.reader.pos; - const header = this.reader.readFrameHeader(); - if (!header) { - break; - } - - // Check if the entire frame fits in the loaded data - if (startPos + header.frameLength > this.lastLoadedPos) { - // Frame doesn't fit, reset positions and stop - this.reader.pos = startPos; - this.lastLoadedPos = startPos; - break; - } - - if (!this.firstFrameHeader) { - this.firstFrameHeader = header; - } - - const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex]; - assert(sampleRate !== undefined); - const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate; - const headerSize = header.crcCheck ? MAX_FRAME_HEADER_SIZE : MAX_FRAME_HEADER_SIZE - 2; - - const sample: Sample = { - timestamp: this.nextTimestampInSamples / sampleRate, - duration: sampleDuration, - dataStart: startPos + headerSize, - dataSize: header.frameLength - headerSize, - }; - - this.loadedSamples.push(sample); - this.nextTimestampInSamples += SAMPLES_PER_AAC_FRAME; - this.reader.pos = startPos + header.frameLength; + async advanceReader() { + let slice = this.reader.requestSliceRange(this.lastLoadedPos, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) { + this.lastSampleLoaded = true; + return; } + + const header = readFrameHeader(slice); + if (!header) { + this.lastSampleLoaded = true; + return; + } + + if (header.startPos + header.frameLength > this.fileSize) { + // Frame doesn't fit in the rest of the file + this.lastSampleLoaded = true; + return; + } + + if (!this.firstFrameHeader) { + this.firstFrameHeader = header; + } + + const sampleRate = aacFrequencyTable[header.samplingFrequencyIndex]; + assert(sampleRate !== undefined); + const sampleDuration = SAMPLES_PER_AAC_FRAME / sampleRate; + const headerSize = header.crcCheck ? MAX_FRAME_HEADER_SIZE : MIN_FRAME_HEADER_SIZE; + + const sample: Sample = { + timestamp: this.nextTimestampInSamples / sampleRate, + duration: sampleDuration, + dataStart: header.startPos + headerSize, + dataSize: header.frameLength - headerSize, + }; + + this.loadedSamples.push(sample); + this.nextTimestampInSamples += SAMPLES_PER_AAC_FRAME; + this.lastLoadedPos = header.startPos + header.frameLength; } async getMimeType() { @@ -219,7 +216,7 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { }; } - getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) { + async getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) { if (sampleIndex === -1) { return null; } @@ -233,8 +230,11 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { if (options.metadataOnly) { data = PLACEHOLDER_DATA; } else { - this.demuxer.reader.pos = rawSample.dataStart; - data = this.demuxer.reader.readBytes(rawSample.dataSize); + let slice = this.demuxer.reader.requestSlice(rawSample.dataStart, rawSample.dataSize); + if (slice instanceof Promise) slice = await slice; + assert(slice); + + data = readBytes(slice, rawSample.dataSize); } return new EncodedPacket( @@ -247,7 +247,7 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { ); } - async getFirstPacket(options: PacketRetrievalOptions) { + getFirstPacket(options: PacketRetrievalOptions) { return this.getPacketAtIndex(0, options); } @@ -268,9 +268,9 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { // Ensure the next sample exists while ( nextIndex >= this.demuxer.loadedSamples.length - && this.demuxer.lastLoadedPos < this.demuxer.fileSize + && !this.demuxer.lastSampleLoaded ) { - await this.demuxer.loadNextChunk(); + await this.demuxer.advanceReader(); } return this.getPacketAtIndex(nextIndex, options); @@ -294,7 +294,7 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { return null; } - if (this.demuxer.lastLoadedPos === this.demuxer.fileSize) { + if (this.demuxer.lastSampleLoaded) { // All data is loaded, return what we found return this.getPacketAtIndex(index, options); } @@ -305,7 +305,7 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking { } // Otherwise, keep loading data - await this.demuxer.loadNextChunk(); + await this.demuxer.advanceReader(); } } finally { release(); diff --git a/src/adts/adts-reader.ts b/src/adts/adts-reader.ts index 5abffca..113dc59 100644 --- a/src/adts/adts-reader.ts +++ b/src/adts/adts-reader.ts @@ -7,8 +7,9 @@ */ import { Bitstream } from '../misc'; -import { Reader } from '../reader'; +import { FileSlice, readBytes } from '../reader2'; +export const MIN_FRAME_HEADER_SIZE = 7; export const MAX_FRAME_HEADER_SIZE = 9; export type FrameHeader = { @@ -21,76 +22,64 @@ export type FrameHeader = { startPos: number; }; -export class AdtsReader { - pos = 0; - constructor(public reader: Reader) {} +export const readFrameHeader = (slice: FileSlice): FrameHeader | null => { + // https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17) - readBytes(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; + const startPos = slice.filePos; - return new Uint8Array(view.buffer, offset, length); + const bytes = readBytes(slice, 9); // 9 with CRC, 7 without CRC + const bitstream = new Bitstream(bytes); + + const syncword = bitstream.readBits(12); + if (syncword !== 0b1111_11111111) { + return null; } - readFrameHeader(): FrameHeader | null { - // https://wiki.multimedia.cx/index.php/ADTS (last visited: 2025/08/17) - - const startPos = this.pos; - - const bytes = this.readBytes(9); // 9 with CRC, 7 without CRC - const bitstream = new Bitstream(bytes); - - const syncword = bitstream.readBits(12); - if (syncword !== 0b1111_11111111) { - return null; - } - - bitstream.skipBits(1); // MPEG version - const layer = bitstream.readBits(2); - if (layer !== 0) { - return null; - } - - const protectionAbsence = bitstream.readBits(1); - const objectType = bitstream.readBits(2) + 1; - const samplingFrequencyIndex = bitstream.readBits(4); - if (samplingFrequencyIndex === 15) { - return null; - } - - bitstream.skipBits(1); // Private bit - const channelConfiguration = bitstream.readBits(3); - if (channelConfiguration === 0) { - throw new Error('ADTS frames with channel configuration 0 are not supported.'); - } - - bitstream.skipBits(1); // Originality - bitstream.skipBits(1); // Home - bitstream.skipBits(1); // Copyright ID bit - bitstream.skipBits(1); // Copyright ID start - const frameLength = bitstream.readBits(13); - bitstream.skipBits(11); // Buffer fullness - const numberOfAacFrames = bitstream.readBits(2) + 1; - if (numberOfAacFrames !== 1) { - throw new Error('ADTS frames with more than one AAC frame are not supported.'); - } - - let crcCheck: number | null = null; - - if (protectionAbsence === 1) { // No CRC - this.pos -= 2; - } else { // CRC - crcCheck = bitstream.readBits(16); - } - - return { - objectType, - samplingFrequencyIndex, - channelConfiguration, - frameLength, - numberOfAacFrames, - crcCheck, - startPos, - }; + bitstream.skipBits(1); // MPEG version + const layer = bitstream.readBits(2); + if (layer !== 0) { + return null; } -} + + const protectionAbsence = bitstream.readBits(1); + const objectType = bitstream.readBits(2) + 1; + const samplingFrequencyIndex = bitstream.readBits(4); + if (samplingFrequencyIndex === 15) { + return null; + } + + bitstream.skipBits(1); // Private bit + const channelConfiguration = bitstream.readBits(3); + if (channelConfiguration === 0) { + throw new Error('ADTS frames with channel configuration 0 are not supported.'); + } + + bitstream.skipBits(1); // Originality + bitstream.skipBits(1); // Home + bitstream.skipBits(1); // Copyright ID bit + bitstream.skipBits(1); // Copyright ID start + const frameLength = bitstream.readBits(13); + bitstream.skipBits(11); // Buffer fullness + const numberOfAacFrames = bitstream.readBits(2) + 1; + if (numberOfAacFrames !== 1) { + throw new Error('ADTS frames with more than one AAC frame are not supported.'); + } + + let crcCheck: number | null = null; + + if (protectionAbsence === 1) { // No CRC + slice.filePos -= 2; + } else { // CRC + crcCheck = bitstream.readBits(16); + } + + return { + objectType, + samplingFrequencyIndex, + channelConfiguration, + frameLength, + numberOfAacFrames, + crcCheck, + startPos, + }; +}; diff --git a/src/input-format.ts b/src/input-format.ts index 961ef8b..bc14a1c 100644 --- a/src/input-format.ts +++ b/src/input-format.ts @@ -9,18 +9,25 @@ import { Demuxer } from './demuxer'; import { Input } from './input'; import { IsobmffDemuxer } from './isobmff/isobmff-demuxer'; -import { IsobmffReader } from './isobmff/isobmff-reader'; -import { EBMLId, EBMLReader, MIN_HEADER_SIZE } from './matroska/ebml'; +import { + EBMLId, + MAX_HEADER_SIZE, + MIN_HEADER_SIZE, + readAsciiString, + readElementHeader, + readElementSize, + readUnsignedInt, + readVarIntSize, +} from './matroska/ebml'; import { MatroskaDemuxer } from './matroska/matroska-demuxer'; import { Mp3Demuxer } from './mp3/mp3-demuxer'; import { FRAME_HEADER_SIZE } from '../shared/mp3-misc'; -import { Mp3Reader } from './mp3/mp3-reader'; +import { readId3, readNextFrameHeader } from './mp3/mp3-reader'; import { OggDemuxer } from './ogg/ogg-demuxer'; -import { OggReader } from './ogg/ogg-reader'; -import { RiffReader } from './wave/riff-reader'; import { WaveDemuxer } from './wave/wave-demuxer'; -import { AdtsReader, MAX_FRAME_HEADER_SIZE } from './adts/adts-reader'; +import { MAX_FRAME_HEADER_SIZE, MIN_FRAME_HEADER_SIZE, readFrameHeader } from './adts/adts-reader'; import { AdtsDemuxer } from './adts/adts-demuxer'; +import { readAscii } from './reader2'; /** * Base class representing an input media file format. @@ -46,20 +53,18 @@ export abstract class InputFormat { export abstract class IsobmffInputFormat extends InputFormat { /** @internal */ protected async _getMajorBrand(input: Input) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < 12) { - return null; - } + let slice = input._reader2.requestSlice(0, 12); + if (slice instanceof Promise) slice = await slice; + if (!slice) return null; - const isobmffReader = new IsobmffReader(input._mainReader); - isobmffReader.pos = 4; - const fourCc = isobmffReader.readAscii(4); + slice.skip(4); + const fourCc = readAscii(slice, 4); if (fourCc !== 'ftyp') { return null; } - return isobmffReader.readAscii(4); + return readAscii(slice, 4); } /** @internal */ @@ -108,10 +113,6 @@ export class QuickTimeInputFormat extends IsobmffInputFormat { } } -function foo() { - return 5; -} - /** * Matroska file format. * @public @@ -119,70 +120,71 @@ function foo() { export class MatroskaInputFormat extends InputFormat { /** @internal */ protected async isSupportedEBMLOfDocType(input: Input, desiredDocType: string) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < 8) { - return false; - } + let headerSlice = input._reader2.requestSlice(0, MAX_HEADER_SIZE); + if (headerSlice instanceof Promise) headerSlice = await headerSlice; + if (!headerSlice) return false; - const ebmlReader = new EBMLReader(input._mainReader); - const varIntSize = ebmlReader.readVarIntSize(); + const varIntSize = readVarIntSize(headerSlice); if (varIntSize === null) { return false; } - foo(); - if (varIntSize < 1 || varIntSize > 8) { return false; } - const id = ebmlReader.readUnsignedInt(varIntSize); + const id = readUnsignedInt(headerSlice, varIntSize); if (id !== EBMLId.EBML) { return false; } - const dataSize = ebmlReader.readElementSize(); + const dataSize = readElementSize(headerSlice); if (dataSize === null) { return false; // Miss me with that shit } - const startPos = ebmlReader.pos; - while (ebmlReader.pos <= startPos + dataSize - MIN_HEADER_SIZE) { - const header = ebmlReader.readElementHeader(); + let dataSlice = input._reader2.requestSlice(headerSlice.filePos, dataSize); + if (dataSlice instanceof Promise) dataSlice = await dataSlice; + if (!dataSlice) return false; + + const startPos = headerSlice.filePos; + + while (dataSlice.filePos <= startPos + dataSize - MIN_HEADER_SIZE) { + const header = readElementHeader(dataSlice); if (!header) break; const { id, size } = header; - const dataStartPos = ebmlReader.pos; + const dataStartPos = dataSlice.filePos; if (size === null) return false; switch (id) { case EBMLId.EBMLVersion: { - const ebmlVersion = ebmlReader.readUnsignedInt(size); + const ebmlVersion = readUnsignedInt(dataSlice, size); if (ebmlVersion !== 1) { return false; } }; break; case EBMLId.EBMLReadVersion: { - const ebmlReadVersion = ebmlReader.readUnsignedInt(size); + const ebmlReadVersion = readUnsignedInt(dataSlice, size); if (ebmlReadVersion !== 1) { return false; } }; break; case EBMLId.DocType: { - const docType = ebmlReader.readAsciiString(size); + const docType = readAsciiString(dataSlice, size); if (docType !== desiredDocType) { return false; } }; break; case EBMLId.DocTypeVersion: { - const docTypeVersion = ebmlReader.readUnsignedInt(size); + const docTypeVersion = readUnsignedInt(dataSlice, size); if (docTypeVersion > 4) { // Support up to Matroska v4 return false; } }; break; } - ebmlReader.pos = dataStartPos + size; + dataSlice.filePos = dataStartPos + size; } return true; @@ -233,25 +235,20 @@ export class WebMInputFormat extends MatroskaInputFormat { export class Mp3InputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < 4) { - return false; - } + let slice = input._reader2.requestSlice(0, 10); + if (slice instanceof Promise) slice = await slice; + if (!slice) return false; - const mp3Reader = new Mp3Reader(input._mainReader); - mp3Reader.fileSize = sourceSize; + let currentPos = 0; - const id3Tag = mp3Reader.readId3(); + const id3Tag = readId3(slice); if (id3Tag) { - mp3Reader.pos += id3Tag.size; + currentPos = slice.filePos + id3Tag.size; } - const framesStartPos = mp3Reader.pos; - await mp3Reader.reader.loadRange(mp3Reader.pos, mp3Reader.pos + 4096); - - const firstHeader = mp3Reader.readNextFrameHeader(Math.min(framesStartPos + 4096, sourceSize)); - if (!firstHeader) { + const firstResult = await readNextFrameHeader(input._reader2, currentPos, currentPos + 4096); + if (!firstResult) { return false; } @@ -260,15 +257,18 @@ export class Mp3InputFormat extends InputFormat { return true; } + currentPos = firstResult.startPos += firstResult.header.totalSize; + // Fine, we found one frame header, but we're still not entirely sure this is MP3. Let's check if we can find // another header right after it: - mp3Reader.pos = firstHeader.startPos + firstHeader.totalSize; - await mp3Reader.reader.loadRange(mp3Reader.pos, mp3Reader.pos + FRAME_HEADER_SIZE); - const secondHeader = mp3Reader.readNextFrameHeader(mp3Reader.pos + FRAME_HEADER_SIZE); - if (!secondHeader) { + const secondResult = await readNextFrameHeader(input._reader2, currentPos, currentPos + FRAME_HEADER_SIZE); + if (!secondResult) { return false; } + const firstHeader = firstResult.header; + const secondHeader = secondResult.header; + // In a well-formed MP3 file, we'd expect these two frames to share some similarities: if (firstHeader.channel !== secondHeader.channel || firstHeader.sampleRate !== secondHeader.sampleRate) { return false; @@ -299,19 +299,18 @@ export class Mp3InputFormat extends InputFormat { export class WaveInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < 12) { - return false; - } + let slice = input._reader2.requestSlice(0, 12); + if (slice instanceof Promise) slice = await slice; + if (!slice) return false; - const riffReader = new RiffReader(input._mainReader); - const riffType = riffReader.readAscii(4); + const riffType = readAscii(slice, 4); if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') { return false; } - riffReader.pos = 8; - const format = riffReader.readAscii(4); + slice.skip(4); + + const format = readAscii(slice, 4); return format === 'WAVE'; } @@ -336,13 +335,11 @@ export class WaveInputFormat extends InputFormat { export class OggInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < 4) { - return false; - } + let slice = input._reader2.requestSlice(0, 4); + if (slice instanceof Promise) slice = await slice; + if (!slice) return false; - const oggReader = new OggReader(input._mainReader); - return oggReader.readAscii(4) === 'OggS'; + return readAscii(slice, 4) === 'OggS'; } /** @internal */ @@ -366,24 +363,20 @@ export class OggInputFormat extends InputFormat { export class AdtsInputFormat extends InputFormat { /** @internal */ async _canReadInput(input: Input) { - const sourceSize = await input._mainReader.source.getSize(); - if (sourceSize < MAX_FRAME_HEADER_SIZE) { - return false; - } + let slice = input._reader2.requestSliceRange(0, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) return false; - const adtsReader = new AdtsReader(input._mainReader); - const firstHeader = adtsReader.readFrameHeader(); + const firstHeader = readFrameHeader(slice); if (!firstHeader) { return false; } - if (sourceSize < firstHeader.frameLength + MAX_FRAME_HEADER_SIZE) { - return false; - } + slice = input._reader2.requestSliceRange(firstHeader.frameLength, MIN_FRAME_HEADER_SIZE, MAX_FRAME_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) return false; - adtsReader.pos = firstHeader.frameLength; - await adtsReader.reader.loadRange(adtsReader.pos, adtsReader.pos + MAX_FRAME_HEADER_SIZE); - const secondHeader = adtsReader.readFrameHeader(); + const secondHeader = readFrameHeader(slice); if (!secondHeader) { return false; } diff --git a/src/input.ts b/src/input.ts index 5c3d086..de134df 100644 --- a/src/input.ts +++ b/src/input.ts @@ -10,6 +10,7 @@ import { Demuxer } from './demuxer'; import { InputFormat } from './input-format'; import { assert } from './misc'; import { Reader } from './reader'; +import { Reader2 } from './reader2'; import { Source } from './source'; /** @@ -39,6 +40,8 @@ export class Input { /** @internal */ _format: InputFormat | null = null; + _reader2: Reader2; + constructor(options: InputOptions) { if (!options || typeof options !== 'object') { throw new TypeError('options must be an object.'); @@ -53,6 +56,7 @@ export class Input { this._formats = options.formats; this._source = options.source; this._mainReader = new Reader(options.source); + this._reader2 = new Reader2(options.source); } /** @internal */ diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index ba5ec2d..6d002b4 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -19,12 +19,12 @@ import { VideoCodec, } from '../codec'; import { + Av1CodecInfo, AvcDecoderConfigurationRecord, + extractAv1CodecInfoFromPacket, + extractVp9CodecInfoFromPacket, HevcDecoderConfigurationRecord, Vp9CodecInfo, - Av1CodecInfo, - extractVp9CodecInfoFromPacket, - extractAv1CodecInfoFromPacket, } from '../codec-data'; import { Demuxer } from '../demuxer'; import { Input } from '../input'; @@ -39,29 +39,50 @@ import { import { PacketRetrievalOptions } from '../media-sink'; import { assert, - COLOR_PRIMARIES_MAP_INVERSE, - MATRIX_COEFFICIENTS_MAP_INVERSE, - TRANSFER_CHARACTERISTICS_MAP_INVERSE, - binarySearchLessOrEqual, - binarySearchExact, - Rotation, - last, AsyncMutex, - findLastIndex, - UNDETERMINED_LANGUAGE, - TransformationMatrix, - roundToPrecision, - isIso639Dash2LanguageCode, - roundToMultiple, - normalizeRotation, + binarySearchExact, + binarySearchLessOrEqual, Bitstream, + COLOR_PRIMARIES_MAP_INVERSE, + findLastIndex, insertSorted, + isIso639Dash2LanguageCode, + last, + MATRIX_COEFFICIENTS_MAP_INVERSE, + normalizeRotation, + roundToMultiple, + roundToPrecision, + Rotation, textDecoder, + TransformationMatrix, + TRANSFER_CHARACTERISTICS_MAP_INVERSE, + UNDETERMINED_LANGUAGE, } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; -import { Reader } from '../reader'; import { buildIsobmffMimeType } from './isobmff-misc'; -import { IsobmffReader, MAX_BOX_HEADER_SIZE, MIN_BOX_HEADER_SIZE } from './isobmff-reader'; +import { + MAX_BOX_HEADER_SIZE, + MIN_BOX_HEADER_SIZE, + readBoxHeader, + readFixed_16_16, + readFixed_2_30, + readIsomVariableInteger, +} from './isobmff-reader'; +import { + FileSlice, + readBytes, + readF64Be, + readI16Be, + readI32Be, + readI64Be, + Reader2, + readU16Be, + readU24Be, + readU32Be, + readU64Be, + readU8, + readAscii, +} from '../reader2'; type InternalTrack = { id: number; @@ -202,7 +223,9 @@ type Fragment = { }; export class IsobmffDemuxer extends Demuxer { - metadataReader: IsobmffReader; + reader: Reader2; + moovSlice: FileSlice | null = null; + currentTrack: InternalTrack | null = null; tracks: InternalTrack[] = []; metadataPromise: Promise | null = null; @@ -216,13 +239,10 @@ export class IsobmffDemuxer extends Demuxer { currentFragment: Fragment | null = null; fragmentLookupMutex = new AsyncMutex(); - chunkReader: IsobmffReader; - constructor(input: Input) { super(input); - this.metadataReader = new IsobmffReader(input._mainReader); - this.chunkReader = new IsobmffReader(new Reader(input.source, 64 * 2 ** 20)); // Max 64 MiB of stored chunks + this.reader = input._reader2; } override async computeDuration() { @@ -251,29 +271,33 @@ export class IsobmffDemuxer extends Demuxer { readMetadata() { return this.metadataPromise ??= (async () => { - const sourceSize = await this.metadataReader.reader.source.getSize(); + let sourceSize = this.reader.requestSize(); + if (sourceSize instanceof Promise) sourceSize = await sourceSize; - while (this.metadataReader.pos < sourceSize) { - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + MAX_BOX_HEADER_SIZE, - ); - const startPos = this.metadataReader.pos; - const boxInfo = this.metadataReader.readBoxHeader(); + let currentPos = 0; + while (currentPos < sourceSize) { + 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 startPos = currentPos; + const boxInfo = readBoxHeader(slice); if (!boxInfo) { break; } if (boxInfo.name === 'ftyp') { - const majorBrand = this.metadataReader.readAscii(4); + const majorBrand = readAscii(slice, 4); this.isQuickTime = majorBrand === 'qt '; } else if (boxInfo.name === 'moov') { // Found moov, load it - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + boxInfo.contentSize, - ); - this.readContiguousBoxes(boxInfo.contentSize); + + let moovSlice = this.reader.requestSlice(slice.filePos, boxInfo.contentSize); + if (moovSlice instanceof Promise) moovSlice = await moovSlice; + if (!moovSlice) break; + + this.moovSlice = moovSlice; + this.readContiguousBoxes(this.moovSlice); for (const track of this.tracks) { // Modify the edit list offset based on the previous segment durations. They are in different @@ -286,32 +310,38 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos = startPos + boxInfo.totalSize; + currentPos = startPos + boxInfo.totalSize; } if (this.isFragmented) { // The last 4 bytes may contain the size of the mfra box at the end of the file - await this.metadataReader.reader.loadRange(sourceSize - 4, sourceSize); + let lastWordSlice = this.reader.requestSlice(sourceSize - 4, 4); + if (lastWordSlice instanceof Promise) lastWordSlice = await lastWordSlice; + assert(lastWordSlice); - this.metadataReader.pos = sourceSize - 4; - const lastWord = this.metadataReader.readU32(); + const lastWord = readU32Be(lastWordSlice); const potentialMfraPos = sourceSize - lastWord; if (potentialMfraPos >= 0 && potentialMfraPos <= sourceSize - MAX_BOX_HEADER_SIZE) { - // Load the header and a bit more, likely covering the entire box - await this.metadataReader.reader.loadRange(potentialMfraPos, potentialMfraPos + 2 ** 16); + let mfraHeaderSlice = this.reader.requestSliceRange( + potentialMfraPos, + MIN_BOX_HEADER_SIZE, + MAX_BOX_HEADER_SIZE, + ); + if (mfraHeaderSlice instanceof Promise) mfraHeaderSlice = await mfraHeaderSlice; - this.metadataReader.pos = potentialMfraPos; - const boxInfo = this.metadataReader.readBoxHeader(); + if (mfraHeaderSlice) { + const boxInfo = readBoxHeader(mfraHeaderSlice); - if (boxInfo && boxInfo.name === 'mfra') { - // We found the mfra box, allowing for much better random access. Let's parse it. + if (boxInfo && boxInfo.name === 'mfra') { + // We found the mfra box, allowing for much better random access. Let's parse it. + let mfraSlice = this.reader.requestSlice(mfraHeaderSlice.filePos, boxInfo.contentSize); + if (mfraSlice instanceof Promise) mfraSlice = await mfraSlice; - await this.metadataReader.reader.loadRange( - potentialMfraPos, - potentialMfraPos + boxInfo.totalSize, - ); - this.readContiguousBoxes(boxInfo.contentSize); + if (mfraSlice) { + this.readContiguousBoxes(mfraSlice); + } + } } } } @@ -335,9 +365,11 @@ export class IsobmffDemuxer extends Demuxer { }; internalTrack.sampleTable = sampleTable; - this.metadataReader.pos = internalTrack.sampleTableByteOffset; + assert(this.moovSlice); + const stblContainerSlice = this.moovSlice.slice(internalTrack.sampleTableByteOffset); + this.currentTrack = internalTrack; - this.traverseBox(); + this.traverseBox(stblContainerSlice); this.currentTrack = null; const isPcmCodec = internalTrack.info?.type === 'audio' @@ -459,22 +491,19 @@ export class IsobmffDemuxer extends Demuxer { return sampleTable; } - async readFragment(): Promise { - const startPos = this.metadataReader.pos; + async readFragment(startPos: number): Promise { + let headerSlice = this.reader.requestSliceRange(startPos, MIN_BOX_HEADER_SIZE, MAX_BOX_HEADER_SIZE); + if (headerSlice instanceof Promise) headerSlice = await headerSlice; + assert(headerSlice); - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + MAX_BOX_HEADER_SIZE, - ); - - const moofBoxInfo = this.metadataReader.readBoxHeader(); + const moofBoxInfo = readBoxHeader(headerSlice); assert(moofBoxInfo?.name === 'moof'); - const contentStart = this.metadataReader.pos; - await this.metadataReader.reader.loadRange(contentStart, contentStart + moofBoxInfo.contentSize); + let entireSlice = this.reader.requestSlice(startPos, moofBoxInfo.totalSize); + if (entireSlice instanceof Promise) entireSlice = await entireSlice; + assert(entireSlice); - this.metadataReader.pos = startPos; - this.traverseBox(); + this.traverseBox(entireSlice); const index = binarySearchExact(this.fragments, startPos, x => x.moofOffset); assert(index !== -1); @@ -482,10 +511,6 @@ export class IsobmffDemuxer extends Demuxer { const fragment = this.fragments[index]!; assert(fragment.moofOffset === startPos); - // We have read everything in the moof box, there's no need to keep the data around anymore - // (keep the header tho) - this.metadataReader.reader.forgetRange(contentStart, contentStart + moofBoxInfo.contentSize); - // 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) { @@ -495,7 +520,7 @@ export class IsobmffDemuxer extends Demuxer { const internalTrack = this.tracks.find(x => x.id === trackId)!; - this.metadataReader.pos = 0; + let currentPos = 0; let currentFragment: Fragment | null = null; let lastFragment: Fragment | null = null; @@ -509,34 +534,32 @@ export class IsobmffDemuxer extends Demuxer { // already has final timestamps). currentFragment = internalTrack.fragments[index]!; lastFragment = currentFragment; - this.metadataReader.pos = currentFragment.moofOffset + currentFragment.moofSize; + currentPos = currentFragment.moofOffset + currentFragment.moofSize; } - let nextFragmentIsFirstFragment = this.metadataReader.pos === 0; + let nextFragmentIsFirstFragment = currentPos === 0; - while (this.metadataReader.pos <= startPos - MIN_BOX_HEADER_SIZE) { + while (currentPos <= startPos - MIN_BOX_HEADER_SIZE) { if (currentFragment?.nextFragment) { currentFragment = currentFragment.nextFragment; - this.metadataReader.pos = currentFragment.moofOffset + currentFragment.moofSize; + currentPos = currentFragment.moofOffset + currentFragment.moofSize; } else { - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + MAX_BOX_HEADER_SIZE, - ); - const startPos = this.metadataReader.pos; - const boxInfo = this.metadataReader.readBoxHeader(); + 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; } if (boxInfo.name === 'moof') { - const index = binarySearchExact(this.fragments, startPos, x => x.moofOffset); + const index = binarySearchExact(this.fragments, boxStartPos, x => x.moofOffset); let fragment: Fragment; if (index === -1) { - this.metadataReader.pos = startPos; - - fragment = await this.readFragment(); // Recursive call + fragment = await this.readFragment(boxStartPos); // Recursive call } else { // We already know this fragment fragment = this.fragments[index]!; @@ -552,7 +575,7 @@ export class IsobmffDemuxer extends Demuxer { } } - this.metadataReader.pos = startPos + boxInfo.totalSize; + currentPos = boxStartPos + boxInfo.totalSize; } if (currentFragment && currentFragment.trackData.has(trackId)) { @@ -573,11 +596,11 @@ export class IsobmffDemuxer extends Demuxer { return fragment; } - readContiguousBoxes(totalSize: number) { - const startIndex = this.metadataReader.pos; + readContiguousBoxes(slice: FileSlice) { + const startIndex = slice.filePos; - while (this.metadataReader.pos - startIndex <= totalSize - MIN_BOX_HEADER_SIZE) { - const foundBox = this.traverseBox(); + while (slice.filePos - startIndex <= slice.length - MIN_BOX_HEADER_SIZE) { + const foundBox = this.traverseBox(slice); if (!foundBox) { break; @@ -585,13 +608,14 @@ export class IsobmffDemuxer extends Demuxer { } } - traverseBox() { - const startPos = this.metadataReader.pos; - const boxInfo = this.metadataReader.readBoxHeader(); + traverseBox(slice: FileSlice): boolean { + const startPos = slice.filePos; + const boxInfo = readBoxHeader(slice); if (!boxInfo) { return false; } + const contentStartPos = slice.filePos; const boxEndPos = startPos + boxInfo.totalSize; switch (boxInfo.name) { @@ -601,21 +625,21 @@ export class IsobmffDemuxer extends Demuxer { case 'mfra': case 'edts': case 'udta': { - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); }; break; case 'mvhd': { - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const version = readU8(slice); + slice.skip(3); // Flags if (version === 1) { - this.metadataReader.pos += 8 + 8; - this.movieTimescale = this.metadataReader.readU32(); - this.movieDurationInTimescale = this.metadataReader.readU64(); + slice.skip(8 + 8); + this.movieTimescale = readU32Be(slice); + this.movieDurationInTimescale = readU64Be(slice); } else { - this.metadataReader.pos += 4 + 4; - this.movieTimescale = this.metadataReader.readU32(); - this.movieDurationInTimescale = this.metadataReader.readU32(); + slice.skip(4 + 4); + this.movieTimescale = readU32Be(slice); + this.movieDurationInTimescale = readU32Be(slice); } }; break; @@ -643,7 +667,7 @@ export class IsobmffDemuxer extends Demuxer { } satisfies InternalTrack as InternalTrack; this.currentTrack = track; - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); if (track.id !== -1 && track.timescale !== -1 && track.info !== null) { if (track.info.type === 'video' && track.info.width !== -1) { @@ -664,8 +688,8 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track); - const version = this.metadataReader.readU8(); - const flags = this.metadataReader.readU24(); + const version = readU8(slice); + const flags = readU24Be(slice); const trackEnabled = (flags & 0x1) !== 0; if (!trackEnabled) { @@ -674,30 +698,30 @@ export class IsobmffDemuxer extends Demuxer { // Skip over creation & modification time to reach the track ID if (version === 0) { - this.metadataReader.pos += 8; - track.id = this.metadataReader.readU32(); - this.metadataReader.pos += 4; - track.durationInMovieTimescale = this.metadataReader.readU32(); + slice.skip(8); + track.id = readU32Be(slice); + slice.skip(4); + track.durationInMovieTimescale = readU32Be(slice); } else if (version === 1) { - this.metadataReader.pos += 16; - track.id = this.metadataReader.readU32(); - this.metadataReader.pos += 4; - track.durationInMovieTimescale = this.metadataReader.readU64(); + slice.skip(16); + track.id = readU32Be(slice); + slice.skip(4); + track.durationInMovieTimescale = readU64Be(slice); } else { throw new Error(`Incorrect track header version ${version}.`); } - this.metadataReader.pos += 2 * 4 + 2 + 2 + 2 + 2; + slice.skip(2 * 4 + 2 + 2 + 2 + 2); const matrix: TransformationMatrix = [ - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_2_30(), - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_2_30(), - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_16_16(), - this.metadataReader.readFixed_2_30(), + readFixed_16_16(slice), + readFixed_16_16(slice), + readFixed_2_30(slice), + readFixed_16_16(slice), + readFixed_16_16(slice), + readFixed_2_30(slice), + readFixed_16_16(slice), + readFixed_16_16(slice), + readFixed_2_30(slice), ]; const rotation = normalizeRotation(roundToMultiple(extractRotationFromMatrix(matrix), 90)); @@ -710,21 +734,21 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track); - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const version = readU8(slice); + slice.skip(3); // Flags let relevantEntryFound = false; let previousSegmentDurations = 0; - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); for (let i = 0; i < entryCount; i++) { const segmentDuration = version === 1 - ? this.metadataReader.readU64() - : this.metadataReader.readU32(); + ? readU64Be(slice) + : readU32Be(slice); const mediaTime = version === 1 - ? this.metadataReader.readI64() - : this.metadataReader.readI32(); - const mediaRate = this.metadataReader.readFixed_16_16(); + ? readI64Be(slice) + : readI32Be(slice); + const mediaRate = readFixed_16_16(slice); if (segmentDuration === 0) { // Don't care @@ -758,20 +782,20 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track); - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const version = readU8(slice); + slice.skip(3); // Flags if (version === 0) { - this.metadataReader.pos += 8; - track.timescale = this.metadataReader.readU32(); - track.durationInMediaTimescale = this.metadataReader.readU32(); + slice.skip(8); + track.timescale = readU32Be(slice); + track.durationInMediaTimescale = readU32Be(slice); } else if (version === 1) { - this.metadataReader.pos += 16; - track.timescale = this.metadataReader.readU32(); - track.durationInMediaTimescale = this.metadataReader.readU64(); + slice.skip(16); + track.timescale = readU32Be(slice); + track.durationInMediaTimescale = readU64Be(slice); } - let language = this.metadataReader.readU16(); + let language = readU16Be(slice); if (language > 0) { track.languageCode = ''; @@ -792,8 +816,8 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track); - this.metadataReader.pos += 8; // Version + flags + pre-defined - const handlerType = this.metadataReader.readAscii(4); + slice.skip(8); // Version + flags + pre-defined + const handlerType = readAscii(slice, 4); if (handlerType === 'vide') { track.info = { @@ -826,7 +850,7 @@ export class IsobmffDemuxer extends Demuxer { track.sampleTableByteOffset = startPos; - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); }; break; case 'stsd': { @@ -837,14 +861,14 @@ export class IsobmffDemuxer extends Demuxer { break; } - const stsdVersion = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const stsdVersion = readU8(slice); + slice.skip(3); // Flags - const entries = this.metadataReader.readU32(); + const entries = readU32Be(slice); for (let i = 0; i < entries; i++) { - const startPos = this.metadataReader.pos; - const sampleBoxInfo = this.metadataReader.readBoxHeader(); + const sampleBoxStartPos = slice.filePos; + const sampleBoxInfo = readBoxHeader(slice); if (!sampleBoxInfo) { break; } @@ -867,14 +891,19 @@ export class IsobmffDemuxer extends Demuxer { console.warn(`Unsupported video codec (sample entry type '${sampleBoxInfo.name}').`); } - this.metadataReader.pos += 6 * 1 + 2 + 2 + 2 + 3 * 4; + slice.skip(6 * 1 + 2 + 2 + 2 + 3 * 4); - track.info.width = this.metadataReader.readU16(); - track.info.height = this.metadataReader.readU16(); + track.info.width = readU16Be(slice); + track.info.height = readU16Be(slice); - this.metadataReader.pos += 4 + 4 + 4 + 2 + 32 + 2 + 2; + slice.skip(4 + 4 + 4 + 2 + 32 + 2 + 2); - this.readContiguousBoxes((startPos + sampleBoxInfo.totalSize) - this.metadataReader.pos); + this.readContiguousBoxes( + slice.slice( + slice.filePos, + (sampleBoxStartPos + sampleBoxInfo.totalSize) - slice.filePos, + ), + ); } else { if (lowercaseBoxName === 'mp4a') { // We don't know the codec yet (might be AAC, might be MP3), need to read the esds box @@ -904,36 +933,36 @@ export class IsobmffDemuxer extends Demuxer { console.warn(`Unsupported audio codec (sample entry type '${sampleBoxInfo.name}').`); } - this.metadataReader.pos += 6 * 1 + 2; + slice.skip(6 * 1 + 2); - const version = this.metadataReader.readU16(); - this.metadataReader.pos += 3 * 2; + const version = readU16Be(slice); + slice.skip(3 * 2); - let channelCount = this.metadataReader.readU16(); - let sampleSize = this.metadataReader.readU16(); + let channelCount = readU16Be(slice); + let sampleSize = readU16Be(slice); - this.metadataReader.pos += 2 * 2; + slice.skip(2 * 2); // Can't use fixed16_16 as that's signed - let sampleRate = this.metadataReader.readU32() / 0x10000; + let sampleRate = readU32Be(slice) / 0x10000; if (stsdVersion === 0 && version > 0) { // Additional QuickTime fields if (version === 1) { - this.metadataReader.pos += 4; - sampleSize = 8 * this.metadataReader.readU32(); - this.metadataReader.pos += 2 * 4; + slice.skip(4); + sampleSize = 8 * readU32Be(slice); + slice.skip(2 * 4); } else if (version === 2) { - this.metadataReader.pos += 4; - sampleRate = this.metadataReader.readF64(); - channelCount = this.metadataReader.readU32(); - this.metadataReader.pos += 4; // Always 0x7f000000 + slice.skip(4); + sampleRate = readF64Be(slice); + channelCount = readU32Be(slice); + slice.skip(4); // Always 0x7f000000 - sampleSize = this.metadataReader.readU32(); + sampleSize = readU32Be(slice); - const flags = this.metadataReader.readU32(); + const flags = readU32Be(slice); - this.metadataReader.pos += 2 * 4; + slice.skip(2 * 4); if (lowercaseBoxName === 'lpcm') { const bytesPerSample = (sampleSize + 7) >> 3; @@ -1010,7 +1039,12 @@ export class IsobmffDemuxer extends Demuxer { track.info.codec = 'pcm-f32be'; // Placeholder, will be adjusted by the pcmC box } - this.readContiguousBoxes((startPos + sampleBoxInfo.totalSize) - this.metadataReader.pos); + this.readContiguousBoxes( + slice.slice( + slice.filePos, + (sampleBoxStartPos + sampleBoxInfo.totalSize) - slice.filePos, + ), + ); } } }; break; @@ -1019,31 +1053,31 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info); - track.info.codecDescription = this.metadataReader.readBytes(boxInfo.contentSize); + track.info.codecDescription = readBytes(slice, boxInfo.contentSize); }; break; case 'hvcC': { const track = this.currentTrack; assert(track && track.info); - track.info.codecDescription = this.metadataReader.readBytes(boxInfo.contentSize); + track.info.codecDescription = readBytes(slice, boxInfo.contentSize); }; break; case 'vpcC': { const track = this.currentTrack; assert(track && track.info?.type === 'video'); - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const profile = this.metadataReader.readU8(); - const level = this.metadataReader.readU8(); - const thirdByte = this.metadataReader.readU8(); + const profile = readU8(slice); + const level = readU8(slice); + const thirdByte = readU8(slice); const bitDepth = thirdByte >> 4; const chromaSubsampling = (thirdByte >> 1) & 0b111; const videoFullRangeFlag = thirdByte & 1; - const colourPrimaries = this.metadataReader.readU8(); - const transferCharacteristics = this.metadataReader.readU8(); - const matrixCoefficients = this.metadataReader.readU8(); + const colourPrimaries = readU8(slice); + const transferCharacteristics = readU8(slice); + const matrixCoefficients = readU8(slice); track.info.vp9CodecInfo = { profile, @@ -1061,13 +1095,13 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'video'); - this.metadataReader.pos += 1; // Marker + version + slice.skip(1); // Marker + version - const secondByte = this.metadataReader.readU8(); + const secondByte = readU8(slice); const profile = secondByte >> 5; const level = secondByte & 0b11111; - const thirdByte = this.metadataReader.readU8(); + const thirdByte = readU8(slice); const tier = thirdByte >> 7; const highBitDepth = (thirdByte >> 6) & 1; const twelveBit = (thirdByte >> 5) & 1; @@ -1095,15 +1129,15 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'video'); - const colourType = this.metadataReader.readAscii(4); + const colourType = readAscii(slice, 4); if (colourType !== 'nclx') { break; } - const colourPrimaries = this.metadataReader.readU16(); - const transferCharacteristics = this.metadataReader.readU16(); - const matrixCoefficients = this.metadataReader.readU16(); - const fullRangeFlag = Boolean(this.metadataReader.readU8() & 0x80); + const colourPrimaries = readU16Be(slice); + const transferCharacteristics = readU16Be(slice); + const matrixCoefficients = readU16Be(slice); + const fullRangeFlag = Boolean(readU8(slice) & 0x80); track.info.colorSpace = { primaries: COLOR_PRIMARIES_MAP_INVERSE[colourPrimaries], @@ -1114,46 +1148,46 @@ export class IsobmffDemuxer extends Demuxer { }; break; case 'wave': { - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); }; break; case 'esds': { const track = this.currentTrack; assert(track && track.info?.type === 'audio'); - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const tag = this.metadataReader.readU8(); + const tag = readU8(slice); assert(tag === 0x03); // ES Descriptor - this.metadataReader.readIsomVariableInteger(); // Length + readIsomVariableInteger(slice); // Length - this.metadataReader.pos += 2; // ES ID - const mixed = this.metadataReader.readU8(); + slice.skip(2); // ES ID + const mixed = readU8(slice); const streamDependenceFlag = (mixed & 0x80) !== 0; const urlFlag = (mixed & 0x40) !== 0; const ocrStreamFlag = (mixed & 0x20) !== 0; if (streamDependenceFlag) { - this.metadataReader.pos += 2; + slice.skip(2); } if (urlFlag) { - const urlLength = this.metadataReader.readU8(); - this.metadataReader.pos += urlLength; + const urlLength = readU8(slice); + slice.skip(urlLength); } if (ocrStreamFlag) { - this.metadataReader.pos += 2; + slice.skip(2); } - const decoderConfigTag = this.metadataReader.readU8(); + const decoderConfigTag = readU8(slice); assert(decoderConfigTag === 0x04); // DecoderConfigDescriptor - const decoderConfigDescriptorLength = this.metadataReader.readIsomVariableInteger(); // Length + const decoderConfigDescriptorLength = readIsomVariableInteger(slice); // Length - const payloadStart = this.metadataReader.pos; + const payloadStart = slice.filePos; - const objectTypeIndication = this.metadataReader.readU8(); + const objectTypeIndication = readU8(slice); if (objectTypeIndication === 0x40 || objectTypeIndication === 0x67) { track.info.codec = 'aac'; track.info.aacCodecInfo = { isMpeg2: objectTypeIndication === 0x67 }; @@ -1167,16 +1201,16 @@ export class IsobmffDemuxer extends Demuxer { ); } - this.metadataReader.pos += 1 + 3 + 4 + 4; + slice.skip(1 + 3 + 4 + 4); - if (decoderConfigDescriptorLength > this.metadataReader.pos - payloadStart) { + if (decoderConfigDescriptorLength > slice.filePos - payloadStart) { // There's a DecoderSpecificInfo at the end, let's read it - const decoderSpecificInfoTag = this.metadataReader.readU8(); + const decoderSpecificInfoTag = readU8(slice); assert(decoderSpecificInfoTag === 0x05); // DecoderSpecificInfo - const decoderSpecificInfoLength = this.metadataReader.readIsomVariableInteger(); - track.info.codecDescription = this.metadataReader.readBytes(decoderSpecificInfoLength); + const decoderSpecificInfoLength = readIsomVariableInteger(slice); + track.info.codecDescription = readBytes(slice, decoderSpecificInfoLength); if (track.info.codec === 'aac') { // Let's try to deduce more accurate values directly from the AudioSpecificConfig: @@ -1195,7 +1229,7 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'audio'); - const littleEndian = this.metadataReader.readU16() & 0xff; // 0xff is from FFmpeg + const littleEndian = readU16Be(slice) & 0xff; // 0xff is from FFmpeg if (littleEndian) { if (track.info.codec === 'pcm-s16be') { @@ -1216,13 +1250,13 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'audio'); - this.metadataReader.pos += 1 + 3; // Version + flags + slice.skip(1 + 3); // Version + flags // ISO/IEC 23003-5 - const formatFlags = this.metadataReader.readU8(); + const formatFlags = readU8(slice); const isLittleEndian = Boolean(formatFlags & 0x01); - const pcmSampleSize = this.metadataReader.readU8(); + const pcmSampleSize = readU8(slice); if (track.info.codec === 'pcm-s16be') { // ipcm @@ -1281,18 +1315,18 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'audio'); - this.metadataReader.pos += 1; // Version + slice.skip(1); // Version // https://www.opus-codec.org/docs/opus_in_isobmff.html - const outputChannelCount = this.metadataReader.readU8(); - const preSkip = this.metadataReader.readU16(); - const inputSampleRate = this.metadataReader.readU32(); - const outputGain = this.metadataReader.readI16(); - const channelMappingFamily = this.metadataReader.readU8(); + const outputChannelCount = readU8(slice); + const preSkip = readU16Be(slice); + const inputSampleRate = readU32Be(slice); + const outputGain = readI16Be(slice); + const channelMappingFamily = readU8(slice); let channelMappingTable: Uint8Array; if (channelMappingFamily !== 0) { - channelMappingTable = this.metadataReader.readBytes(2 + outputChannelCount); + channelMappingTable = readBytes(slice, 2 + outputChannelCount); } else { channelMappingTable = new Uint8Array(0); } @@ -1319,36 +1353,36 @@ export class IsobmffDemuxer extends Demuxer { const track = this.currentTrack; assert(track && track.info?.type === 'audio'); - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags // https://datatracker.ietf.org/doc/rfc9639/ const BLOCK_TYPE_MASK = 0x7f; const LAST_METADATA_BLOCK_FLAG_MASK = 0x80; - const startPos = this.metadataReader.pos; + const startPos = slice.filePos; - while (this.metadataReader.pos < boxEndPos) { - const flagAndType = this.metadataReader.readU8(); - const metadataBlockLength = this.metadataReader.readU24(); + while (slice.filePos < boxEndPos) { + const flagAndType = readU8(slice); + const metadataBlockLength = readU24Be(slice); const type = flagAndType & BLOCK_TYPE_MASK; // It's a STREAMINFO block; let's extract the actual sample rate and channel count if (type === 0) { - this.metadataReader.pos += 10; + slice.skip(10); - // Extract sample rate - const word = this.metadataReader.readU32(); + // Extract sample rate and channel count + const word = readU32Be(slice); const sampleRate = word >>> 12; const numberOfChannels = ((word >> 9) & 0b111) + 1; track.info.sampleRate = sampleRate; track.info.numberOfChannels = numberOfChannels; - this.metadataReader.pos += 20; + slice.skip(20); } else { // Simply skip ahead to the next block - this.metadataReader.pos += metadataBlockLength; + slice.skip(metadataBlockLength); } if (flagAndType & LAST_METADATA_BLOCK_FLAG_MASK) { @@ -1356,9 +1390,9 @@ export class IsobmffDemuxer extends Demuxer { } } - const endPos = this.metadataReader.pos; - this.metadataReader.pos = startPos; - const bytes = this.metadataReader.readBytes(endPos - startPos); + const endPos = slice.filePos; + slice.filePos = startPos; + const bytes = readBytes(slice, endPos - startPos); const description = new Uint8Array(4 + bytes.byteLength); const view = new DataView(description.buffer); @@ -1377,16 +1411,16 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); let currentIndex = 0; let currentTimestamp = 0; for (let i = 0; i < entryCount; i++) { - const sampleCount = this.metadataReader.readU32(); - const sampleDelta = this.metadataReader.readU32(); + const sampleCount = readU32Be(slice); + const sampleDelta = readU32Be(slice); track.sampleTable.sampleTimingEntries.push({ startIndex: currentIndex, @@ -1408,14 +1442,14 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 1 + 3; // Version + flags + slice.skip(1 + 3); // Version + flags - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); let sampleIndex = 0; for (let i = 0; i < entryCount; i++) { - const sampleCount = this.metadataReader.readU32(); - const sampleOffset = this.metadataReader.readI32(); + const sampleCount = readU32Be(slice); + const sampleOffset = readI32Be(slice); track.sampleTable.sampleCompositionTimeOffsets.push({ startIndex: sampleIndex, @@ -1435,14 +1469,14 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const sampleSize = this.metadataReader.readU32(); - const sampleCount = this.metadataReader.readU32(); + const sampleSize = readU32Be(slice); + const sampleCount = readU32Be(slice); if (sampleSize === 0) { for (let i = 0; i < sampleCount; i++) { - const sampleSize = this.metadataReader.readU32(); + const sampleSize = readU32Be(slice); track.sampleTable.sampleSizes.push(sampleSize); } } else { @@ -1458,13 +1492,13 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags - this.metadataReader.pos += 3; // Reserved + slice.skip(4); // Version + flags + slice.skip(3); // Reserved - const fieldSize = this.metadataReader.readU8(); // in bits - const sampleCount = this.metadataReader.readU32(); + const fieldSize = readU8(slice); // in bits + const sampleCount = readU32Be(slice); - const bytes = this.metadataReader.readBytes(Math.ceil(sampleCount * fieldSize / 8)); + const bytes = readBytes(slice, Math.ceil(sampleCount * fieldSize / 8)); const bitstream = new Bitstream(bytes); for (let i = 0; i < sampleCount; i++) { @@ -1481,13 +1515,13 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags track.sampleTable.keySampleIndices = []; - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); for (let i = 0; i < entryCount; i++) { - const sampleIndex = this.metadataReader.readU32() - 1; // Convert to 0-indexed + const sampleIndex = readU32Be(slice) - 1; // Convert to 0-indexed track.sampleTable.keySampleIndices.push(sampleIndex); } @@ -1506,14 +1540,14 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; + slice.skip(4); - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); for (let i = 0; i < entryCount; i++) { - const startChunkIndex = this.metadataReader.readU32() - 1; // Convert to 0-indexed - const samplesPerChunk = this.metadataReader.readU32(); - const sampleDescriptionIndex = this.metadataReader.readU32(); + const startChunkIndex = readU32Be(slice) - 1; // Convert to 0-indexed + const samplesPerChunk = readU32Be(slice); + const sampleDescriptionIndex = readU32Be(slice); track.sampleTable.sampleToChunk.push({ startSampleIndex: -1, @@ -1544,12 +1578,12 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); for (let i = 0; i < entryCount; i++) { - const chunkOffset = this.metadataReader.readU32(); + const chunkOffset = readU32Be(slice); track.sampleTable.chunkOffsets.push(chunkOffset); } }; break; @@ -1562,37 +1596,37 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const entryCount = this.metadataReader.readU32(); + const entryCount = readU32Be(slice); for (let i = 0; i < entryCount; i++) { - const chunkOffset = this.metadataReader.readU64(); + const chunkOffset = readU64Be(slice); track.sampleTable.chunkOffsets.push(chunkOffset); } }; break; case 'mvex': { this.isFragmented = true; - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); }; break; case 'mehd': { - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const version = readU8(slice); + slice.skip(3); // Flags - const fragmentDuration = version === 1 ? this.metadataReader.readU64() : this.metadataReader.readU32(); + const fragmentDuration = version === 1 ? readU64Be(slice) : readU32Be(slice); this.movieDurationInTimescale = fragmentDuration; }; break; case 'trex': { - this.metadataReader.pos += 4; // Version + flags + slice.skip(4); // Version + flags - const trackId = this.metadataReader.readU32(); - const defaultSampleDescriptionIndex = this.metadataReader.readU32(); - const defaultSampleDuration = this.metadataReader.readU32(); - const defaultSampleSize = this.metadataReader.readU32(); - const defaultSampleFlags = this.metadataReader.readU32(); + const trackId = readU32Be(slice); + const defaultSampleDescriptionIndex = readU32Be(slice); + const defaultSampleDuration = readU32Be(slice); + const defaultSampleSize = readU32Be(slice); + const defaultSampleFlags = readU32Be(slice); // We store these separately rather than in the tracks since the tracks may not exist yet this.fragmentTrackDefaults.push({ @@ -1605,10 +1639,10 @@ export class IsobmffDemuxer extends Demuxer { }; break; case 'tfra': { - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags + const version = readU8(slice); + slice.skip(3); // Flags - const trackId = this.metadataReader.readU32(); + const trackId = readU32Be(slice); const track = this.tracks.find(x => x.id === trackId); if (!track) { break; @@ -1616,30 +1650,26 @@ export class IsobmffDemuxer extends Demuxer { track.fragmentLookupTable = []; - const word = this.metadataReader.readU32(); + const word = readU32Be(slice); const lengthSizeOfTrafNum = (word & 0b110000) >> 4; const lengthSizeOfTrunNum = (word & 0b001100) >> 2; const lengthSizeOfSampleNum = word & 0b000011; - const x = this.metadataReader; - const functions = [x.readU8.bind(x), x.readU16.bind(x), x.readU24.bind(x), x.readU32.bind(x)]; + const functions = [readU8, readU16Be, readU24Be, readU32Be]; const readTrafNum = functions[lengthSizeOfTrafNum]!; const readTrunNum = functions[lengthSizeOfTrunNum]!; const readSampleNum = functions[lengthSizeOfSampleNum]!; - const numberOfEntries = this.metadataReader.readU32(); + const numberOfEntries = readU32Be(slice); for (let i = 0; i < numberOfEntries; i++) { - const time = version === 1 ? this.metadataReader.readU64() : this.metadataReader.readU32(); - const moofOffset = version === 1 ? this.metadataReader.readU64() : this.metadataReader.readU32(); + const time = version === 1 ? readU64Be(slice) : readU32Be(slice); + const moofOffset = version === 1 ? readU64Be(slice) : readU32Be(slice); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const trafNumber = readTrafNum(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const trunNumber = readTrunNum(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const sampleNumber = readSampleNum(); + readTrafNum(slice); + readTrunNum(slice); + readSampleNum(slice); track.fragmentLookupTable.push({ timestamp: time, @@ -1660,7 +1690,7 @@ export class IsobmffDemuxer extends Demuxer { isKnownToBeFirstFragment: false, }; - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); insertSorted(this.fragments, this.currentFragment, x => x.moofOffset); @@ -1685,7 +1715,7 @@ export class IsobmffDemuxer extends Demuxer { case 'traf': { assert(this.currentFragment); - this.readContiguousBoxes(boxInfo.contentSize); + this.readContiguousBoxes(slice.slice(contentStartPos, boxInfo.contentSize)); // It is possible that there is no current track, for example when we don't care about the track // referenced in the track fragment header. @@ -1722,9 +1752,9 @@ export class IsobmffDemuxer extends Demuxer { case 'tfhd': { assert(this.currentFragment); - this.metadataReader.pos += 1; // Version + slice.skip(1); // Version - const flags = this.metadataReader.readU24(); + const flags = readU24Be(slice); const baseDataOffsetPresent = Boolean(flags & 0x000001); const sampleDescriptionIndexPresent = Boolean(flags & 0x000002); const defaultSampleDurationPresent = Boolean(flags & 0x000008); @@ -1733,7 +1763,7 @@ export class IsobmffDemuxer extends Demuxer { const durationIsEmpty = Boolean(flags & 0x010000); const defaultBaseIsMoof = Boolean(flags & 0x020000); - const trackId = this.metadataReader.readU32(); + const trackId = readU32Be(slice); const track = this.tracks.find(x => x.id === trackId); if (!track) { // We don't care about this track @@ -1753,21 +1783,21 @@ export class IsobmffDemuxer extends Demuxer { }; if (baseDataOffsetPresent) { - track.currentFragmentState.baseDataOffset = this.metadataReader.readU64(); + track.currentFragmentState.baseDataOffset = readU64Be(slice); } else if (defaultBaseIsMoof) { track.currentFragmentState.baseDataOffset = this.currentFragment.moofOffset; } if (sampleDescriptionIndexPresent) { - track.currentFragmentState.sampleDescriptionIndex = this.metadataReader.readU32(); + track.currentFragmentState.sampleDescriptionIndex = readU32Be(slice); } if (defaultSampleDurationPresent) { - track.currentFragmentState.defaultSampleDuration = this.metadataReader.readU32(); + track.currentFragmentState.defaultSampleDuration = readU32Be(slice); } if (defaultSampleSizePresent) { - track.currentFragmentState.defaultSampleSize = this.metadataReader.readU32(); + track.currentFragmentState.defaultSampleSize = readU32Be(slice); } if (defaultSampleFlagsPresent) { - track.currentFragmentState.defaultSampleFlags = this.metadataReader.readU32(); + track.currentFragmentState.defaultSampleFlags = readU32Be(slice); } if (durationIsEmpty) { track.currentFragmentState.defaultSampleDuration = 0; @@ -1782,14 +1812,10 @@ export class IsobmffDemuxer extends Demuxer { assert(track.currentFragmentState); - // break; + const version = readU8(slice); + slice.skip(3); // Flags - const version = this.metadataReader.readU8(); - this.metadataReader.pos += 3; // Flags - - const baseMediaDecodeTime = version === 0 - ? this.metadataReader.readU32() - : this.metadataReader.readU64(); + const baseMediaDecodeTime = version === 0 ? readU32Be(slice) : readU64Be(slice); track.currentFragmentState.startTimestamp = baseMediaDecodeTime; }; break; @@ -1807,9 +1833,9 @@ export class IsobmffDemuxer extends Demuxer { break; } - const version = this.metadataReader.readU8(); + const version = readU8(slice); - const flags = this.metadataReader.readU24(); + const flags = readU24Be(slice); const dataOffsetPresent = Boolean(flags & 0x000001); const firstSampleFlagsPresent = Boolean(flags & 0x000004); const sampleDurationPresent = Boolean(flags & 0x000100); @@ -1817,15 +1843,15 @@ export class IsobmffDemuxer extends Demuxer { const sampleFlagsPresent = Boolean(flags & 0x000400); const sampleCompositionTimeOffsetsPresent = Boolean(flags & 0x000800); - const sampleCount = this.metadataReader.readU32(); + const sampleCount = readU32Be(slice); let dataOffset = track.currentFragmentState.baseDataOffset; if (dataOffsetPresent) { - dataOffset += this.metadataReader.readI32(); + dataOffset += readI32Be(slice); } let firstSampleFlags: number | null = null; if (firstSampleFlagsPresent) { - firstSampleFlags = this.metadataReader.readU32(); + firstSampleFlags = readU32Be(slice); } let currentOffset = dataOffset; @@ -1851,7 +1877,7 @@ export class IsobmffDemuxer extends Demuxer { for (let i = 0; i < sampleCount; i++) { let sampleDuration: number; if (sampleDurationPresent) { - sampleDuration = this.metadataReader.readU32(); + sampleDuration = readU32Be(slice); } else { assert(track.currentFragmentState.defaultSampleDuration !== null); sampleDuration = track.currentFragmentState.defaultSampleDuration; @@ -1859,7 +1885,7 @@ export class IsobmffDemuxer extends Demuxer { let sampleSize: number; if (sampleSizePresent) { - sampleSize = this.metadataReader.readU32(); + sampleSize = readU32Be(slice); } else { assert(track.currentFragmentState.defaultSampleSize !== null); sampleSize = track.currentFragmentState.defaultSampleSize; @@ -1867,7 +1893,7 @@ export class IsobmffDemuxer extends Demuxer { let sampleFlags: number; if (sampleFlagsPresent) { - sampleFlags = this.metadataReader.readU32(); + sampleFlags = readU32Be(slice); } else { assert(track.currentFragmentState.defaultSampleFlags !== null); sampleFlags = track.currentFragmentState.defaultSampleFlags; @@ -1879,9 +1905,9 @@ export class IsobmffDemuxer extends Demuxer { let sampleCompositionTimeOffset = 0; if (sampleCompositionTimeOffsetsPresent) { if (version === 0) { - sampleCompositionTimeOffset = this.metadataReader.readU32(); + sampleCompositionTimeOffset = readU32Be(slice); } else { - sampleCompositionTimeOffset = this.metadataReader.readI32(); + sampleCompositionTimeOffset = readI32Be(slice); } } @@ -1934,11 +1960,11 @@ export class IsobmffDemuxer extends Demuxer { break; } - this.currentTrack.name = textDecoder.decode(this.metadataReader.readBytes(boxInfo.contentSize)); + this.currentTrack.name = textDecoder.decode(readBytes(slice, boxInfo.contentSize)); }; break; } - this.metadataReader.pos = boxEndPos; + slice.filePos = boxEndPos; return true; } } @@ -2236,14 +2262,14 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { if (options.metadataOnly) { data = PLACEHOLDER_DATA; } else { - // Load the entire chunk - await this.internalTrack.demuxer.chunkReader.reader.loadRange( - sampleInfo.chunkOffset, - sampleInfo.chunkOffset + sampleInfo.chunkSize, + let slice = this.internalTrack.demuxer.reader.requestSlice( + sampleInfo.sampleOffset, + sampleInfo.sampleSize, ); + if (slice instanceof Promise) slice = await slice; + assert(slice); - this.internalTrack.demuxer.chunkReader.pos = sampleInfo.sampleOffset; - data = this.internalTrack.demuxer.chunkReader.readBytes(sampleInfo.sampleSize); + data = readBytes(slice, sampleInfo.sampleSize); } const timestamp = (sampleInfo.presentationTimestamp - this.internalTrack.editListOffset) @@ -2276,11 +2302,14 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { if (options.metadataOnly) { data = PLACEHOLDER_DATA; } else { - // Load the entire fragment - await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd); + let slice = this.internalTrack.demuxer.reader.requestSlice( + fragmentSample.byteOffset, + fragmentSample.byteSize, + ); + if (slice instanceof Promise) slice = await slice; + assert(slice); - this.internalTrack.demuxer.chunkReader.pos = fragmentSample.byteOffset; - data = this.internalTrack.demuxer.chunkReader.readBytes(fragmentSample.byteSize); + data = readBytes(slice, fragmentSample.byteSize); } const timestamp = (fragmentSample.presentationTimestamp - this.internalTrack.editListOffset) @@ -2388,8 +2417,8 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { return this.fetchPacketInFragment(fragment, sampleIndex, options); } - const metadataReader = demuxer.metadataReader; - const sourceSize = await metadataReader.reader.source.getSize(); + let sourceSize = demuxer.reader.requestSize(); + if (sourceSize instanceof Promise) sourceSize = await sourceSize; let prevFragment: Fragment | null = null; let bestFragmentIndex = fragmentIndex; @@ -2408,24 +2437,25 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { ? this.internalTrack.fragmentLookupTable![lookupEntryIndex]! : null; + let currentPos: number; let nextFragmentIsFirstFragment = false; if (fragmentIndex === -1) { - metadataReader.pos = lookupEntry?.moofOffset ?? 0; - nextFragmentIsFirstFragment = metadataReader.pos === 0; + currentPos = lookupEntry?.moofOffset ?? 0; + nextFragmentIsFirstFragment = currentPos === 0; } else { const fragment = this.internalTrack.fragments[fragmentIndex]!; if (!lookupEntry || fragment.moofOffset >= lookupEntry.moofOffset) { - metadataReader.pos = fragment.moofOffset + fragment.moofSize; + currentPos = fragment.moofOffset + fragment.moofSize; prevFragment = fragment; } else { // Use the lookup entry - metadataReader.pos = lookupEntry.moofOffset; + currentPos = lookupEntry.moofOffset; } } - while (metadataReader.pos < sourceSize) { + while (currentPos < sourceSize) { if (prevFragment) { const trackData = prevFragment.trackData.get(this.internalTrack.id); if (trackData && trackData.startTimestamp > latestTimestamp) { @@ -2435,16 +2465,19 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { if (prevFragment.nextFragment) { // Skip ahead quickly without needing to read the file again - metadataReader.pos = prevFragment.nextFragment.moofOffset + prevFragment.nextFragment.moofSize; + currentPos = prevFragment.nextFragment.moofOffset + prevFragment.nextFragment.moofSize; prevFragment = prevFragment.nextFragment; continue; } } // Load the header - await metadataReader.reader.loadRange(metadataReader.pos, metadataReader.pos + MAX_BOX_HEADER_SIZE); - const startPos = metadataReader.pos; - const boxInfo = metadataReader.readBoxHeader(); + 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 startPos = currentPos; + const boxInfo = readBoxHeader(slice); if (!boxInfo) { break; } @@ -2455,8 +2488,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { let fragment: Fragment; if (index === -1) { // This is the first time we've seen this fragment - metadataReader.pos = startPos; - fragment = await demuxer.readFragment(); + fragment = await demuxer.readFragment(startPos); } else { // We already know this fragment fragment = demuxer.fragments[index]!; @@ -2482,7 +2514,7 @@ abstract class IsobmffTrackBacking implements InputTrackBacking { } } - metadataReader.pos = startPos + boxInfo.totalSize; + currentPos = startPos + boxInfo.totalSize; } const bestFragment = bestFragmentIndex !== -1 ? this.internalTrack.fragments[bestFragmentIndex]! : null; diff --git a/src/isobmff/isobmff-reader.ts b/src/isobmff/isobmff-reader.ts index bbcd541..b3ffa29 100644 --- a/src/isobmff/isobmff-reader.ts +++ b/src/isobmff/isobmff-reader.ts @@ -6,137 +6,50 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Reader } from '../reader'; +import { FileSlice, readAscii, readI32Be, readU32Be, readU64Be, readU8 } from '../reader2'; export const MIN_BOX_HEADER_SIZE = 8; export const MAX_BOX_HEADER_SIZE = 16; -export class IsobmffReader { - pos = 0; +export const readBoxHeader = (slice: FileSlice) => { + let totalSize = readU32Be(slice); + const name = readAscii(slice, 4); + let headerSize = 8; - constructor(public reader: Reader) {} - - readBytes(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - return new Uint8Array(view.buffer, offset, length); + const hasLargeSize = totalSize === 1; + if (hasLargeSize) { + totalSize = readU64Be(slice); + headerSize = 16; } - readU8() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); - this.pos++; - - return view.getUint8(offset); + const contentSize = totalSize - headerSize; + if (contentSize < 0) { + return null; // Hardly a box is it } - readU16() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); - this.pos += 2; + return { name, totalSize, headerSize, contentSize }; +}; - return view.getUint16(offset, false); - } +export const readFixed_16_16 = (slice: FileSlice) => { + return readI32Be(slice) / 0x10000; +}; - readI16() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); - this.pos += 2; +export const readFixed_2_30 = (slice: FileSlice) => { + return readI32Be(slice) / 0x40000000; +}; - return view.getInt16(offset, false); - } +export const readIsomVariableInteger = (slice: FileSlice) => { + let result = 0; - readU24() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 3); - this.pos += 3; + for (let i = 0; i < 4; i++) { + result <<= 7; + const nextByte = readU8(slice); + result |= nextByte & 0x7f; - const high = view.getUint16(offset, false); - const low = view.getUint8(offset + 2); - return high * 0x100 + low; - } - - readU32() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); - this.pos += 4; - - return view.getUint32(offset, false); - } - - readI32() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); - this.pos += 4; - - return view.getInt32(offset, false); - } - - readU64() { - const high = this.readU32(); - const low = this.readU32(); - return high * 0x100000000 + low; - } - - readI64() { - const high = this.readI32(); - const low = this.readU32(); - return high * 0x100000000 + low; - } - - readF64() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 8); - this.pos += 8; - - return view.getFloat64(offset, false); - } - - readFixed_16_16() { - return this.readI32() / 0x10000; - } - - readFixed_2_30() { - return this.readI32() / 0x40000000; - } - - readAscii(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - let str = ''; - for (let i = 0; i < length; i++) { - str += String.fromCharCode(view.getUint8(offset + i)); + if ((nextByte & 0x80) === 0) { + break; } - return str; } - readIsomVariableInteger() { - let result = 0; - - for (let i = 0; i < 4; i++) { - result <<= 7; - const nextByte = this.readU8(); - result |= nextByte & 0x7f; - - if ((nextByte & 0x80) === 0) { - break; - } - } - - return result; - } - - readBoxHeader() { - let totalSize = this.readU32(); - const name = this.readAscii(4); - let headerSize = 8; - - const hasLargeSize = totalSize === 1; - if (hasLargeSize) { - totalSize = this.readU64(); - headerSize = 16; - } - - const contentSize = totalSize - headerSize; - if (contentSize < 0) { - return null; // Hardly a box is it - } - - return { name, totalSize, headerSize, contentSize }; - } -} + return result; +}; diff --git a/src/matroska/ebml.ts b/src/matroska/ebml.ts index 17207f8..276795a 100644 --- a/src/matroska/ebml.ts +++ b/src/matroska/ebml.ts @@ -8,7 +8,7 @@ import { MediaCodec } from '../codec'; import { assertNever, textDecoder, textEncoder } from '../misc'; -import { Reader } from '../reader'; +import { FileSlice, readBytes, Reader2, readF32Be, readF64Be, readU8 } from '../reader2'; import { Writer } from '../writer'; export interface EBMLElement { @@ -68,6 +68,7 @@ export enum EBMLId { DocType = 0x4282, DocTypeVersion = 0x4287, DocTypeReadVersion = 0x4285, + Segment = 0x18538067, SeekHead = 0x114d9b74, Seek = 0x4dbb, SeekID = 0x53ab, @@ -101,7 +102,6 @@ export enum EBMLId { SamplingFrequency = 0xb5, Channels = 0x9f, BitDepth = 0x6264, - Segment = 0x18538067, SimpleBlock = 0xa3, BlockGroup = 0xa0, Block = 0xa1, @@ -397,250 +397,215 @@ export class EBMLWriter { } } -const MAX_VAR_INT_SIZE = 8; +export const MAX_VAR_INT_SIZE = 8; export const MIN_HEADER_SIZE = 2; // 1-byte ID and 1-byte size export const MAX_HEADER_SIZE = 2 * MAX_VAR_INT_SIZE; // 8-byte ID and 8-byte size -export class EBMLReader { - pos = 0; +export const readVarIntSize = (slice: FileSlice) => { + const firstByte = readU8(slice); + slice.skip(-1); - constructor(public reader: Reader) {} - - readBytes(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - return new Uint8Array(view.buffer, offset, length); + if (firstByte === 0) { + return null; // Invalid VINT } - readU8() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); - this.pos++; - - return view.getUint8(offset); + let width = 1; + let mask = 0x80; + while ((firstByte & mask) === 0) { + width++; + mask >>= 1; } - readS16() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); - this.pos += 2; + return width; +}; - return view.getInt16(offset, false); +export const readVarInt = (slice: FileSlice) => { + // Read the first byte to determine the width of the variable-length integer + const firstByte = readU8(slice); + + if (firstByte === 0) { + return null; // Invalid VINT } - readVarIntSize() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); - const firstByte = view.getUint8(offset); - - if (firstByte === 0) { - return null; // Invalid VINT - } - - let width = 1; - let mask = 0x80; - while ((firstByte & mask) === 0) { - width++; - mask >>= 1; - } - - return width; + // Find the position of VINT_MARKER, which determines the width + let width = 1; + let mask = 1 << 7; + while ((firstByte & mask) === 0) { + width++; + mask >>= 1; } - readVarInt() { - // Read the first byte to determine the width of the variable-length integer - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); - const firstByte = view.getUint8(offset); + // First byte's value needs the marker bit cleared + let value = firstByte & (mask - 1); - if (firstByte === 0) { - return null; // Invalid VINT - } - - // Find the position of VINT_MARKER, which determines the width - let width = 1; - let mask = 1 << 7; - while ((firstByte & mask) === 0) { - width++; - mask >>= 1; - } - - const { view: fullView, offset: fullOffset } = this.reader.getViewAndOffset(this.pos, this.pos + width); - - // First byte's value needs the marker bit cleared - let value = firstByte & (mask - 1); - - // Read remaining bytes - for (let i = 1; i < width; i++) { - value *= 1 << 8; - value += fullView.getUint8(fullOffset + i); - } - - this.pos += width; - return value; + // Read remaining bytes + for (let i = 1; i < width; i++) { + value *= 1 << 8; + value += readU8(slice); } - readUnsignedInt(width: number) { - if (width < 1 || width > 8) { - throw new Error('Bad unsigned int size ' + width); - } + return value; +}; - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + width); - let value = 0; - - // Read bytes from most significant to least significant - for (let i = 0; i < width; i++) { - value *= 1 << 8; - value += view.getUint8(offset + i); - } - - this.pos += width; - return value; +export const readUnsignedInt = (slice: FileSlice, width: number) => { + if (width < 1 || width > 8) { + throw new Error('Bad unsigned int size ' + width); } - readSignedInt(width: number) { - let value = this.readUnsignedInt(width); + let value = 0; - // If the highest bit is set, convert from two's complement - if (value & (1 << (width * 8 - 1))) { - value -= 2 ** (width * 8); - } - - return value; + // Read bytes from most significant to least significant + for (let i = 0; i < width; i++) { + value *= 1 << 8; + value += readU8(slice); } - readFloat(width: number) { - if (width === 0) { - return 0; - } + return value; +}; - if (width !== 4 && width !== 8) { - throw new Error('Bad float size ' + width); - } +export const readSignedInt = (slice: FileSlice, width: number) => { + let value = readUnsignedInt(slice, width); - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + width); - const value = width === 4 ? view.getFloat32(offset, false) : view.getFloat64(offset, false); - - this.pos += width; - return value; + // If the highest bit is set, convert from two's complement + if (value & (1 << (width * 8 - 1))) { + value -= 2 ** (width * 8); } - readAsciiString(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; + return value; +}; - // Actual string length might be shorter due to null terminators - let strLength = 0; - while (strLength < length && view.getUint8(offset + strLength) !== 0) { - strLength += 1; - } - - return String.fromCharCode(...new Uint8Array(view.buffer, offset, strLength)); +export const readElementId = (slice: FileSlice) => { + const size = readVarIntSize(slice); + if (size === null) { + return null; } - readUnicodeString(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; + const id = readUnsignedInt(slice, size); + return id; +}; - // Actual string length might be shorter due to null terminators - let strLength = 0; - while (strLength < length && view.getUint8(offset + strLength) !== 0) { - strLength += 1; - } +export const readElementSize = (slice: FileSlice) => { + let size: number | null = readU8(slice); - return textDecoder.decode(new Uint8Array(view.buffer, offset, strLength)); - } + if (size === 0xff) { + size = null; + } else { + slice.skip(-1); + size = readVarInt(slice); - readElementId() { - const size = this.readVarIntSize(); - if (size === null) { - return null; - } - - const id = this.readUnsignedInt(size); - return id; - } - - readElementSize() { - let size: number | null = this.readU8(); - - if (size === 0xff) { + // In some (livestreamed) files, this is the value of the size field. While this technically is just a very + // large number, it is intended to behave like the reserved size 0xFF, meaning the size is undefined. We + // catch the number here. Note that it cannot be perfectly represented as a double, but the comparison works + // nonetheless. + // eslint-disable-next-line no-loss-of-precision + if (size === 0x00ffffffffffffff) { size = null; - } else { - this.pos--; - size = this.readVarInt(); - - // In some (livestreamed) files, this is the value of the size field. While this technically is just a very - // large number, it is intended to behave like the reserved size 0xFF, meaning the size is undefined. We - // catch the number here. Note that it cannot be perfectly represented as a double, but the comparison works - // nonetheless. - // eslint-disable-next-line no-loss-of-precision - if (size === 0x00ffffffffffffff) { - size = null; - } } - - return size; } - readElementHeader() { - const id = this.readElementId(); - if (id === null) { - return null; - } - - const size = this.readElementSize(); - - return { id, size }; - } - - /** Returns the byte offset in the file of the next element with a matching ID. */ - async searchForNextElementId(ids: EBMLId[], until: number) { - const loadChunkSize = 2 ** 20; // 1 MiB - const idsSet = new Set(ids); - - while (this.pos <= until - MIN_HEADER_SIZE) { - if (!this.reader.rangeIsLoaded(this.pos, Math.min(this.pos + MAX_HEADER_SIZE, until))) { - await this.reader.loadRange(this.pos, Math.min(this.pos + loadChunkSize, until)); - } - - const elementStartPos = this.pos; - const elementHeader = this.readElementHeader(); - if (!elementHeader) { - break; - } - - if (idsSet.has(elementHeader.id)) { - return elementStartPos; - } - - assertDefinedSize(elementHeader.size); - - this.pos += elementHeader.size; - } + return size; +}; +export const readElementHeader = (slice: FileSlice) => { + const id = readElementId(slice); + if (id === null) { return null; } - /** Searches for the next occurrence of an element ID using a naive byte-wise search. */ - async resync(ids: EBMLId[], until: number) { - const loadChunkSize = 2 ** 20; // 1 MiB - const idsSet = new Set(ids); + const size = readElementSize(slice); - while (this.pos <= until - MIN_HEADER_SIZE) { - if (!this.reader.rangeIsLoaded(this.pos, Math.min(this.pos + MAX_HEADER_SIZE, until))) { - await this.reader.loadRange(this.pos, Math.min(this.pos + loadChunkSize, until)); - } + return { id, size }; +}; - const elementStartPos = this.pos; - const elementId = this.readElementId(); +export const readAsciiString = (slice: FileSlice, length: number) => { + const bytes = readBytes(slice, length); + + // Actual string length might be shorter due to null terminators + let strLength = 0; + while (strLength < length && bytes[strLength] !== 0) { + strLength += 1; + } + + return String.fromCharCode(...bytes.subarray(0, strLength)); +}; + +export const readUnicodeString = (slice: FileSlice, length: number) => { + const bytes = readBytes(slice, length); + + // Actual string length might be shorter due to null terminators + let strLength = 0; + while (strLength < length && bytes[strLength] !== 0) { + strLength += 1; + } + + return textDecoder.decode(bytes.subarray(0, strLength)); +}; + +export const readFloat = (slice: FileSlice, width: number) => { + if (width === 0) { + return 0; + } + + if (width !== 4 && width !== 8) { + throw new Error('Bad float size ' + width); + } + + return width === 4 ? readF32Be(slice) : readF64Be(slice); +}; + +/** Returns the byte offset in the file of the next element with a matching ID. */ +export const searchForNextElementId = async (reader: Reader2, startPos: number, ids: EBMLId[], until: number) => { + const idsSet = new Set(ids); + let currentPos = startPos; + + while (currentPos < until) { + let slice = reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + + const elementHeader = readElementHeader(slice); + if (!elementHeader) { + break; + } + + if (idsSet.has(elementHeader.id)) { + return currentPos; + } + + assertDefinedSize(elementHeader.size); + + currentPos = slice.filePos + elementHeader.size; + } + + return null; +}; + +/** Searches for the next occurrence of an element ID using a naive byte-wise search. */ +export const resync = async (reader: Reader2, startPos: number, ids: EBMLId[], until: number) => { + const CHUNK_SIZE = 2 ** 16; // So we don't need to grab thousands of slices + const idsSet = new Set(ids); + let currentPos = startPos; + + while (currentPos < until) { + let slice = reader.requestSliceRange(currentPos, 0, Math.min(CHUNK_SIZE, until - currentPos)); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + if (slice.length < MAX_VAR_INT_SIZE) break; + + for (let i = 0; i < slice.length - MAX_VAR_INT_SIZE; i++) { + slice.filePos = currentPos; + + const elementId = readElementId(slice); if (elementId !== null && idsSet.has(elementId)) { - return elementStartPos; + return currentPos; } - this.pos = elementStartPos + 1; + currentPos++; } - - return null; } -} + + return null; +}; export const CODEC_STRING_MAP: Partial> = { 'avc': 'V_MPEG4/ISO/AVC', @@ -667,38 +632,6 @@ export const CODEC_STRING_MAP: Partial> = { 'webvtt': 'S_TEXT/WEBVTT', }; -export const readVarInt = (data: Uint8Array, offset: number) => { - if (offset >= data.length) { - throw new Error('Offset out of bounds.'); - } - - // Read the first byte to determine the width of the variable-length integer - const firstByte = data[offset]!; - - // Find the position of VINT_MARKER, which determines the width - let width = 1; - let mask = 1 << 7; - while ((firstByte & mask) === 0 && width < 8) { - width++; - mask >>= 1; - } - - if (offset + width > data.length) { - throw new Error('VarInt extends beyond data bounds.'); - } - - // First byte's value needs the marker bit cleared - let value = firstByte & (mask - 1); - - // Read remaining bytes - for (let i = 1; i < width; i++) { - value *= 1 << 8; - value += data[offset + i]!; - } - - return { value, width }; -}; - export function assertDefinedSize(size: number | null): asserts size is number { if (size === null) { throw new Error('Undefined element size is used in a place where it is not supported.'); diff --git a/src/matroska/matroska-demuxer.ts b/src/matroska/matroska-demuxer.ts index e044c59..4d66609 100644 --- a/src/matroska/matroska-demuxer.ts +++ b/src/matroska/matroska-demuxer.ts @@ -49,19 +49,27 @@ import { UNDETERMINED_LANGUAGE, } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; -import { Reader } from '../reader'; import { assertDefinedSize, CODEC_STRING_MAP, EBMLId, - EBMLReader, LEVEL_0_AND_1_EBML_IDS, LEVEL_1_EBML_IDS, MAX_HEADER_SIZE, MIN_HEADER_SIZE, + readAsciiString, + readUnicodeString, + readElementHeader, + readElementId, + readFloat, + readSignedInt, + readUnsignedInt, readVarInt, + resync, + searchForNextElementId, } from './ebml'; import { buildMatroskaMimeType } from './matroska-misc'; +import { FileSlice, readBytes, Reader2, readI16Be, readU8 } from '../reader2'; type Segment = { seekHeadSeen: boolean; @@ -180,8 +188,7 @@ const METADATA_ELEMENTS = [ const MAX_RESYNC_LENGTH = 10 * 2 ** 20; // 10 MiB export class MatroskaDemuxer extends Demuxer { - metadataReader: EBMLReader; - clusterReader: EBMLReader; + reader: Reader2; readMetadataPromise: Promise | null = null; @@ -197,10 +204,7 @@ export class MatroskaDemuxer extends Demuxer { constructor(input: Input) { super(input); - this.metadataReader = new EBMLReader(input._mainReader); - - // Max 64 MiB of stored clusters - this.clusterReader = new EBMLReader(new Reader(input.source, 64 * 2 ** 20)); + this.reader = input._reader2; } override async computeDuration() { @@ -230,33 +234,35 @@ export class MatroskaDemuxer extends Demuxer { readMetadata() { return this.readMetadataPromise ??= (async () => { - this.metadataReader.pos = 0; - - const fileSize = await this.input.source.getSize(); + let fileSize = this.reader.requestSize(); + if (fileSize instanceof Promise) fileSize = await fileSize; // Loop over all top-level elements in the file - while (this.metadataReader.pos <= fileSize - MIN_HEADER_SIZE) { - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + MAX_HEADER_SIZE, - ); + let currentPos = 0; + while (currentPos < fileSize) { + let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; - const header = this.metadataReader.readElementHeader(); + const header = readElementHeader(slice); if (!header) { break; // Zero padding at the end of the file triggers this, for example } const id = header.id; let size = header.size; - const startPos = this.metadataReader.pos; + const dataStartPos = slice.filePos; if (id === EBMLId.EBML) { assertDefinedSize(size); - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size); - this.readContiguousElements(this.metadataReader, size); + let slice = this.reader.requestSlice(dataStartPos, size); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + + this.readContiguousElements(slice); } else if (id === EBMLId.Segment) { // Segment found! - await this.readSegment(size); + await this.readSegment(dataStartPos, size); if (size === null) { // Segment sizes can be undefined (common in livestreamed files), so assume this is the last @@ -271,29 +277,29 @@ export class MatroskaDemuxer extends Demuxer { if (size === null) { // Just in case this is one of those weird sizeless clusters, let's do our best and still try to // determine its size. - const nextElementPos = await this.clusterReader.searchForNextElementId( + const nextElementPos = await searchForNextElementId( + this.reader, + dataStartPos, LEVEL_0_AND_1_EBML_IDS, fileSize, ); - size = (nextElementPos ?? fileSize) - startPos; + size = (nextElementPos ?? fileSize) - dataStartPos; } const lastSegment = last(this.segments); if (lastSegment) { // Extend the previous segment's size - lastSegment.elementEndPos = startPos + size; + lastSegment.elementEndPos = dataStartPos + size; } } assertDefinedSize(size); - this.metadataReader.pos = startPos + size; + currentPos = dataStartPos + size; } })(); } - async readSegment(dataSize: number | null) { - const segmentDataStart = this.metadataReader.pos; - + async readSegment(segmentDataStart: number, dataSize: number | null) { this.currentSegment = { seekHeadSeen: false, infoSeen: false, @@ -318,33 +324,29 @@ export class MatroskaDemuxer extends Demuxer { }; this.segments.push(this.currentSegment); - // Let's load a good amount of data, enough for all segment metadata to likely fit into (minus cues) - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + 2 ** 14, - ); - + let currentPos = 0; let clusterEncountered = false; - while (this.metadataReader.pos <= this.currentSegment.elementEndPos - MIN_HEADER_SIZE) { - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + MAX_HEADER_SIZE, - ); - const elementStartPos = this.metadataReader.pos; - const header = this.metadataReader.readElementHeader(); + while (currentPos < this.currentSegment.elementEndPos) { + let slice = this.reader.requestSliceRange(currentPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + + const elementStartPos = currentPos; + const header = readElementHeader(slice); if (!header || !LEVEL_1_EBML_IDS.includes(header.id)) { // Potential junk. Let's try to resync - this.metadataReader.pos = elementStartPos; - const nextPos = await this.metadataReader.resync( + const nextPos = await resync( + this.reader, + elementStartPos, LEVEL_1_EBML_IDS, - Math.min(this.currentSegment.elementEndPos, this.metadataReader.pos + MAX_RESYNC_LENGTH), + Math.min(this.currentSegment.elementEndPos, elementStartPos + MAX_RESYNC_LENGTH), ); if (nextPos) { - this.metadataReader.pos = nextPos; + currentPos = nextPos; continue; } else { break; // Resync failed @@ -352,7 +354,7 @@ export class MatroskaDemuxer extends Demuxer { } const { id, size } = header; - const dataStartPos = this.metadataReader.pos; + const dataStartPos = slice.filePos; const metadataElementIndex = METADATA_ELEMENTS.findIndex(x => x.id === id); if (metadataElementIndex !== -1) { @@ -360,8 +362,13 @@ export class MatroskaDemuxer extends Demuxer { this.currentSegment[field] = true; assertDefinedSize(size); - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size); - this.readContiguousElements(this.metadataReader, size); + + let slice = this.reader.requestSlice(dataStartPos, size); + if (slice instanceof Promise) slice = await slice; + + if (slice) { + this.readContiguousElements(slice); + } } else if (id === EBMLId.Cluster) { if (!clusterEncountered) { clusterEncountered = true; @@ -370,7 +377,7 @@ export class MatroskaDemuxer extends Demuxer { } if (size !== null) { - this.metadataReader.pos = dataStartPos + size; + currentPos = dataStartPos + size; } if (this.currentSegment.infoSeen && this.currentSegment.tracksSeen && this.currentSegment.cuesSeen) { @@ -411,7 +418,7 @@ export class MatroskaDemuxer extends Demuxer { // The seek head points us to the first cluster, nice this.currentSegment.clusterSeekStartPos = segmentDataStart + seekEntry.segmentPosition; } else { - this.currentSegment.clusterSeekStartPos = this.metadataReader.pos; + this.currentSegment.clusterSeekStartPos = currentPos; } } @@ -422,12 +429,15 @@ export class MatroskaDemuxer extends Demuxer { const seekEntry = this.currentSegment.seekEntries.find(entry => entry.id === target.id); if (!seekEntry) continue; - this.metadataReader.pos = segmentDataStart + seekEntry.segmentPosition; - await this.metadataReader.reader.loadRange( - this.metadataReader.pos, - this.metadataReader.pos + 2 ** 12, // Load a larger range, assuming the correct element will be there + let slice = this.reader.requestSliceRange( + segmentDataStart + seekEntry.segmentPosition, + MIN_HEADER_SIZE, + MAX_HEADER_SIZE, ); - const header = this.metadataReader.readElementHeader(); + if (slice instanceof Promise) slice = await slice; + if (!slice) continue; + + const header = readElementHeader(slice); if (!header) continue; const { id, size } = header; @@ -436,8 +446,12 @@ export class MatroskaDemuxer extends Demuxer { assertDefinedSize(size); this.currentSegment[target.flag] = true; - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size); - this.readContiguousElements(this.metadataReader, size); + + let dataSlice = this.reader.requestSlice(slice.filePos, size); + if (dataSlice instanceof Promise) dataSlice = await dataSlice; + if (!dataSlice) continue; + + this.readContiguousElements(dataSlice); } if (this.currentSegment.timestampScale === -1) { @@ -502,23 +516,26 @@ export class MatroskaDemuxer extends Demuxer { this.currentSegment = null; } - async readCluster(segment: Segment) { - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + MAX_HEADER_SIZE); + async readCluster(startPos: number, segment: Segment) { + let headerSlice = this.reader.requestSliceRange(startPos, MIN_HEADER_SIZE, MAX_HEADER_SIZE); + if (headerSlice instanceof Promise) headerSlice = await headerSlice; + assert(headerSlice); - const elementStartPos = this.metadataReader.pos; - const elementHeader = this.metadataReader.readElementHeader(); + const elementStartPos = startPos; + const elementHeader = readElementHeader(headerSlice); assert(elementHeader); const id = elementHeader.id; let size = elementHeader.size; - const dataStartPos = this.metadataReader.pos; + const dataStartPos = headerSlice.filePos; if (size === null) { // The cluster's size is undefined (can happen in livestreamed files). We'd still like to know the size of // it, so we have no other choice but to iterate over the EBML structure until we find an element at level // 0 or 1, indicating the end of the cluster (all elements inside the cluster are at level 2). - this.clusterReader.pos = dataStartPos; - const nextElementPos = await this.clusterReader.searchForNextElementId( + const nextElementPos = await searchForNextElementId( + this.reader, + dataStartPos, LEVEL_0_AND_1_EBML_IDS, segment.elementEndPos, ); @@ -529,8 +546,8 @@ export class MatroskaDemuxer extends Demuxer { assert(id === EBMLId.Cluster); // Load the entire cluster - this.clusterReader.pos = dataStartPos; - await this.clusterReader.reader.loadRange(this.clusterReader.pos, this.clusterReader.pos + size); + let dataSlice = this.reader.requestSlice(dataStartPos, size); + if (dataSlice instanceof Promise) dataSlice = await dataSlice; const cluster: Cluster = { elementStartPos, @@ -542,7 +559,10 @@ export class MatroskaDemuxer extends Demuxer { isKnownToBeFirstCluster: false, }; this.currentCluster = cluster; - this.readContiguousElements(this.clusterReader, size); + + if (dataSlice) { + this.readContiguousElements(dataSlice); + } for (const [trackId, trackData] of cluster.trackData) { const track = segment.tracks.find(x => x.id === trackId) ?? null; @@ -651,12 +671,10 @@ export class MatroskaDemuxer extends Demuxer { continue; } - const data = originalBlock.data; - let pos = 0; + const slice = FileSlice.tempFromBytes(originalBlock.data); const frameSizes: number[] = []; - const frameCount = data[pos]! + 1; - pos++; + const frameCount = readU8(slice) + 1; switch (originalBlock.lacing) { case BlockLacing.Xiph: { @@ -666,10 +684,9 @@ export class MatroskaDemuxer extends Demuxer { for (let i = 0; i < frameCount - 1; i++) { let frameSize = 0; - while (pos < data.length) { - const value = data[pos]!; + while (slice.bufferPos < slice.length) { + const value = readU8(slice); frameSize += value; - pos++; if (value < 255) { frameSizes.push(frameSize); @@ -681,12 +698,12 @@ export class MatroskaDemuxer extends Demuxer { } // Compute the last frame's size from whatever's left - frameSizes.push(data.length - (pos + totalUsedSize)); + frameSizes.push(slice.length - (slice.bufferPos + totalUsedSize)); }; break; case BlockLacing.FixedSize: { // Fixed size lacing: all frames have same size - const totalDataSize = data.length - 1; // Minus the frame count byte + const totalDataSize = slice.length - 1; // Minus the frame count byte const frameSize = Math.floor(totalDataSize / frameCount); for (let i = 0; i < frameCount; i++) { @@ -696,28 +713,32 @@ export class MatroskaDemuxer extends Demuxer { case BlockLacing.Ebml: { // EBML lacing: first size absolute, subsequent ones are coded as signed differences from the last - const firstResult = readVarInt(data, pos); - let currentSize = firstResult.value; + const firstResult = readVarInt(slice); + assert(firstResult !== null); // Assume it's not an invalid VINT + + let currentSize = firstResult; frameSizes.push(currentSize); - pos += firstResult.width; let totalUsedSize = currentSize; for (let i = 1; i < frameCount - 1; i++) { - const diffResult = readVarInt(data, pos); - const unsignedDiff = diffResult.value; - const bias = (1 << (diffResult.width * 7 - 1)) - 1; // Typo-corrected version of 2^((7*n)-1)^-1 + const startPos = slice.bufferPos; + const diffResult = readVarInt(slice); + assert(diffResult !== null); + + const unsignedDiff = diffResult; + const width = slice.bufferPos - startPos; + const bias = (1 << (width * 7 - 1)) - 1; // Typo-corrected version of 2^((7*n)-1)^-1 const diff = unsignedDiff - bias; currentSize += diff; frameSizes.push(currentSize); - pos += diffResult.width; totalUsedSize += currentSize; } // Compute the last frame's size from whatever's left - frameSizes.push(data.length - (pos + totalUsedSize)); + frameSizes.push(slice.length - (slice.bufferPos + totalUsedSize)); }; break; default: assert(false); @@ -726,12 +747,11 @@ export class MatroskaDemuxer extends Demuxer { assert(frameSizes.length === frameCount); blocks.splice(blockIndex, 1); // Remove the original block - let dataOffset = pos; // Now, let's insert each frame as its own block for (let i = 0; i < frameCount; i++) { const frameSize = frameSizes[i]!; - const frameData = data.subarray(dataOffset, dataOffset + frameSize); + const frameData = readBytes(slice, frameSize); const blockDuration = originalBlock.duration || (frameCount * (track?.defaultDuration ?? 0)); @@ -747,8 +767,6 @@ export class MatroskaDemuxer extends Demuxer { data: frameData, lacing: BlockLacing.None, }); - - dataOffset += frameSize; } blockIndex += frameCount; // Skip the blocks we just added @@ -756,11 +774,11 @@ export class MatroskaDemuxer extends Demuxer { } } - readContiguousElements(reader: EBMLReader, totalSize: number) { - const startIndex = reader.pos; + readContiguousElements(slice: FileSlice) { + const startIndex = slice.filePos; - while (reader.pos - startIndex <= totalSize - MIN_HEADER_SIZE) { - const foundElement = this.traverseElement(reader); + while (slice.filePos - startIndex <= slice.length - MIN_HEADER_SIZE) { + const foundElement = this.traverseElement(slice); if (!foundElement) { break; @@ -768,26 +786,26 @@ export class MatroskaDemuxer extends Demuxer { } } - traverseElement(reader: EBMLReader): boolean { - const header = reader.readElementHeader(); + traverseElement(slice: FileSlice): boolean { + const header = readElementHeader(slice); if (!header) { return false; } const { id, size } = header; - const dataStartPos = reader.pos; + const dataStartPos = slice.filePos; assertDefinedSize(size); switch (id) { case EBMLId.DocType: { - this.isWebM = reader.readAsciiString(size) === 'webm'; + this.isWebM = readAsciiString(slice, size) === 'webm'; }; break; case EBMLId.Seek: { if (!this.currentSegment) break; const seekEntry: SeekEntry = { id: -1, segmentPosition: -1 }; this.currentSegment.seekEntries.push(seekEntry); - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); if (seekEntry.id === -1 || seekEntry.segmentPosition === -1) { this.currentSegment.seekEntries.pop(); @@ -798,27 +816,27 @@ export class MatroskaDemuxer extends Demuxer { const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1]; if (!lastSeekEntry) break; - lastSeekEntry.id = reader.readUnsignedInt(size); + lastSeekEntry.id = readUnsignedInt(slice, size); }; break; case EBMLId.SeekPosition: { const lastSeekEntry = this.currentSegment?.seekEntries[this.currentSegment.seekEntries.length - 1]; if (!lastSeekEntry) break; - lastSeekEntry.segmentPosition = reader.readUnsignedInt(size); + lastSeekEntry.segmentPosition = readUnsignedInt(slice, size); }; break; case EBMLId.TimestampScale: { if (!this.currentSegment) break; - this.currentSegment.timestampScale = reader.readUnsignedInt(size); + this.currentSegment.timestampScale = readUnsignedInt(slice, size); this.currentSegment.timestampFactor = 1e9 / this.currentSegment.timestampScale; }; break; case EBMLId.Duration: { if (!this.currentSegment) break; - this.currentSegment.duration = reader.readFloat(size); + this.currentSegment.duration = readFloat(slice, size); }; break; case EBMLId.TrackEntry: { @@ -842,7 +860,7 @@ export class MatroskaDemuxer extends Demuxer { info: null, }; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); if ( this.currentTrack @@ -941,13 +959,13 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.TrackNumber: { if (!this.currentTrack) break; - this.currentTrack.id = reader.readUnsignedInt(size); + this.currentTrack.id = readUnsignedInt(slice, size); }; break; case EBMLId.TrackType: { if (!this.currentTrack) break; - const type = reader.readUnsignedInt(size); + const type = readUnsignedInt(slice, size); if (type === 1) { this.currentTrack.info = { type: 'video', @@ -974,7 +992,7 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.FlagEnabled: { if (!this.currentTrack) break; - const enabled = reader.readUnsignedInt(size); + const enabled = readUnsignedInt(slice, size); if (!enabled) { this.currentSegment!.tracks.pop(); this.currentTrack = null; @@ -984,32 +1002,32 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.FlagDefault: { if (!this.currentTrack) break; - this.currentTrack.isDefault = !!reader.readUnsignedInt(size); + this.currentTrack.isDefault = !!readUnsignedInt(slice, size); }; break; case EBMLId.CodecID: { if (!this.currentTrack) break; - this.currentTrack.codecId = reader.readAsciiString(size); + this.currentTrack.codecId = readAsciiString(slice, size); }; break; case EBMLId.CodecPrivate: { if (!this.currentTrack) break; - this.currentTrack.codecPrivate = reader.readBytes(size); + this.currentTrack.codecPrivate = readBytes(slice, size); }; break; case EBMLId.DefaultDuration: { if (!this.currentTrack) break; this.currentTrack.defaultDuration - = this.currentTrack.segment.timestampFactor * reader.readUnsignedInt(size) / 1e9; + = this.currentTrack.segment.timestampFactor * readUnsignedInt(slice, size) / 1e9; }; break; case EBMLId.Name: { if (!this.currentTrack) break; - this.currentTrack.name = reader.readUnicodeString(size); + this.currentTrack.name = readUnicodeString(slice, size); }; break; case EBMLId.Language: { @@ -1019,7 +1037,7 @@ export class MatroskaDemuxer extends Demuxer { break; } - this.currentTrack.languageCode = reader.readAsciiString(size); + this.currentTrack.languageCode = readAsciiString(slice, size); if (!isIso639Dash2LanguageCode(this.currentTrack.languageCode)) { this.currentTrack.languageCode = UNDETERMINED_LANGUAGE; @@ -1029,7 +1047,7 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.LanguageBCP47: { if (!this.currentTrack) break; - const bcp47 = reader.readAsciiString(size); + const bcp47 = readAsciiString(slice, size); const languageSubtag = bcp47.split('-')[0]; if (languageSubtag) { @@ -1046,32 +1064,32 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Video: { if (this.currentTrack?.info?.type !== 'video') break; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); }; break; case EBMLId.PixelWidth: { if (this.currentTrack?.info?.type !== 'video') break; - this.currentTrack.info.width = reader.readUnsignedInt(size); + this.currentTrack.info.width = readUnsignedInt(slice, size); }; break; case EBMLId.PixelHeight: { if (this.currentTrack?.info?.type !== 'video') break; - this.currentTrack.info.height = reader.readUnsignedInt(size); + this.currentTrack.info.height = readUnsignedInt(slice, size); }; break; case EBMLId.Colour: { if (this.currentTrack?.info?.type !== 'video') break; this.currentTrack.info.colorSpace = {}; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); }; break; case EBMLId.MatrixCoefficients: { if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; - const matrixCoefficients = reader.readUnsignedInt(size); + const matrixCoefficients = readUnsignedInt(slice, size); const mapped = MATRIX_COEFFICIENTS_MAP_INVERSE[matrixCoefficients] ?? null; this.currentTrack.info.colorSpace.matrix = mapped as VideoColorSpaceInit['matrix']; }; break; @@ -1079,13 +1097,13 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Range: { if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; - this.currentTrack.info.colorSpace.fullRange = reader.readUnsignedInt(size) === 2; + this.currentTrack.info.colorSpace.fullRange = readUnsignedInt(slice, size) === 2; }; break; case EBMLId.TransferCharacteristics: { if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; - const transferCharacteristics = reader.readUnsignedInt(size); + const transferCharacteristics = readUnsignedInt(slice, size); const mapped = TRANSFER_CHARACTERISTICS_MAP_INVERSE[transferCharacteristics] ?? null; this.currentTrack.info.colorSpace.transfer = mapped as VideoColorSpaceInit['transfer']; }; break; @@ -1093,7 +1111,7 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Primaries: { if (this.currentTrack?.info?.type !== 'video' || !this.currentTrack.info.colorSpace) break; - const primaries = reader.readUnsignedInt(size); + const primaries = readUnsignedInt(slice, size); const mapped = COLOR_PRIMARIES_MAP_INVERSE[primaries] ?? null; this.currentTrack.info.colorSpace.primaries = mapped as VideoColorSpaceInit['primaries']; }; break; @@ -1101,13 +1119,13 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Projection: { if (this.currentTrack?.info?.type !== 'video') break; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); }; break; case EBMLId.ProjectionPoseRoll: { if (this.currentTrack?.info?.type !== 'video') break; - const rotation = reader.readFloat(size); + const rotation = readFloat(slice, size); const flippedRotation = -rotation; // Convert counter-clockwise to clockwise try { @@ -1120,36 +1138,36 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Audio: { if (this.currentTrack?.info?.type !== 'audio') break; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); }; break; case EBMLId.SamplingFrequency: { if (this.currentTrack?.info?.type !== 'audio') break; - this.currentTrack.info.sampleRate = reader.readFloat(size); + this.currentTrack.info.sampleRate = readFloat(slice, size); }; break; case EBMLId.Channels: { if (this.currentTrack?.info?.type !== 'audio') break; - this.currentTrack.info.numberOfChannels = reader.readUnsignedInt(size); + this.currentTrack.info.numberOfChannels = readUnsignedInt(slice, size); }; break; case EBMLId.BitDepth: { if (this.currentTrack?.info?.type !== 'audio') break; - this.currentTrack.info.bitDepth = reader.readUnsignedInt(size); + this.currentTrack.info.bitDepth = readUnsignedInt(slice, size); }; break; case EBMLId.CuePoint: { if (!this.currentSegment) break; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); this.currentCueTime = null; }; break; case EBMLId.CueTime: { - this.currentCueTime = reader.readUnsignedInt(size); + this.currentCueTime = readUnsignedInt(slice, size); }; break; case EBMLId.CueTrackPositions: { @@ -1158,7 +1176,7 @@ export class MatroskaDemuxer extends Demuxer { const cuePoint: CuePoint = { time: this.currentCueTime, trackId: -1, clusterPosition: -1 }; this.currentSegment.cuePoints.push(cuePoint); - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); if (cuePoint.trackId === -1 || cuePoint.clusterPosition === -1) { this.currentSegment.cuePoints.pop(); @@ -1169,7 +1187,7 @@ export class MatroskaDemuxer extends Demuxer { const lastCuePoint = this.currentSegment?.cuePoints[this.currentSegment.cuePoints.length - 1]; if (!lastCuePoint) break; - lastCuePoint.trackId = reader.readUnsignedInt(size); + lastCuePoint.trackId = readUnsignedInt(slice, size); }; break; case EBMLId.CueClusterPosition: { @@ -1177,24 +1195,24 @@ export class MatroskaDemuxer extends Demuxer { if (!lastCuePoint) break; assert(this.currentSegment); - lastCuePoint.clusterPosition = this.currentSegment.dataStartPos + reader.readUnsignedInt(size); + lastCuePoint.clusterPosition = this.currentSegment.dataStartPos + readUnsignedInt(slice, size); }; break; case EBMLId.Timestamp: { if (!this.currentCluster) break; - this.currentCluster.timestamp = reader.readUnsignedInt(size); + this.currentCluster.timestamp = readUnsignedInt(slice, size); }; break; case EBMLId.SimpleBlock: { if (!this.currentCluster) break; - const trackNumber = reader.readVarInt(); + const trackNumber = readVarInt(slice); if (trackNumber === null) break; - const relativeTimestamp = reader.readS16(); + const relativeTimestamp = readI16Be(slice); - const flags = reader.readU8(); + const flags = readU8(slice); const isKeyFrame = !!(flags & 0x80); const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later @@ -1204,7 +1222,7 @@ export class MatroskaDemuxer extends Demuxer { duration: 0, // Will set later isKeyFrame, referencedTimestamps: [], - data: reader.readBytes(size - (reader.pos - dataStartPos)), + data: readBytes(slice, size - (slice.filePos - dataStartPos)), lacing, }); }; break; @@ -1212,7 +1230,7 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.BlockGroup: { if (!this.currentCluster) break; - this.readContiguousElements(reader, size); + this.readContiguousElements(slice.slice(dataStartPos, size)); if (this.currentBlock) { for (let i = 0; i < this.currentBlock.referencedTimestamps.length; i++) { @@ -1226,12 +1244,12 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.Block: { if (!this.currentCluster) break; - const trackNumber = reader.readVarInt(); + const trackNumber = readVarInt(slice); if (trackNumber === null) break; - const relativeTimestamp = reader.readS16(); + const relativeTimestamp = readI16Be(slice); - const flags = reader.readU8(); + const flags = readU8(slice); const lacing = (flags >> 1) & 0x3 as BlockLacing; // If the block is laced, we'll expand it later const trackData = this.getTrackDataInCluster(this.currentCluster, trackNumber); @@ -1240,7 +1258,7 @@ export class MatroskaDemuxer extends Demuxer { duration: 0, // Will set later isKeyFrame: true, referencedTimestamps: [], - data: reader.readBytes(size - (reader.pos - dataStartPos)), + data: readBytes(slice, size - (slice.filePos - dataStartPos)), lacing, }; trackData.blocks.push(this.currentBlock); @@ -1249,7 +1267,7 @@ export class MatroskaDemuxer extends Demuxer { case EBMLId.BlockDuration: { if (!this.currentBlock) break; - this.currentBlock.duration = reader.readUnsignedInt(size); + this.currentBlock.duration = readUnsignedInt(slice, size); }; break; case EBMLId.ReferenceBlock: { @@ -1257,14 +1275,14 @@ export class MatroskaDemuxer extends Demuxer { this.currentBlock.isKeyFrame = false; - const relativeTimestamp = reader.readSignedInt(size); + const relativeTimestamp = readSignedInt(slice, size); // We'll offset this by the block's timestamp later this.currentBlock.referencedTimestamps.push(relativeTimestamp); }; break; } - reader.pos = dataStartPos + size; + slice.filePos = dataStartPos + size; return true; } } @@ -1618,10 +1636,6 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { return this.fetchPacketInCluster(cluster, blockIndex, options); } - // We use the metadata reader to find the cluster, but the cluster reader to load the cluster - const metadataReader = demuxer.metadataReader; - const clusterReader = demuxer.clusterReader; - let prevCluster: Cluster | null = null; let bestClusterIndex = clusterIndex; let bestBlockIndex = blockIndex; @@ -1635,24 +1649,25 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { ); const cuePoint = cuePointIndex !== -1 ? this.internalTrack.cuePoints[cuePointIndex]! : null; + let currentPos: number; let nextClusterIsFirstCluster = false; if (clusterIndex === -1) { - metadataReader.pos = cuePoint?.clusterPosition ?? segment.clusterSeekStartPos; - nextClusterIsFirstCluster = metadataReader.pos === segment.clusterSeekStartPos; + currentPos = cuePoint?.clusterPosition ?? segment.clusterSeekStartPos; + nextClusterIsFirstCluster = currentPos === segment.clusterSeekStartPos; } else { const cluster = this.internalTrack.clusters[clusterIndex]!; if (!cuePoint || cluster.elementStartPos >= cuePoint.clusterPosition) { - metadataReader.pos = cluster.elementEndPos; + currentPos = cluster.elementEndPos; prevCluster = cluster; } else { // Use the lookup entry - metadataReader.pos = cuePoint.clusterPosition; + currentPos = cuePoint.clusterPosition; } } - while (metadataReader.pos <= segment.elementEndPos - MIN_HEADER_SIZE) { + while (currentPos <= segment.elementEndPos - MIN_HEADER_SIZE) { if (prevCluster) { const trackData = prevCluster.trackData.get(this.internalTrack.id); if (trackData && trackData.startTimestamp > latestTimestamp) { @@ -1662,30 +1677,32 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { if (prevCluster.nextCluster) { // Skip ahead quickly without needing to read the file again - metadataReader.pos = prevCluster.nextCluster.elementEndPos; + currentPos = prevCluster.nextCluster.elementEndPos; prevCluster = prevCluster.nextCluster; continue; } } // Load the header - await metadataReader.reader.loadRange(metadataReader.pos, metadataReader.pos + MAX_HEADER_SIZE); - const elementStartPos = metadataReader.pos; - const elementHeader = metadataReader.readElementHeader(); + 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)) { // There's an element here that shouldn't be here (or Void). Might be garbage. In this case, let's // try and resync to the next valid element. - - metadataReader.pos = elementStartPos; - - const nextPos = await metadataReader.resync( + const nextPos = await resync( + demuxer.reader, + elementStartPos, LEVEL_1_EBML_IDS, - Math.min(segment.elementEndPos, metadataReader.pos + MAX_RESYNC_LENGTH), + Math.min(segment.elementEndPos, elementStartPos + MAX_RESYNC_LENGTH), ); if (nextPos) { - metadataReader.pos = nextPos; + currentPos = nextPos; continue; } else { break; // Resync failed @@ -1694,7 +1711,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { const id = elementHeader.id; let size = elementHeader.size; - const dataStartPos = metadataReader.pos; + const dataStartPos = slice.filePos; if (id === EBMLId.Cluster) { const index = binarySearchExact(segment.clusters, elementStartPos, x => x.elementStartPos); @@ -1702,8 +1719,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { let cluster: Cluster; if (index === -1) { // This is the first time we've seen this cluster - metadataReader.pos = elementStartPos; - cluster = await demuxer.readCluster(segment); + cluster = await demuxer.readCluster(elementStartPos, segment); } else { // We already know this cluster cluster = segment.clusters[index]!; @@ -1739,8 +1755,9 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { size = prevCluster.elementEndPos - dataStartPos; } else { // Search for the next element at level 0 or 1 - clusterReader.pos = dataStartPos; - const nextElementPos = await clusterReader.searchForNextElementId( + const nextElementPos = await searchForNextElementId( + demuxer.reader, + dataStartPos, LEVEL_0_AND_1_EBML_IDS, segment.elementEndPos, ); @@ -1756,8 +1773,12 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { // 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. - clusterReader.pos = endPos; - const elementId = clusterReader.readElementId(); + + 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; @@ -1765,7 +1786,7 @@ abstract class MatroskaTrackBacking implements InputTrackBacking { } } - metadataReader.pos = dataStartPos + size; + currentPos = dataStartPos + size; } const bestCluster = bestClusterIndex !== -1 ? this.internalTrack.clusters[bestClusterIndex]! : null; diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts index 012b91c..3809d40 100644 --- a/src/mp3/mp3-demuxer.ts +++ b/src/mp3/mp3-demuxer.ts @@ -14,7 +14,8 @@ import { PacketRetrievalOptions } from '../media-sink'; import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; import { FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc'; -import { Mp3Reader } from './mp3-reader'; +import { readId3, readNextFrameHeader } from './mp3-reader'; +import { readBytes, Reader2, readU32Be } from '../reader2'; type Sample = { timestamp: number; @@ -24,7 +25,7 @@ type Sample = { }; export class Mp3Demuxer extends Demuxer { - reader: Mp3Reader; + reader: Reader2; metadataPromise: Promise | null = null; firstFrameHeader: FrameHeader | null = null; @@ -33,6 +34,7 @@ export class Mp3Demuxer extends Demuxer { tracks: InputAudioTrack[] = []; readingMutex = new AsyncMutex(); + lastSampleLoaded = false; lastLoadedPos = 0; fileSize = 0; nextTimestampInSamples = 0; @@ -40,17 +42,18 @@ export class Mp3Demuxer extends Demuxer { constructor(input: Input) { super(input); - this.reader = new Mp3Reader(input._mainReader); + this.reader = input._reader2; } async readMetadata() { return this.metadataPromise ??= (async () => { - this.fileSize = await this.input.source.getSize(); - this.reader.fileSize = this.fileSize; + let fileSize = this.reader.requestSize(); + if (fileSize instanceof Promise) fileSize = await fileSize; + this.fileSize = fileSize; // Keep loading until we find the first frame header while (!this.firstFrameHeader && this.lastLoadedPos < this.fileSize) { - await this.loadNextChunk(); + await this.advanceReader(); } // There has to be a frame if this demuxer got selected @@ -60,72 +63,65 @@ export class Mp3Demuxer extends Demuxer { })(); } - /** Loads the next 0.5 MiB of frames. */ - async loadNextChunk() { - assert(this.lastLoadedPos < this.fileSize); + async advanceReader() { + if (this.lastLoadedPos === 0) { + let slice = this.reader.requestSlice(0, 10); + if (slice instanceof Promise) slice = await slice; - const chunkSize = 0.5 * 1024 * 1024; // 0.5 MiB - const endPos = Math.min(this.lastLoadedPos + chunkSize, this.fileSize); - await this.reader.reader.loadRange(this.lastLoadedPos, endPos); + if (!slice) { + this.lastSampleLoaded = true; + return; + } - this.lastLoadedPos = endPos; - assert(this.lastLoadedPos <= this.fileSize); - - if (this.reader.pos === 0) { // First time, let's see if there's an ID3 tag - const id3Tag = this.reader.readId3(); + const id3Tag = readId3(slice); if (id3Tag) { - this.reader.pos += id3Tag.size; + this.lastLoadedPos += 10 + id3Tag.size; } } - this.parseFramesFromLoadedData(); - } + const startPos = this.lastLoadedPos; - private parseFramesFromLoadedData() { - while (true) { - const startPos = this.reader.pos; - const header = this.reader.readNextFrameHeader(); - if (!header) { - break; - } - - // Check if the entire frame fits in the loaded data - if (header.startPos + header.totalSize > this.lastLoadedPos) { - // Frame doesn't fit, reset positions and stop - this.reader.pos = startPos; - this.lastLoadedPos = startPos; // Snap this back too so that the next read is frame-aligned - - break; - } - - const xingOffset = getXingOffset(header.mpegVersionId, header.channel); - this.reader.pos = header.startPos + xingOffset; - const word = this.reader.readU32(); - const isXing = word === XING || word === INFO; - - this.reader.pos = header.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short - - if (isXing) { - // There's no actual audio data in this frame, so let's skip it - continue; - } - - if (!this.firstFrameHeader) { - this.firstFrameHeader = header; - } - - const sampleDuration = header.audioSamplesInFrame / header.sampleRate; - const sample: Sample = { - timestamp: this.nextTimestampInSamples / header.sampleRate, - duration: sampleDuration, - dataStart: header.startPos, - dataSize: header.totalSize, - }; - - this.loadedSamples.push(sample); - this.nextTimestampInSamples += header.audioSamplesInFrame; + const result = await readNextFrameHeader(this.reader, startPos, this.fileSize); + if (!result) { + this.lastSampleLoaded = true; + return; } + + const header = result.header; + + this.lastLoadedPos = result.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short + + const xingOffset = getXingOffset(header.mpegVersionId, header.channel); + + let slice = this.reader.requestSlice(startPos + xingOffset, 4); + if (slice instanceof Promise) slice = await slice; + assert(slice); + + const word = readU32Be(slice); + const isXing = word === XING || word === INFO; + + if (isXing) { + // There's no actual audio data in this frame, so let's skip it + return; + } + + if (!this.firstFrameHeader) { + this.firstFrameHeader = header; + } + + const sampleDuration = header.audioSamplesInFrame / header.sampleRate; + const sample: Sample = { + timestamp: this.nextTimestampInSamples / header.sampleRate, + duration: sampleDuration, + dataStart: startPos, + dataSize: header.totalSize, + }; + + this.loadedSamples.push(sample); + this.nextTimestampInSamples += header.audioSamplesInFrame; + + return; } async getMimeType() { @@ -204,7 +200,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { }; } - getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) { + async getPacketAtIndex(sampleIndex: number, options: PacketRetrievalOptions) { if (sampleIndex === -1) { return null; } @@ -218,8 +214,11 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { if (options.metadataOnly) { data = PLACEHOLDER_DATA; } else { - this.demuxer.reader.pos = rawSample.dataStart; - data = this.demuxer.reader.readBytes(rawSample.dataSize); + let slice = this.demuxer.reader.requestSlice(rawSample.dataStart, rawSample.dataSize); + if (slice instanceof Promise) slice = await slice; + assert(slice); + + data = readBytes(slice, rawSample.dataSize); } return new EncodedPacket( @@ -232,7 +231,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { ); } - async getFirstPacket(options: PacketRetrievalOptions) { + getFirstPacket(options: PacketRetrievalOptions) { return this.getPacketAtIndex(0, options); } @@ -246,6 +245,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { x => x.timestamp, ); if (sampleIndex === -1) { + console.log('uh', packet, this.demuxer.loadedSamples); throw new Error('Packet was not created from this track.'); } @@ -253,9 +253,9 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { // Ensure the next sample exists while ( nextIndex >= this.demuxer.loadedSamples.length - && this.demuxer.lastLoadedPos < this.demuxer.fileSize + && !this.demuxer.lastSampleLoaded ) { - await this.demuxer.loadNextChunk(); + await this.demuxer.advanceReader(); } return this.getPacketAtIndex(nextIndex, options); @@ -266,6 +266,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { async getPacket(timestamp: number, options: PacketRetrievalOptions) { const release = await this.demuxer.readingMutex.acquire(); + try { while (true) { const index = binarySearchLessOrEqual( @@ -279,7 +280,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { return null; } - if (this.demuxer.lastLoadedPos === this.demuxer.fileSize) { + if (this.demuxer.lastSampleLoaded) { // All data is loaded, return what we found return this.getPacketAtIndex(index, options); } @@ -290,7 +291,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking { } // Otherwise, keep loading data - await this.demuxer.loadNextChunk(); + await this.demuxer.advanceReader(); } } finally { release(); diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts index 9a9627d..40bff2d 100644 --- a/src/mp3/mp3-reader.ts +++ b/src/mp3/mp3-reader.ts @@ -6,78 +6,48 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { assert } from '../misc'; -import { Reader } from '../reader'; import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from '../../shared/mp3-misc'; +import { FileSlice, readAscii, Reader2, readU32Be } from '../reader2'; -export class Mp3Reader { - pos = 0; - fileSize: number | null = null; - - constructor(public reader: Reader) {} - - readBytes(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - return new Uint8Array(view.buffer, offset, length); - } - - readU16() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 2); - this.pos += 2; - - return view.getUint16(offset, false); - } - - readU32() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); - this.pos += 4; - - return view.getUint32(offset, false); - } - - readAscii(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - let str = ''; - for (let i = 0; i < length; i++) { - str += String.fromCharCode(view.getUint8(offset + i)); - } - return str; - } - - readId3() { - const tag = this.readAscii(3); - if (tag !== 'ID3') { - this.pos -= 3; - return null; - } - - this.pos += 3; - - const size = decodeSynchsafe(this.readU32()); - return { size }; - } - - readNextFrameHeader(until?: number): FrameHeader | null { - assert(this.fileSize); - until ??= this.fileSize; - - while (this.pos <= until - FRAME_HEADER_SIZE) { - const word = this.readU32(); - this.pos -= 4; - - const header = readFrameHeader(word, this); - if (header) { - return header; - } - } - +export const readId3 = (slice: FileSlice) => { + const tag = readAscii(slice, 3); + if (tag !== 'ID3') { + slice.skip(-3); return null; } -} + + slice.skip(3); + + const size = decodeSynchsafe(readU32Be(slice)); + return { size }; +}; + +export const readNextFrameHeader = async (reader: Reader2, startPos: number, until: number): Promise<{ + header: FrameHeader; + startPos: number; +} | null> => { + let fileSize = reader.requestSize(); + if (fileSize instanceof Promise) fileSize = await fileSize; + + let currentPos = startPos; + + while (currentPos < until) { + let slice = reader.requestSlice(currentPos, FRAME_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; + + const word = readU32Be(slice); + + const result = readFrameHeader(word, fileSize - currentPos); + if (result.header) { + return { header: result.header, startPos: currentPos }; + } + + currentPos += result.bytesAdvanced; + } + + return null; +}; export const decodeSynchsafe = (synchsafed: number) => { let mask = 0x7f000000; diff --git a/src/ogg/ogg-demuxer.ts b/src/ogg/ogg-demuxer.ts index 39a5ed3..3ba43f3 100644 --- a/src/ogg/ogg-demuxer.ts +++ b/src/ogg/ogg-demuxer.ts @@ -12,11 +12,18 @@ import { Demuxer } from '../demuxer'; import { Input } from '../input'; import { InputAudioTrack, InputAudioTrackBacking } from '../input-track'; import { PacketRetrievalOptions } from '../media-sink'; -import { assert, AsyncMutex, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc'; +import { assert, findLast, roundToPrecision, toDataView, UNDETERMINED_LANGUAGE } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; -import { Reader } from '../reader'; +import { readBytes, Reader2 } from '../reader2'; import { buildOggMimeType, computeOggPageCrc, extractSampleMetadata, OggCodecInfo } from './ogg-misc'; -import { MAX_PAGE_HEADER_SIZE, MAX_PAGE_SIZE, MIN_PAGE_HEADER_SIZE, OggReader, Page } from './ogg-reader'; +import { + findNextPageHeader, + MAX_PAGE_HEADER_SIZE, + MAX_PAGE_SIZE, + MIN_PAGE_HEADER_SIZE, + Page, + readPageHeader, +} from './ogg-reader'; type LogicalBitstream = { serialNumber: number; @@ -36,12 +43,7 @@ type Packet = { }; export class OggDemuxer extends Demuxer { - reader: OggReader; - /** - * Lots of reading operations require multiple async reads and thus need to be mutually exclusive to avoid - * conflicts in reader position. - */ - readingMutex = new AsyncMutex(); + reader: Reader2; metadataPromise: Promise | null = null; fileSize: number | null = null; @@ -51,21 +53,22 @@ export class OggDemuxer extends Demuxer { constructor(input: Input) { super(input); - // We don't need a persistent metadata reader as we read all metadata once at the start and then never again - this.reader = new OggReader(new Reader(input.source, 64 * 2 ** 20)); + this.reader = input._reader2; } async readMetadata() { return this.metadataPromise ??= (async () => { - this.fileSize = await this.input.source.getSize(); + let fileSize = this.reader.requestSize(); + if (fileSize instanceof Promise) fileSize = await fileSize; + this.fileSize = fileSize; - while (this.reader.pos < this.fileSize - MIN_PAGE_HEADER_SIZE) { - await this.reader.reader.loadRange( - this.reader.pos, - this.reader.pos + MAX_PAGE_HEADER_SIZE, - ); + let currentPos = 0; + while (currentPos <= this.fileSize - MIN_PAGE_HEADER_SIZE) { + let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; - const page = this.reader.readPageHeader(); + const page = readPageHeader(slice); if (!page) { break; } @@ -91,11 +94,11 @@ export class OggDemuxer extends Demuxer { lastMetadataPacket: null, }); - this.reader.pos = page.headerStartPos + page.totalSize; + currentPos = page.headerStartPos + page.totalSize; } for (const bitstream of this.bitstreams) { - const firstPacket = await this.readPacket(this.reader, bitstream.bosPage, 0); + const firstPacket = await this.readPacket(bitstream.bosPage, 0); if (!firstPacket) { continue; } @@ -135,30 +138,22 @@ export class OggDemuxer extends Demuxer { } async readVorbisMetadata(firstPacket: Packet, bitstream: LogicalBitstream) { - let nextPacketPosition = await this.findNextPacketStart(this.reader, firstPacket); + let nextPacketPosition = await this.findNextPacketStart(firstPacket); if (!nextPacketPosition) { return; } - const secondPacket = await this.readPacket( - this.reader, - nextPacketPosition.startPage, - nextPacketPosition.startSegmentIndex, - ); + const secondPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex); if (!secondPacket) { return; } - nextPacketPosition = await this.findNextPacketStart(this.reader, secondPacket); + nextPacketPosition = await this.findNextPacketStart(secondPacket); if (!nextPacketPosition) { return; } - const thirdPacket = await this.readPacket( - this.reader, - nextPacketPosition.startPage, - nextPacketPosition.startSegmentIndex, - ); + const thirdPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex); if (!thirdPacket) { return; } @@ -224,13 +219,12 @@ export class OggDemuxer extends Demuxer { // From https://datatracker.ietf.org/doc/html/rfc7845#section-5: // "An Ogg Opus logical stream contains exactly two mandatory header packets: an identification header and a // comment header." - const nextPacketPosition = await this.findNextPacketStart(this.reader, firstPacket); + const nextPacketPosition = await this.findNextPacketStart(firstPacket); if (!nextPacketPosition) { return; } const secondPacket = await this.readPacket( - this.reader, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex, ); @@ -253,7 +247,7 @@ export class OggDemuxer extends Demuxer { }; } - async readPacket(reader: OggReader, startPage: Page, startSegmentIndex: number): Promise { + async readPacket(startPage: Page, startSegmentIndex: number): Promise { assert(startSegmentIndex < startPage.lacingValues.length); assert(this.fileSize); @@ -271,12 +265,10 @@ export class OggDemuxer extends Demuxer { outer: while (true) { // Load the entire page data - await reader.reader.loadRange( - currentPage.dataStartPos, - currentPage.dataStartPos + currentPage.dataSize, - ); - reader.pos = currentPage.dataStartPos; - const pageData = reader.readBytes(currentPage.dataSize); + let pageSlice = this.reader.requestSlice(currentPage.dataStartPos, currentPage.dataSize); + if (pageSlice instanceof Promise) pageSlice = await pageSlice; + assert(pageSlice); + const pageData = readBytes(pageSlice, currentPage.dataSize); while (true) { if (currentSegmentIndex === currentPage.lacingValues.length) { @@ -296,14 +288,19 @@ export class OggDemuxer extends Demuxer { } // The packet extends to the next page; let's find it + let currentPos = currentPage.headerStartPos + currentPage.totalSize; while (true) { - reader.pos = currentPage.headerStartPos + currentPage.totalSize; - if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) { + if (currentPos > this.fileSize - MIN_PAGE_HEADER_SIZE) { return null; } - await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); - const nextPage = reader.readPageHeader(); + let headerSlice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE); + if (headerSlice instanceof Promise) headerSlice = await headerSlice; + if (!headerSlice) { + return null; + } + + const nextPage = readPageHeader(headerSlice); if (!nextPage) { return null; } @@ -312,6 +309,7 @@ export class OggDemuxer extends Demuxer { if (currentPage.serialNumber === startPage.serialNumber) { break; } + currentPos = currentPage.headerStartPos + currentPage.totalSize; } startDataOffset = 0; @@ -336,7 +334,7 @@ export class OggDemuxer extends Demuxer { }; } - async findNextPacketStart(reader: OggReader, lastPacket: Packet) { + async findNextPacketStart(lastPacket: Packet) { assert(this.fileSize !== null); // If there's another segment in the same page, return it @@ -351,14 +349,19 @@ export class OggDemuxer extends Demuxer { } // Otherwise, search for the next page belonging to the same bitstream - reader.pos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize; + let currentPos = lastPacket.endPage.headerStartPos + lastPacket.endPage.totalSize; while (true) { - if (reader.pos >= this.fileSize - MIN_PAGE_HEADER_SIZE) { + if (currentPos >= this.fileSize - MIN_PAGE_HEADER_SIZE) { return null; } - await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); - const nextPage = reader.readPageHeader(); + let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + if (!slice) { + return null; + } + + const nextPage = readPageHeader(slice); if (!nextPage) { return null; } @@ -367,7 +370,7 @@ export class OggDemuxer extends Demuxer { return { startPage: nextPage, startSegmentIndex: 0 }; } - reader.pos = nextPage.headerStartPos + nextPage.totalSize; + currentPos = nextPage.headerStartPos + nextPage.totalSize; } } @@ -508,409 +511,388 @@ class OggAudioTrackBacking implements InputAudioTrackBacking { return encodedPacket; } - async getFirstPacket(options: PacketRetrievalOptions, exclusive = true) { - const release = exclusive ? await this.demuxer.readingMutex.acquire() : null; - - try { - assert(this.bitstream.lastMetadataPacket); - const packetPosition = await this.demuxer.findNextPacketStart( - this.demuxer.reader, - this.bitstream.lastMetadataPacket, - ); - if (!packetPosition) { - return null; - } - - let timestampInSamples = 0; - if (this.bitstream.codecInfo.codec === 'opus') { - assert(this.bitstream.codecInfo.opusInfo); - timestampInSamples -= this.bitstream.codecInfo.opusInfo.preSkip; - } - - const packet = await this.demuxer.readPacket( - this.demuxer.reader, - packetPosition.startPage, - packetPosition.startSegmentIndex, - ); - - return this.createEncodedPacketFromOggPacket( - packet, - { - timestampInSamples, - vorbisLastBlocksize: null, - }, - options, - ); - } finally { - release?.(); + async getFirstPacket(options: PacketRetrievalOptions) { + assert(this.bitstream.lastMetadataPacket); + const packetPosition = await this.demuxer.findNextPacketStart(this.bitstream.lastMetadataPacket); + if (!packetPosition) { + return null; } + + let timestampInSamples = 0; + if (this.bitstream.codecInfo.codec === 'opus') { + assert(this.bitstream.codecInfo.opusInfo); + timestampInSamples -= this.bitstream.codecInfo.opusInfo.preSkip; + } + + const packet = await this.demuxer.readPacket(packetPosition.startPage, packetPosition.startSegmentIndex); + + return this.createEncodedPacketFromOggPacket( + packet, + { + timestampInSamples, + vorbisLastBlocksize: null, + }, + options, + ); } async getNextPacket(prevPacket: EncodedPacket, options: PacketRetrievalOptions) { - const release = await this.demuxer.readingMutex.acquire(); - - try { - const prevMetadata = this.encodedPacketToMetadata.get(prevPacket); - if (!prevMetadata) { - throw new Error('Packet was not created from this track.'); - } - - const packetPosition = await this.demuxer.findNextPacketStart(this.demuxer.reader, prevMetadata.packet); - if (!packetPosition) { - return null; - } - - const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples; - - const packet = await this.demuxer.readPacket( - this.demuxer.reader, - packetPosition.startPage, - packetPosition.startSegmentIndex, - ); - - return this.createEncodedPacketFromOggPacket( - packet, - { - timestampInSamples, - vorbisLastBlocksize: prevMetadata.vorbisBlockSize, - }, - options, - ); - } finally { - release(); + const prevMetadata = this.encodedPacketToMetadata.get(prevPacket); + if (!prevMetadata) { + throw new Error('Packet was not created from this track.'); } + + const packetPosition = await this.demuxer.findNextPacketStart(prevMetadata.packet); + if (!packetPosition) { + return null; + } + + const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples; + + const packet = await this.demuxer.readPacket( + packetPosition.startPage, + packetPosition.startSegmentIndex, + ); + + return this.createEncodedPacketFromOggPacket( + packet, + { + timestampInSamples, + vorbisLastBlocksize: prevMetadata.vorbisBlockSize, + }, + options, + ); } async getPacket(timestamp: number, options: PacketRetrievalOptions) { - const release = await this.demuxer.readingMutex.acquire(); + assert(this.demuxer.fileSize !== null); - try { - assert(this.demuxer.fileSize !== null); + const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14); + if (timestampInSamples === 0) { + // Fast path for timestamp 0 - avoids binary search when playing back from the start + return this.getFirstPacket(options); + } + if (timestampInSamples < 0) { + // There's nothing here + return null; + } - const timestampInSamples = roundToPrecision(timestamp * this.internalSampleRate, 14); - if (timestampInSamples === 0) { - // Fast path for timestamp 0 - avoids binary search when playing back from the start - return this.getFirstPacket(options, false); - } - if (timestampInSamples < 0) { - // There's nothing here - return null; - } + assert(this.bitstream.lastMetadataPacket); + const startPosition = await this.demuxer.findNextPacketStart(this.bitstream.lastMetadataPacket); + if (!startPosition) { + return null; + } - const reader = this.demuxer.reader; + let lowPage = startPosition.startPage; + let high = this.demuxer.fileSize; - assert(this.bitstream.lastMetadataPacket); - const startPosition = await this.demuxer.findNextPacketStart( - reader, - this.bitstream.lastMetadataPacket, - ); - if (!startPosition) { - return null; - } + const lowPages: Page[] = [lowPage]; - let lowPage = startPosition.startPage; - let high = this.demuxer.fileSize; + // First, let's perform a binary serach (bisection search) on the file to find the approximate page where + // we'll find the packet. We want to find a page whose end packet position is less than or equal to the + // packet position we're searching for. - const lowPages: Page[] = [lowPage]; + // Outer loop: Does the binary serach + outer: + while (lowPage.headerStartPos + lowPage.totalSize < high) { + const low = lowPage.headerStartPos; + const mid = Math.floor((low + high) / 2); - // First, let's perform a binary serach (bisection search) on the file to find the approximate page where - // we'll find the packet. We want to find a page whose end packet position is less than or equal to the - // packet position we're searching for. + let searchStartPos = mid; - // Outer loop: Does the binary serach - outer: - while (lowPage.headerStartPos + lowPage.totalSize < high) { - const low = lowPage.headerStartPos; - const mid = Math.floor((low + high) / 2); + // Inner loop: Does a linear forward scan if the page cannot be found immediately + while (true) { + const until = Math.min( + searchStartPos + MAX_PAGE_SIZE, + high - MIN_PAGE_HEADER_SIZE, + ); - let searchStartPos = mid; - - // Inner loop: Does a linear forward scan if the page cannot be found immediately - while (true) { - const until = Math.min( - searchStartPos + MAX_PAGE_SIZE, - high - MIN_PAGE_HEADER_SIZE, - ); - - await reader.reader.loadRange(searchStartPos, until); - - reader.pos = searchStartPos; - const found = reader.findNextPageHeader(until); - - if (!found) { - high = mid + MIN_PAGE_HEADER_SIZE; - continue outer; - } - - await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); - const page = reader.readPageHeader(); - assert(page); - - let pageValid = false; - if (page.serialNumber === this.bitstream.serialNumber) { - // Serial numbers are basically random numbers, and the chance of finding a fake page with - // matching serial number is astronomically low, so we can be pretty sure this page is legit. - pageValid = true; - } else { - await reader.reader.loadRange(page.headerStartPos, page.headerStartPos + page.totalSize); - - // Validate the page by checking checksum - reader.pos = page.headerStartPos; - const bytes = reader.readBytes(page.totalSize); - const crc = computeOggPageCrc(bytes); - - pageValid = crc === page.checksum; - } - - if (!pageValid) { - // Keep searching for a valid page - searchStartPos = page.headerStartPos + 4; // 'OggS' is 4 bytes - continue; - } - - if (pageValid && page.serialNumber !== this.bitstream.serialNumber) { - // Page is valid but from a different bitstream, so keep searching forward until we find one - // belonging to the our bitstream - searchStartPos = page.headerStartPos + page.totalSize; - continue; - } - - const isContinuationPage = page.granulePosition === -1; - if (isContinuationPage) { - // No packet ends on this page - keep looking - searchStartPos = page.headerStartPos + page.totalSize; - continue; - } - - // The page is valid and belongs to our bitstream; let's check its granule position to see where we - // need to take the bisection search. - if (this.granulePositionToTimestampInSamples(page.granulePosition) > timestampInSamples) { - high = page.headerStartPos; - } else { - lowPage = page; - lowPages.push(page); - } + let searchSlice = this.demuxer.reader.requestSlice(searchStartPos, until - searchStartPos); + if (searchSlice instanceof Promise) searchSlice = await searchSlice; + assert(searchSlice); + const found = findNextPageHeader(searchSlice, until); + if (!found) { + high = mid + MIN_PAGE_HEADER_SIZE; continue outer; } + + let headerSlice = this.demuxer.reader.requestSliceRange( + searchSlice.filePos, + MIN_PAGE_HEADER_SIZE, + MAX_PAGE_HEADER_SIZE, + ); + if (headerSlice instanceof Promise) headerSlice = await headerSlice; + assert(headerSlice); + + const page = readPageHeader(headerSlice); + assert(page); + + let pageValid = false; + if (page.serialNumber === this.bitstream.serialNumber) { + // Serial numbers are basically random numbers, and the chance of finding a fake page with + // matching serial number is astronomically low, so we can be pretty sure this page is legit. + pageValid = true; + } else { + let pageSlice = this.demuxer.reader.requestSlice(page.headerStartPos, page.totalSize); + if (pageSlice instanceof Promise) pageSlice = await pageSlice; + assert(pageSlice); + + // Validate the page by checking checksum + const bytes = readBytes(pageSlice, page.totalSize); + const crc = computeOggPageCrc(bytes); + + pageValid = crc === page.checksum; + } + + if (!pageValid) { + // Keep searching for a valid page + searchStartPos = page.headerStartPos + 4; // 'OggS' is 4 bytes + continue; + } + + if (pageValid && page.serialNumber !== this.bitstream.serialNumber) { + // Page is valid but from a different bitstream, so keep searching forward until we find one + // belonging to the our bitstream + searchStartPos = page.headerStartPos + page.totalSize; + continue; + } + + const isContinuationPage = page.granulePosition === -1; + if (isContinuationPage) { + // No packet ends on this page - keep looking + searchStartPos = page.headerStartPos + page.totalSize; + continue; + } + + // The page is valid and belongs to our bitstream; let's check its granule position to see where we + // need to take the bisection search. + if (this.granulePositionToTimestampInSamples(page.granulePosition) > timestampInSamples) { + high = page.headerStartPos; + } else { + lowPage = page; + lowPages.push(page); + } + + continue outer; + } + } + + // Now we have the last page with a packet position <= the packet position we're looking for, but there + // might be multiple pages with the packet position, in which case we actually need to find the first of + // such pages. We'll do this in two steps: First, let's find the latest page we know with an earlier packet + // position, and then linear scan ourselves forward until we find the correct page. + + let lowerPage = startPosition.startPage; + for (const otherLowPage of lowPages) { + if (otherLowPage.granulePosition === lowPage.granulePosition) { + break; } - // Now we have the last page with a packet position <= the packet position we're looking for, but there - // might be multiple pages with the packet position, in which case we actually need to find the first of - // such pages. We'll do this in two steps: First, let's find the latest page we know with an earlier packet - // position, and then linear scan ourselves forward until we find the correct page. + if (!lowerPage || otherLowPage.headerStartPos > lowerPage.headerStartPos) { + lowerPage = otherLowPage; + } + } - let lowerPage = startPosition.startPage; - for (const otherLowPage of lowPages) { - if (otherLowPage.granulePosition === lowPage.granulePosition) { + let currentPage = lowerPage; + // Keep track of the pages we traversed, we need these later for backwards seeking + const previousPages: Page[] = [currentPage]; + + while (true) { + // This loop must terminate as we'll eventually reach lowPage + if ( + currentPage.serialNumber === this.bitstream.serialNumber + && currentPage.granulePosition === lowPage.granulePosition + ) { + break; + } + + const nextPos = currentPage.headerStartPos + currentPage.totalSize; + let slice = this.demuxer.reader.requestSliceRange(nextPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE); + if (slice instanceof Promise) slice = await slice; + assert(slice); + + const nextPage = readPageHeader(slice); + assert(nextPage); + + currentPage = nextPage; + + if (currentPage.serialNumber === this.bitstream.serialNumber) { + previousPages.push(currentPage); + } + } + + assert(currentPage.granulePosition !== -1); + + let currentSegmentIndex: number | null = null; + let currentTimestampInSamples: number; + let currentTimestampIsCorrect: boolean; + + // These indicate the end position of the packet that the granule position belongs to + let endPage = currentPage; + let endSegmentIndex = 0; + + if (currentPage.headerStartPos === startPosition.startPage.headerStartPos) { + currentTimestampInSamples = this.granulePositionToTimestampInSamples(0); + currentTimestampIsCorrect = true; + currentSegmentIndex = 0; + } else { + currentTimestampInSamples = 0; // Placeholder value! We'll refine it once we can + currentTimestampIsCorrect = false; + + // Find the segment index of the next packet + for (let i = currentPage.lacingValues.length - 1; i >= 0; i--) { + const value = currentPage.lacingValues[i]!; + if (value < 255) { + // We know the last packet ended at i, so the next one starts at i + 1 + currentSegmentIndex = i + 1; break; } - - if (!lowerPage || otherLowPage.headerStartPos > lowerPage.headerStartPos) { - lowerPage = otherLowPage; - } } - let currentPage: Page | null = lowerPage; - // Keep track of the pages we traversed, we need these later for backwards seeking - const previousPages: Page[] = [currentPage]; - - while (true) { - // This loop must terminate as we'll eventually reach lowPage - if ( - currentPage.serialNumber === this.bitstream.serialNumber - && currentPage.granulePosition === lowPage.granulePosition - ) { - break; - } - - reader.pos = currentPage.headerStartPos + currentPage.totalSize; - await reader.reader.loadRange(reader.pos, reader.pos + MAX_PAGE_HEADER_SIZE); - - const nextPage = reader.readPageHeader(); - assert(nextPage); - - currentPage = nextPage; - - if (currentPage.serialNumber === this.bitstream.serialNumber) { - previousPages.push(currentPage); - } + // This must hold: Since this page has a granule position set, that means there must be a packet that + // ends in this page. + if (currentSegmentIndex === null) { + throw new Error('Invalid page with granule position: no packets end on this page.'); } - assert(currentPage.granulePosition !== -1); + endSegmentIndex = currentSegmentIndex - 1; + const pseudopacket: Packet = { + data: PLACEHOLDER_DATA, + endPage, + endSegmentIndex, + }; + const nextPosition = await this.demuxer.findNextPacketStart(pseudopacket); - let currentSegmentIndex: number | null = null; - let currentTimestampInSamples: number; - let currentTimestampIsCorrect: boolean; + if (nextPosition) { + // Let's rewind a single step (packet) - this previous packet ensures that we'll correctly compute + // the duration for the packet we're looking for. + const endPosition = findPreviousPacketEndPosition(previousPages, currentPage, currentSegmentIndex); + assert(endPosition); - // These indicate the end position of the packet that the granule position belongs to - let endPage = currentPage; - let endSegmentIndex = 0; - - if (currentPage.headerStartPos === startPosition.startPage.headerStartPos) { - currentTimestampInSamples = this.granulePositionToTimestampInSamples(0); - currentTimestampIsCorrect = true; - currentSegmentIndex = 0; + const startPosition = findPacketStartPosition( + previousPages, endPosition.page, endPosition.segmentIndex, + ); + if (startPosition) { + currentPage = startPosition.page; + currentSegmentIndex = startPosition.segmentIndex; + } } else { - currentTimestampInSamples = 0; // Placeholder value! We'll refine it once we can - currentTimestampIsCorrect = false; - - // Find the segment index of the next packet - for (let i = currentPage.lacingValues.length - 1; i >= 0; i--) { - const value = currentPage.lacingValues[i]!; - if (value < 255) { - // We know the last packet ended at i, so the next one starts at i + 1 - currentSegmentIndex = i + 1; + // There is no next position, which means we're looking for the last packet in the bitstream. The + // granule position on the last page tends to be fucky, so let's instead start the search on the + // page before that. So let's loop until we find a packet that ends in a previous page. + while (true) { + const endPosition = findPreviousPacketEndPosition( + previousPages, currentPage, currentSegmentIndex, + ); + if (!endPosition) { break; } - } - - // This must hold: Since this page has a granule position set, that means there must be a packet that - // ends in this page. - if (currentSegmentIndex === null) { - throw new Error('Invalid page with granule position: no packets end on this page.'); - } - - endSegmentIndex = currentSegmentIndex - 1; - const pseudopacket: Packet = { - data: PLACEHOLDER_DATA, - endPage, - endSegmentIndex, - }; - const nextPosition = await this.demuxer.findNextPacketStart(reader, pseudopacket); - - if (nextPosition) { - // Let's rewind a single step (packet) - this previous packet ensures that we'll correctly compute - // the duration for the packet we're looking for. - const endPosition = findPreviousPacketEndPosition(previousPages, currentPage, currentSegmentIndex); - assert(endPosition); const startPosition = findPacketStartPosition( previousPages, endPosition.page, endPosition.segmentIndex, ); - if (startPosition) { - currentPage = startPosition.page; - currentSegmentIndex = startPosition.segmentIndex; + if (!startPosition) { + break; } - } else { - // There is no next position, which means we're looking for the last packet in the bitstream. The - // granule position on the last page tends to be fucky, so let's instead start the search on the - // page before that. So let's loop until we find a packet that ends in a previous page. - while (true) { - const endPosition = findPreviousPacketEndPosition( - previousPages, currentPage, currentSegmentIndex, - ); - if (!endPosition) { - break; - } - const startPosition = findPacketStartPosition( - previousPages, endPosition.page, endPosition.segmentIndex, - ); - if (!startPosition) { - break; - } + currentPage = startPosition.page; + currentSegmentIndex = startPosition.segmentIndex; - currentPage = startPosition.page; - currentSegmentIndex = startPosition.segmentIndex; - - if (endPosition.page.headerStartPos !== endPage.headerStartPos) { - endPage = endPosition.page; - endSegmentIndex = endPosition.segmentIndex; - break; - } + if (endPosition.page.headerStartPos !== endPage.headerStartPos) { + endPage = endPosition.page; + endSegmentIndex = endPosition.segmentIndex; + break; } } } + } - let lastEncodedPacket: EncodedPacket | null = null; - let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null; + let lastEncodedPacket: EncodedPacket | null = null; + let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null; - // Alright, now it's time for the final, granular seek: We keep iterating over packets until we've found the - // one with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for. - while (currentPage !== null) { - assert(currentSegmentIndex !== null); + // Alright, now it's time for the final, granular seek: We keep iterating over packets until we've found the + // one with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for. + while (currentPage !== null) { + assert(currentSegmentIndex !== null); - const packet = await this.demuxer.readPacket(reader, currentPage, currentSegmentIndex); - if (!packet) { - break; - } + const packet = await this.demuxer.readPacket(currentPage, currentSegmentIndex); + if (!packet) { + break; + } - // We might need to skip the packet if it's a metadata one - const skipPacket = currentPage.headerStartPos === startPosition.startPage.headerStartPos - && currentSegmentIndex < startPosition.startSegmentIndex; + // We might need to skip the packet if it's a metadata one + const skipPacket = currentPage.headerStartPos === startPosition.startPage.headerStartPos + && currentSegmentIndex < startPosition.startSegmentIndex; - if (!skipPacket) { - let encodedPacket = this.createEncodedPacketFromOggPacket( + if (!skipPacket) { + let encodedPacket = this.createEncodedPacketFromOggPacket( + packet, + { + timestampInSamples: currentTimestampInSamples, + vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null, + }, + options, + ); + assert(encodedPacket); + + let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); + assert(encodedPacketMetadata); + + if ( + !currentTimestampIsCorrect + && packet.endPage.headerStartPos === endPage.headerStartPos + && packet.endSegmentIndex === endSegmentIndex + ) { + // We know this packet end timestamp can be derived from the page's granule position + currentTimestampInSamples = this.granulePositionToTimestampInSamples( + currentPage.granulePosition, + ); + currentTimestampIsCorrect = true; + + // Let's backpatch the packet we just created with the correct timestamp + encodedPacket = this.createEncodedPacketFromOggPacket( packet, { - timestampInSamples: currentTimestampInSamples, + timestampInSamples: currentTimestampInSamples - encodedPacketMetadata.durationInSamples, vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null, }, options, ); assert(encodedPacket); - let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); + encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); assert(encodedPacketMetadata); - - if ( - !currentTimestampIsCorrect - && packet.endPage.headerStartPos === endPage.headerStartPos - && packet.endSegmentIndex === endSegmentIndex - ) { - // We know this packet end timestamp can be derived from the page's granule position - currentTimestampInSamples = this.granulePositionToTimestampInSamples( - currentPage.granulePosition, - ); - currentTimestampIsCorrect = true; - - // Let's backpatch the packet we just created with the correct timestamp - encodedPacket = this.createEncodedPacketFromOggPacket( - packet, - { - timestampInSamples: currentTimestampInSamples - encodedPacketMetadata.durationInSamples, - vorbisLastBlocksize: lastEncodedPacketMetadata?.vorbisBlockSize ?? null, - }, - options, - ); - assert(encodedPacket); - - encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket); - assert(encodedPacketMetadata); - } else { - currentTimestampInSamples += encodedPacketMetadata.durationInSamples; - } - - lastEncodedPacket = encodedPacket; - lastEncodedPacketMetadata = encodedPacketMetadata; - - if ( - currentTimestampIsCorrect - && ( - // Next timestamp will be too late - Math.max(currentTimestampInSamples, 0) > timestampInSamples - // This timestamp already matches - || Math.max(encodedPacketMetadata.timestampInSamples, 0) === timestampInSamples - ) - ) { - break; - } + } else { + currentTimestampInSamples += encodedPacketMetadata.durationInSamples; } - const nextPosition = await this.demuxer.findNextPacketStart(reader, packet); - if (!nextPosition) { + lastEncodedPacket = encodedPacket; + lastEncodedPacketMetadata = encodedPacketMetadata; + + if ( + currentTimestampIsCorrect + && ( + // Next timestamp will be too late + Math.max(currentTimestampInSamples, 0) > timestampInSamples + // This timestamp already matches + || Math.max(encodedPacketMetadata.timestampInSamples, 0) === timestampInSamples + ) + ) { break; } - - currentPage = nextPosition.startPage; - currentSegmentIndex = nextPosition.startSegmentIndex; } - return lastEncodedPacket; - } finally { - release(); + const nextPosition = await this.demuxer.findNextPacketStart(packet); + if (!nextPosition) { + break; + } + + currentPage = nextPosition.startPage; + currentSegmentIndex = nextPosition.startSegmentIndex; } + + return lastEncodedPacket; } getKeyPacket(timestamp: number, options: PacketRetrievalOptions) { diff --git a/src/ogg/ogg-reader.ts b/src/ogg/ogg-reader.ts index 3459b30..34a4eae 100644 --- a/src/ogg/ogg-reader.ts +++ b/src/ogg/ogg-reader.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Reader } from '../reader'; +import { FileSlice, readI64Le, readU32Le, readU8 } from '../reader2'; import { OGGS } from './ogg-misc'; export const MIN_PAGE_HEADER_SIZE = 27; @@ -26,118 +26,68 @@ export type Page = { lacingValues: Uint8Array; }; -export class OggReader { - pos = 0; - constructor(public reader: Reader) {} +export const readPageHeader = (slice: FileSlice): Page | null => { + const startPos = slice.filePos; - readBytes(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - return new Uint8Array(view.buffer, offset, length); + const capturePattern = readU32Le(slice); + if (capturePattern !== OGGS) { + return null; } - readU8() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 1); - this.pos += 1; + slice.skip(1); // Version + const headerType = readU8(slice); + const granulePosition = readI64Le(slice); + const serialNumber = readU32Le(slice); + const sequenceNumber = readU32Le(slice); + const checksum = readU32Le(slice); - return view.getUint8(offset); + const numberPageSegments = readU8(slice); + const lacingValues = new Uint8Array(numberPageSegments); + + for (let i = 0; i < numberPageSegments; i++) { + lacingValues[i] = readU8(slice); } - readU32() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); - this.pos += 4; + const headerSize = 27 + numberPageSegments; + const dataSize = lacingValues.reduce((a, b) => a + b, 0); + const totalSize = headerSize + dataSize; - return view.getUint32(offset, true); - } + return { + headerStartPos: startPos, + totalSize, + dataStartPos: startPos + headerSize, + dataSize, + headerType, + granulePosition, + serialNumber, + sequenceNumber, + checksum, + lacingValues, + }; +}; - readI32() { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + 4); - this.pos += 4; +export const findNextPageHeader = (slice: FileSlice, until: number) => { + while (slice.filePos < until - (4 - 1)) { // Size of word minus 1 + const word = readU32Le(slice); + const firstByte = word & 0xff; + const secondByte = (word >>> 8) & 0xff; + const thirdByte = (word >>> 16) & 0xff; + const fourthByte = (word >>> 24) & 0xff; - return view.getInt32(offset, true); - } - - readI64() { - const low = this.readU32(); - const high = this.readI32(); - return high * 0x100000000 + low; - } - - readAscii(length: number) { - const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length); - this.pos += length; - - let str = ''; - for (let i = 0; i < length; i++) { - str += String.fromCharCode(view.getUint8(offset + i)); - } - return str; - } - - readPageHeader(): Page | null { - const startPos = this.pos; - - const capturePattern = this.readU32(); - if (capturePattern !== OGGS) { - return null; + const O = 0x4f; // 'O' + if (firstByte !== O && secondByte !== O && thirdByte !== O && fourthByte !== O) { + continue; } - this.pos += 1; // Version - const headerType = this.readU8(); - const granulePosition = this.readI64(); - const serialNumber = this.readU32(); - const sequenceNumber = this.readU32(); - const checksum = this.readU32(); + slice.skip(-4); - const numberPageSegments = this.readU8(); - const lacingValues = new Uint8Array(numberPageSegments); - - for (let i = 0; i < numberPageSegments; i++) { - lacingValues[i] = this.readU8(); + if (word === OGGS) { + // We have found the capture pattern + return true; } - const headerSize = 27 + numberPageSegments; - const dataSize = lacingValues.reduce((a, b) => a + b, 0); - const totalSize = headerSize + dataSize; - - return { - headerStartPos: startPos, - totalSize, - dataStartPos: startPos + headerSize, - dataSize, - headerType, - granulePosition, - serialNumber, - sequenceNumber, - checksum, - lacingValues, - }; + slice.skip(1); } - findNextPageHeader(until: number) { - while (this.pos < until - (4 - 1)) { // Size of word minus 1 - const word = this.readU32(); - const firstByte = word & 0xff; - const secondByte = (word >>> 8) & 0xff; - const thirdByte = (word >>> 16) & 0xff; - const fourthByte = (word >>> 24) & 0xff; - - const O = 0x4f; // 'O' - if (firstByte !== O && secondByte !== O && thirdByte !== O && fourthByte !== O) { - continue; - } - - this.pos -= 4; - - if (word === OGGS) { - // We have found the capture pattern - return true; - } - - this.pos += 1; - } - - return false; - } -} + return false; +}; diff --git a/src/reader2.ts b/src/reader2.ts new file mode 100644 index 0000000..2440da5 --- /dev/null +++ b/src/reader2.ts @@ -0,0 +1,294 @@ +import { clamp, MaybePromise, toDataView } from './misc'; +import { Source } from './source'; + +export class FileSlice { + bufferPos: number; + + constructor( + public readonly bytes: Uint8Array, + public readonly view: DataView, + private readonly offset: number, + public readonly start: number, + public readonly end: number, + ) { + this.bufferPos = start - offset; + } + + static tempFromBytes(bytes: Uint8Array) { + return new FileSlice( + bytes, + toDataView(bytes), + 0, + 0, + bytes.length, + ); + } + + get length() { + return this.end - this.start; + } + + get filePos() { + return this.offset + this.bufferPos; + } + + set filePos(value: number) { + this.bufferPos = value - this.offset; + } + + skip(byteCount: number) { + this.bufferPos += byteCount; + } + + slice(filePos: number, length = this.end - filePos) { + if (filePos < this.start || filePos + length > this.end) { + throw new RangeError('Slicing outside of original slice.'); + } + + return new FileSlice( + this.bytes, + this.view, + this.offset, + filePos, + filePos + length, + ); + } +} + +export class Reader2 { + private size: number | null = null; + + constructor(public source: Source) { + + } + + requestSize(): MaybePromise { + if (this.size !== null) { + return this.size; + } + + const size = this.source._retrieveSize2(); + if (size instanceof Promise) { + void size.then(x => this.size = x); + return size; + } else { + this.size = size; + return size; + } + } + + requestSlice(start: number, length: number): MaybePromise { + const end = start + length; + const result = this.source._read2(start, end); + + if (result instanceof Promise) { + return result.then((x) => { + if (!x) { + return null; + } + + return new FileSlice(x.bytes, x.view, x.offset, start, end); + }); + } else { + if (!result) { + return null; + } + + return new FileSlice(result.bytes, result.view, result.offset, start, end); + } + } + + requestSliceRange(start: number, minLength: number, maxLength: number): MaybePromise { + const fileSize = this.requestSize(); + + if (fileSize instanceof Promise) { + return fileSize.then(size => this.requestSlice( + start, + clamp(size - start, minLength, maxLength), + )); + } else { + return this.requestSlice( + start, + clamp(fileSize - start, minLength, maxLength), + ); + } + } +} + +export const readBytes = (slice: FileSlice, length: number) => { + const bytes = slice.bytes.subarray(slice.bufferPos, slice.bufferPos + length); + slice.bufferPos += length; + + return bytes; +}; + +export const readU8 = (slice: FileSlice) => slice.view.getUint8(slice.bufferPos++); + +export const readU16 = (slice: FileSlice, littleEndian: boolean) => { + const value = slice.view.getUint16(slice.bufferPos, littleEndian); + slice.bufferPos += 2; + + return value; +}; + +export const readU16Be = (slice: FileSlice) => { + const value = slice.view.getUint16(slice.bufferPos, false); + slice.bufferPos += 2; + + return value; +}; + +export const readU24Be = (slice: FileSlice) => { + const high = readU16Be(slice); + const low = readU8(slice); + return high * 0x100 + low; +}; + +export const readI16Be = (slice: FileSlice) => { + const value = slice.view.getInt16(slice.bufferPos, false); + slice.bufferPos += 2; + + return value; +}; + +export const readU32 = (slice: FileSlice, littleEndian: boolean) => { + const value = slice.view.getUint32(slice.bufferPos, littleEndian); + slice.bufferPos += 4; + + return value; +}; + +export const readU32Be = (slice: FileSlice) => { + const value = slice.view.getUint32(slice.bufferPos, false); + slice.bufferPos += 4; + + return value; +}; + +export const readU32Le = (slice: FileSlice) => { + const value = slice.view.getUint32(slice.bufferPos, true); + slice.bufferPos += 4; + + return value; +}; + +export const readI32Be = (slice: FileSlice) => { + const value = slice.view.getInt32(slice.bufferPos, false); + slice.bufferPos += 4; + + return value; +}; + +export const readI32Le = (slice: FileSlice) => { + const value = slice.view.getInt32(slice.bufferPos, true); + slice.bufferPos += 4; + + return value; +}; + +export const readU64 = (slice: FileSlice, littleEndian: boolean) => { + let low: number; + let high: number; + + if (littleEndian) { + low = readU32(slice, true); + high = readU32(slice, true); + } else { + high = readU32(slice, false); + low = readU32(slice, false); + } + + return high * 0x100000000 + low; +}; + +export const readU64Be = (slice: FileSlice) => { + const high = readU32Be(slice); + const low = readU32Be(slice); + return high * 0x100000000 + low; +}; + +export const readI64Be = (slice: FileSlice) => { + const high = readI32Be(slice); + const low = readU32Be(slice); + return high * 0x100000000 + low; +}; + +export const readI64Le = (slice: FileSlice) => { + const low = readU32Le(slice); + const high = readI32Le(slice); + return high * 0x100000000 + low; +}; + +export const readF32Be = (slice: FileSlice) => { + const value = slice.view.getFloat32(slice.bufferPos, false); + slice.bufferPos += 4; + + return value; +}; + +export const readF64Be = (slice: FileSlice) => { + const value = slice.view.getFloat64(slice.bufferPos, false); + slice.bufferPos += 8; + + return value; +}; + +export const readAscii = (slice: FileSlice, length: number) => { + if (slice.bufferPos + length > slice.bytes.length) { + throw new RangeError('Reading past end of slice.'); + } + + let str = ''; + + for (let i = 0; i < length; i++) { + str += String.fromCharCode(slice.bytes[slice.bufferPos++]!); + } + + return str; +}; + +export class ReturnValue { + isPending = true; + value!: T; + + set(value: T): typeof symbol { + this.isPending = false; + this.value = value; + + return symbol; + } +} + +const symbol: unique symbol = Symbol(); + +export type Uh = Promise; + +/* +class ReturnValue { + isSet = false; + value!: T; + + set(value: T) { + this.isSet = true; + this.value = value; + } +} + +const maybeWait = async (ret: ReturnValue) => { + if (Math.random() < 0.5) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + ret.set(Math.random()); +}; + +const yo = async () => { + const waitResult = new ReturnValue(); + const promise = maybeWait(waitResult); + if (!waitResult.isSet) await promise; + + console.log(waitResult.value); +}; + +yo(); +*/ diff --git a/src/source.ts b/src/source.ts index 20c662c..7f21d98 100644 --- a/src/source.ts +++ b/src/source.ts @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { mergeObjectsDeeply, retriedFetch } from './misc'; +import { assert, binarySearchLessOrEqual, insertSorted, MaybePromise, mergeObjectsDeeply, promiseWithResolvers, retriedFetch, toDataView } from './misc'; /** * The source base class, representing a resource from which bytes can be read. @@ -18,6 +18,13 @@ export abstract class Source { /** @internal */ abstract _retrieveSize(): Promise; + abstract _read2(start: number, end: number): MaybePromise<{ + bytes: Uint8Array; + view: DataView; + offset: number; + } | null>; + abstract _retrieveSize2(): MaybePromise; + /** @internal */ _sizePromise: Promise | null = null; @@ -40,6 +47,8 @@ export abstract class Source { export class BufferSource extends Source { /** @internal */ _bytes: Uint8Array; + /** @internal */ + _view: DataView; constructor(buffer: ArrayBuffer | Uint8Array) { if (!(buffer instanceof ArrayBuffer) && !(buffer instanceof Uint8Array)) { @@ -49,6 +58,7 @@ export class BufferSource extends Source { super(); this._bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + this._view = toDataView(this._bytes); } /** @internal */ @@ -56,6 +66,24 @@ export class BufferSource extends Source { return this._bytes.subarray(start, end); } + _read2(start: number, end: number) { + if (end > this._bytes.byteLength) { + return null; + } + + return { + bytes: this._bytes, + view: this._view, + offset: 0, + }; + + // return this._bytes.subarray(start, end); + } + + _retrieveSize2() { + return this._bytes.byteLength; + } + /** @internal */ async _retrieveSize() { return this._bytes.byteLength; @@ -108,6 +136,33 @@ export class StreamSource extends Source { } } +type BlobSourceReader = { + reader: ReadableStreamDefaultReader; + currentPos: number; + targetPos: number; + running: boolean; + pendingSlices: BlobSourcePendingSlice[]; + age: number; +}; + +type BlobSourceCacheEntry = { + start: number; + end: number; + bytes: Uint8Array; + view: DataView; + age: number; +}; + +type BlobSourcePendingSlice = { + start: number; + bytes: Uint8Array; + holes: { + start: number; + end: number; + }[]; + resolve: (bytes: Uint8Array) => void; +}; + /** * A source backed by a Blob. Since Files are also Blobs, this is the source to use when reading files off the disk. * @public @@ -115,6 +170,10 @@ export class StreamSource extends Source { export class BlobSource extends Source { /** @internal */ _blob: Blob; + _cache: BlobSourceCacheEntry[] = []; + _totalCacheSize = 0; + _readers: BlobSourceReader[] = []; + _nextAge = 0; constructor(blob: Blob) { if (!(blob instanceof Blob)) { @@ -126,6 +185,302 @@ export class BlobSource extends Source { this._blob = blob; } + _read2(start: number, end: number) { + if (end > this._retrieveSize2()) { + return null; + } + + const cacheStartIndex = binarySearchLessOrEqual(this._cache, start, x => x.start); + const startEntry = cacheStartIndex !== -1 ? this._cache[cacheStartIndex] : null; + + if (startEntry && startEntry.start <= start && end <= startEntry.end) { + startEntry.age = this._nextAge++; + + return { + bytes: startEntry.bytes, + view: startEntry.view, + offset: startEntry.start, + }; + } + + const bytes = new Uint8Array(end - start); + let lastEnd = start; + const holes: { + start: number; + end: number; + }[] = []; + + if (cacheStartIndex !== -1) { + for (let i = cacheStartIndex; i < this._cache.length; i++) { + const entry = this._cache[i]!; + if (entry.start >= end) { + break; + } + if (entry.end <= start) { + continue; + } + + const cappedStart = Math.max(start, entry.start); + const cappedEnd = Math.min(end, entry.end); + assert(cappedStart <= cappedEnd); + + if (lastEnd < cappedStart) { + holes.push({ start: lastEnd, end: cappedStart }); + } + lastEnd = cappedEnd; + + bytes.set( + entry.bytes.subarray(cappedStart - entry.start, cappedEnd - entry.start), + cappedStart - start, + ); + entry.age = this._nextAge++; + } + + if (lastEnd < end) { + holes.push({ start: lastEnd, end }); + } + } else { + holes.push({ start, end }); + } + + if (holes.length === 0) { + return { + bytes, + view: toDataView(bytes), + offset: start, + }; + } + + const { promise, resolve } = promiseWithResolvers(); + + for (const hole of holes) { + const pendingSlice: BlobSourcePendingSlice = { + start, + bytes, + holes, + resolve, + }; + + const readerStart = Math.min(hole.start, Math.max(this._retrieveSize2() - 131072, 0)); + const readerEnd = hole.end;// Math.min(Math.max(readerStart + 131072, hole.end), this._retrieveSize2()); + + let readerFound = false; + for (const reader of this._readers) { + if (reader.currentPos <= readerStart && readerStart - 131072 <= reader.targetPos) { + reader.targetPos = Math.max(reader.targetPos, readerEnd); + readerFound = true; + + if (!reader.pendingSlices.includes(pendingSlice)) { + reader.pendingSlices.push(pendingSlice); + } + + if (!reader.running) { + void this._runReader(reader); + } + + break; + } + } + + if (!readerFound) { + const newReader: BlobSourceReader = { + reader: this._blob.slice(readerStart).stream().getReader(), + currentPos: readerStart, + targetPos: readerEnd, + running: false, + pendingSlices: [pendingSlice], + age: 0, // Will be set once we run it + }; + this._readers.push(newReader); + + void this._runReader(newReader); + + if (this._readers.length > 4) { + let oldestIndex = 0; + let oldestReader = this._readers[0]!; + + for (let i = 1; i < this._readers.length; i++) { + const reader = this._readers[i]!; + + if (reader.age < oldestReader.age) { + oldestIndex = i; + oldestReader = reader; + } + } + + this._readers.splice(oldestIndex, 1); + } + } + } + + return promise.then(bytes => ({ + bytes, + view: toDataView(bytes), + offset: start, + })); + } + + async _runReader(reader: BlobSourceReader) { + assert(!reader.running); + reader.running = true; + reader.age = this._nextAge++; + + while (reader.currentPos < reader.targetPos) { + const { done, value } = await reader.reader.read(); + if (done) { + const readerIndex = this._readers.indexOf(reader); + assert(readerIndex !== -1); + + this._readers.splice(readerIndex, 1); + + break; + } + + const start = reader.currentPos; + const end = start + value.length; + + this._insertIntoCache({ + start, + end, + bytes: value, + view: toDataView(value), + age: this._nextAge++, + }); + reader.currentPos += value.length; + reader.targetPos = Math.max(reader.targetPos, reader.currentPos); + + for (let i = 0; i < reader.pendingSlices.length; i++) { + const pendingSlice = reader.pendingSlices[i]!; + + const clampedStart = Math.max(start, pendingSlice.start); + const clampedEnd = Math.min(end, pendingSlice.start + pendingSlice.bytes.length); + + if (clampedStart < clampedEnd) { + pendingSlice.bytes.set( + value.subarray(clampedStart - start, clampedEnd - start), + clampedStart - pendingSlice.start, + ); + } + + for (let j = 0; j < pendingSlice.holes.length; j++) { + // The hole is intentionally not modified here if the read section starts somewhere in the middle of + // the hole. We don't need to do "hole splitting", since the readers are spawned *by* the holes, + // meaning there's always a reader which will consume the hole left to right. + const hole = pendingSlice.holes[j]!; + if (start <= hole.start && end > hole.start) { + hole.start = end; + } + + if (hole.end <= hole.start) { + pendingSlice.holes.splice(j, 1); + j--; + } + } + + if (pendingSlice.holes.length === 0) { + pendingSlice.resolve(pendingSlice.bytes); + reader.pendingSlices.splice(i, 1); + i--; + } + } + } + + reader.running = false; + } + + _insertIntoCache(entry: BlobSourceCacheEntry) { + let insertionIndex = binarySearchLessOrEqual(this._cache, entry.start, x => x.start) + 1; + + if (insertionIndex > 0) { + const previous = this._cache[insertionIndex - 1]!; + if (previous.end >= entry.end) { + // Previous entry swallows the one to be inserted; we don't need to do anything + return; + } + + if (previous.end > entry.start) { + // Partial overlap with the previous entry, let's join + const joined = new Uint8Array(entry.end - previous.start); + joined.set(previous.bytes, 0); + joined.set(entry.bytes, entry.start - previous.start); + + previous.bytes = joined; + previous.view = toDataView(joined); + previous.end = entry.end; + + // Do the rest of the logic with the previous entry instead + insertionIndex--; + entry = previous; + + this._totalCacheSize += entry.end - previous.end; + } else { + this._cache.splice(insertionIndex, 0, entry); + this._totalCacheSize += entry.bytes.length; + } + } else { + this._cache.splice(insertionIndex, 0, entry); + this._totalCacheSize += entry.bytes.length; + } + + for (let i = insertionIndex + 1; i < this._cache.length; i++) { + const next = this._cache[i]!; + if (entry.end <= next.start) { + // Even if they touch, we don't wanna merge them, no need + break; + } + + if (entry.end >= next.end) { + // The inserted entry completely swallows the next entry + this._cache.splice(i, 1); + this._totalCacheSize -= next.bytes.length; + i--; + continue; + } + + // Partial overlap, let's join + const joined = new Uint8Array(next.end - entry.start); + joined.set(entry.bytes, 0); + joined.set(next.bytes, next.start - entry.start); + + entry.bytes = joined; + entry.view = toDataView(joined); + entry.end = next.end; + this._cache.splice(i, 1); + + this._totalCacheSize -= entry.end - next.start; + + break; // After the join case, we're done: the next entry cannot possibly overlap with the inserted one. + } + + const MAX_CACHE_SIZE = 8 * 2 ** 20; // 8 MiB + + while (this._totalCacheSize > MAX_CACHE_SIZE) { + let oldestIndex = 0; + let oldestEntry = this._cache[0]!; + + for (let i = 1; i < this._cache.length; i++) { + const entry = this._cache[i]!; + + if (entry.age < oldestEntry.age) { + oldestIndex = i; + oldestEntry = entry; + } + } + + this._cache.splice(oldestIndex, 1); + this._totalCacheSize -= oldestEntry.bytes.length; + } + } + + _cachedSize: number | null = null; + _retrieveSize2() { + if (this._cachedSize !== null) { + return this._cachedSize; + } + + return this._cachedSize = this._blob.size; // Reading this field is expensive + } + /** @internal */ async _read(start: number, end: number) { const slice = this._blob.slice(start, end); diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts index 1c3aa7d..c576537 100644 --- a/src/wave/wave-demuxer.ts +++ b/src/wave/wave-demuxer.ts @@ -13,8 +13,7 @@ import { InputAudioTrack, InputAudioTrackBacking } from '../input-track'; import { PacketRetrievalOptions } from '../media-sink'; import { assert, UNDETERMINED_LANGUAGE } from '../misc'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; -import { Reader } from '../reader'; -import { RiffReader } from './riff-reader'; +import { readAscii, readBytes, Reader2, readU16, readU32, readU64 } from '../reader2'; export enum WaveFormat { PCM = 0x0001, @@ -25,8 +24,7 @@ export enum WaveFormat { } export class WaveDemuxer extends Demuxer { - metadataReader: RiffReader; - chunkReader: RiffReader; + reader: Reader2; metadataPromise: Promise | null = null; dataStart = -1; @@ -44,60 +42,66 @@ export class WaveDemuxer extends Demuxer { constructor(input: Input) { super(input); - this.metadataReader = new RiffReader(input._mainReader); - this.chunkReader = new RiffReader(new Reader(input.source, 64 * 2 ** 20)); + this.reader = input._reader2; } async readMetadata() { return this.metadataPromise ??= (async () => { - const actualFileSize = await this.metadataReader.reader.source.getSize(); + let actualFileSize = this.reader.requestSize(); + if (actualFileSize instanceof Promise) actualFileSize = await actualFileSize; - const riffType = this.metadataReader.readAscii(4); - this.metadataReader.littleEndian = riffType !== 'RIFX'; + let slice = this.reader.requestSlice(0, 12); + if (slice instanceof Promise) slice = await slice; + assert(slice); + + const riffType = readAscii(slice, 4); + const littleEndian = riffType !== 'RIFX'; const isRf64 = riffType === 'RF64'; - const outerChunkSize = this.metadataReader.readU32(); + const outerChunkSize = readU32(slice, littleEndian); let totalFileSize = isRf64 ? actualFileSize : Math.min(outerChunkSize + 8, actualFileSize); - const format = this.metadataReader.readAscii(4); + const format = readAscii(slice, 4); if (format !== 'WAVE') { throw new Error('Invalid WAVE file - wrong format'); } - this.metadataReader.pos = 12; let chunksRead = 0; let dataChunkSize: number | null = null; + let currentPos = slice.filePos; - while (this.metadataReader.pos < totalFileSize) { - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 8); + while (currentPos < totalFileSize) { + let slice = this.reader.requestSlice(currentPos, 8); + if (slice instanceof Promise) slice = await slice; + if (!slice) break; - const chunkId = this.metadataReader.readAscii(4); - const chunkSize = this.metadataReader.readU32(); - const startPos = this.metadataReader.pos; + const chunkId = readAscii(slice, 4); + const chunkSize = readU32(slice, littleEndian); + const startPos = slice.filePos; if (isRf64 && chunksRead === 0 && chunkId !== 'ds64') { throw new Error('Invalid RF64 file: First chunk must be "ds64".'); } if (chunkId === 'fmt ') { - await this.parseFmtChunk(chunkSize); + await this.parseFmtChunk(startPos, chunkSize, littleEndian); } else if (chunkId === 'data') { dataChunkSize ??= chunkSize; - this.dataStart = this.metadataReader.pos; + this.dataStart = slice.filePos; this.dataSize = Math.min(dataChunkSize, totalFileSize - this.dataStart); } else if (chunkId === 'ds64') { // File and data chunk sizes are defined in here instead - const riffChunkSize = this.metadataReader.readU64(); - dataChunkSize = this.metadataReader.readU64(); + const riffChunkSize = readU64(slice, littleEndian); + dataChunkSize = readU64(slice, littleEndian); totalFileSize = Math.min(riffChunkSize + 8, actualFileSize); } - this.metadataReader.pos = startPos + chunkSize + (chunkSize & 1); // Handle padding + currentPos = startPos + chunkSize + (chunkSize & 1); // Handle padding chunksRead++; } @@ -115,33 +119,35 @@ export class WaveDemuxer extends Demuxer { })(); } - private async parseFmtChunk(size: number) { - await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + size); + private async parseFmtChunk(startPos: number, size: number, littleEndian: boolean) { + let slice = this.reader.requestSlice(startPos, size); + if (slice instanceof Promise) slice = await slice; + if (!slice) return; // File too short - let formatTag = this.metadataReader.readU16(); - const numChannels = this.metadataReader.readU16(); - const sampleRate = this.metadataReader.readU32(); - this.metadataReader.pos += 4; // Bytes per second - const blockAlign = this.metadataReader.readU16(); + let formatTag = readU16(slice, littleEndian); + const numChannels = readU16(slice, littleEndian); + const sampleRate = readU32(slice, littleEndian); + slice.skip(4); // Bytes per second + const blockAlign = readU16(slice, littleEndian); let bitsPerSample: number; if (size === 14) { // Plain WAVEFORMAT bitsPerSample = 8; } else { - bitsPerSample = this.metadataReader.readU16(); + bitsPerSample = readU16(slice, littleEndian); } // Handle WAVEFORMATEXTENSIBLE if (size >= 18 && formatTag !== 0x0165) { - const cbSize = this.metadataReader.readU16(); + const cbSize = readU16(slice, littleEndian); const remainingSize = size - 18; const extensionSize = Math.min(remainingSize, cbSize); if (extensionSize >= 22 && formatTag === WaveFormat.EXTENSIBLE) { // Parse WAVEFORMATEXTENSIBLE - this.metadataReader.pos += 2 + 4; - const subFormat = this.metadataReader.readBytes(16); + slice.skip(2 + 4); + const subFormat = readBytes(slice, 16); // Get actual format from subFormat GUID formatTag = subFormat[0]! | (subFormat[1]! << 8); @@ -291,19 +297,11 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking { if (options.metadataOnly) { data = PLACEHOLDER_DATA; } else { - const sizeOfOnePacket = PACKET_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes; - const chunkSize = Math.ceil(2 ** 19 / sizeOfOnePacket) * sizeOfOnePacket; - const chunkStart = Math.floor(startOffset / chunkSize) * chunkSize; - const chunkEnd = chunkStart + chunkSize; + let slice = this.demuxer.reader.requestSlice(this.demuxer.dataStart + startOffset, sizeInBytes); + if (slice instanceof Promise) slice = await slice; + assert(slice); - // Always load large 0.5 MiB chunks instead of just the required packet - await this.demuxer.chunkReader.reader.loadRange( - this.demuxer.dataStart + chunkStart, - this.demuxer.dataStart + chunkEnd, - ); - - this.demuxer.chunkReader.pos = this.demuxer.dataStart + startOffset; - data = this.demuxer.chunkReader.readBytes(sizeInBytes); + data = readBytes(slice, sizeInBytes); } const timestamp = packetIndex * PACKET_SIZE_IN_FRAMES / this.demuxer.audioInfo.sampleRate;