mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 11:23:45 +02:00
Add new slice-based reader system, port all demuxers to new reader system, add optimized BlobSource
This commit is contained in:
+72
-71
@@ -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();
|
||||
|
||||
+38
-68
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user