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
+72 -71
View File
@@ -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<void> | 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();