Add new slice-based reader system, port all demuxers to new reader system, add optimized BlobSource

This commit is contained in:
Vanilagy
2025-08-28 17:24:31 +02:00
parent 0e6cddc941
commit 641cdd02e3
17 changed files with 2230 additions and 1772 deletions
+62 -62
View File
@@ -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<void> | 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();
+58 -69
View File
@@ -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,
};
};