mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-30 04:13:51 +02:00
Add new slice-based reader system, port all demuxers to new reader system, add optimized BlobSource
This commit is contained in:
+62
-62
@@ -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
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user