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;