mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Migrate Ogg demuxer to new packet system, improve Ogg demuxer performance
This commit is contained in:
+3
-3
@@ -14,15 +14,15 @@
|
|||||||
source: new Mediabunny.BufferSource(await file.arrayBuffer()),
|
source: new Mediabunny.BufferSource(await file.arrayBuffer()),
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoTrack = await input.getPrimaryVideoTrack();
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
|
|
||||||
const iters = 50;
|
const iters = 250;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
for (let i = 0; i < iters; i++) {
|
for (let i = 0; i < iters; i++) {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const reader = new Mediabunny.PacketCursor(videoTrack);
|
const reader = new Mediabunny.PacketCursor(audioTrack);
|
||||||
await reader.iterate(() => count++);
|
await reader.iterate(() => count++);
|
||||||
|
|
||||||
console.log(count)
|
console.log(count)
|
||||||
|
|||||||
+203
-136
@@ -15,13 +15,15 @@ import { PacketRetrievalOptions } from '../media-sink';
|
|||||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||||
import {
|
import {
|
||||||
assert,
|
assert,
|
||||||
AsyncMutex,
|
AsyncMutex4,
|
||||||
binarySearchLessOrEqual,
|
binarySearchLessOrEqual,
|
||||||
findLast,
|
findLast,
|
||||||
last,
|
last,
|
||||||
|
ResultValue,
|
||||||
roundIfAlmostInteger,
|
roundIfAlmostInteger,
|
||||||
toDataView,
|
toDataView,
|
||||||
UNDETERMINED_LANGUAGE,
|
UNDETERMINED_LANGUAGE,
|
||||||
|
Yo,
|
||||||
} from '../misc';
|
} from '../misc';
|
||||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||||
import { readBytes, Reader } from '../reader';
|
import { readBytes, Reader } from '../reader';
|
||||||
@@ -52,6 +54,11 @@ type Packet = {
|
|||||||
endSegmentIndex: number;
|
endSegmentIndex: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PacketStart = {
|
||||||
|
startPage: Page;
|
||||||
|
startSegmentIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class OggDemuxer extends Demuxer {
|
export class OggDemuxer extends Demuxer {
|
||||||
reader: Reader;
|
reader: Reader;
|
||||||
|
|
||||||
@@ -105,7 +112,10 @@ export class OggDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const bitstream of this.bitstreams) {
|
for (const bitstream of this.bitstreams) {
|
||||||
const firstPacket = await this.readPacket(bitstream.bosPage, 0);
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
|
await this.readPacket(packetResult, bitstream.bosPage, 0);
|
||||||
|
|
||||||
|
const firstPacket = packetResult.value;
|
||||||
if (!firstPacket) {
|
if (!firstPacket) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -145,22 +155,32 @@ export class OggDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async readVorbisMetadata(firstPacket: Packet, bitstream: LogicalBitstream) {
|
async readVorbisMetadata(firstPacket: Packet, bitstream: LogicalBitstream) {
|
||||||
let nextPacketPosition = await this.findNextPacketStart(firstPacket);
|
const positionResult = new ResultValue<PacketStart | null>();
|
||||||
|
await this.findNextPacketStart(positionResult, firstPacket);
|
||||||
|
|
||||||
|
let nextPacketPosition = positionResult.value;
|
||||||
if (!nextPacketPosition) {
|
if (!nextPacketPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
|
await this.readPacket(packetResult, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
||||||
|
|
||||||
|
const secondPacket = packetResult.value;
|
||||||
if (!secondPacket) {
|
if (!secondPacket) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPacketPosition = await this.findNextPacketStart(secondPacket);
|
await this.findNextPacketStart(positionResult, secondPacket);
|
||||||
|
|
||||||
|
nextPacketPosition = positionResult.value;
|
||||||
if (!nextPacketPosition) {
|
if (!nextPacketPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const thirdPacket = await this.readPacket(nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
await this.readPacket(packetResult, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
||||||
|
|
||||||
|
const thirdPacket = packetResult.value;
|
||||||
if (!thirdPacket) {
|
if (!thirdPacket) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -228,15 +248,19 @@ export class OggDemuxer extends Demuxer {
|
|||||||
// From https://datatracker.ietf.org/doc/html/rfc7845#section-5:
|
// 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
|
// "An Ogg Opus logical stream contains exactly two mandatory header packets: an identification header and a
|
||||||
// comment header."
|
// comment header."
|
||||||
const nextPacketPosition = await this.findNextPacketStart(firstPacket);
|
|
||||||
|
const positionResult = new ResultValue<PacketStart | null>();
|
||||||
|
await this.findNextPacketStart(positionResult, firstPacket);
|
||||||
|
|
||||||
|
const nextPacketPosition = positionResult.value;
|
||||||
if (!nextPacketPosition) {
|
if (!nextPacketPosition) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondPacket = await this.readPacket(
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
nextPacketPosition.startPage,
|
await this.readPacket(packetResult, nextPacketPosition.startPage, nextPacketPosition.startSegmentIndex);
|
||||||
nextPacketPosition.startSegmentIndex,
|
|
||||||
);
|
const secondPacket = packetResult.value;
|
||||||
if (!secondPacket) {
|
if (!secondPacket) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -256,7 +280,7 @@ export class OggDemuxer extends Demuxer {
|
|||||||
readVorbisComments(secondPacket.data.subarray(8), this.metadataTags); // Skip 'OpusTags'
|
readVorbisComments(secondPacket.data.subarray(8), this.metadataTags); // Skip 'OpusTags'
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPacket(startPage: Page, startSegmentIndex: number): Promise<Packet | null> {
|
async readPacket(res: ResultValue<Packet | null>, startPage: Page, startSegmentIndex: number): Promise<Yo> {
|
||||||
assert(startSegmentIndex < startPage.lacingValues.length);
|
assert(startSegmentIndex < startPage.lacingValues.length);
|
||||||
|
|
||||||
let startDataOffset = 0;
|
let startDataOffset = 0;
|
||||||
@@ -301,12 +325,12 @@ export class OggDemuxer extends Demuxer {
|
|||||||
let headerSlice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
let headerSlice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_PAGE_HEADER_SIZE);
|
||||||
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
if (headerSlice instanceof Promise) headerSlice = await headerSlice;
|
||||||
if (!headerSlice) {
|
if (!headerSlice) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPage = readPageHeader(headerSlice);
|
const nextPage = readPageHeader(headerSlice);
|
||||||
if (!nextPage) {
|
if (!nextPage) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPage = nextPage;
|
currentPage = nextPage;
|
||||||
@@ -321,33 +345,40 @@ export class OggDemuxer extends Demuxer {
|
|||||||
currentSegmentIndex = 0;
|
currentSegmentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalPacketSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
let packetData: Uint8Array;
|
||||||
const packetData = new Uint8Array(totalPacketSize);
|
|
||||||
|
|
||||||
let offset = 0;
|
if (chunks.length === 1) {
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
// Fast path, no need for an allocation. Also typically the common path!
|
||||||
const chunk = chunks[i]!;
|
packetData = chunks[0]!;
|
||||||
packetData.set(chunk, offset);
|
} else {
|
||||||
offset += chunk.length;
|
const totalPacketSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||||
|
packetData = new Uint8Array(totalPacketSize);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
const chunk = chunks[i]!;
|
||||||
|
packetData.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return res.set({
|
||||||
data: packetData,
|
data: packetData,
|
||||||
endPage: currentPage,
|
endPage: currentPage,
|
||||||
endSegmentIndex: currentSegmentIndex,
|
endSegmentIndex: currentSegmentIndex,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNextPacketStart(lastPacket: Packet) {
|
async findNextPacketStart(res: ResultValue<PacketStart | null>, lastPacket: Packet): Promise<Yo> {
|
||||||
// If there's another segment in the same page, return it
|
// If there's another segment in the same page, return it
|
||||||
if (lastPacket.endSegmentIndex < lastPacket.endPage.lacingValues.length - 1) {
|
if (lastPacket.endSegmentIndex < lastPacket.endPage.lacingValues.length - 1) {
|
||||||
return { startPage: lastPacket.endPage, startSegmentIndex: lastPacket.endSegmentIndex + 1 };
|
return res.set({ startPage: lastPacket.endPage, startSegmentIndex: lastPacket.endSegmentIndex + 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEos = !!(lastPacket.endPage.headerType & 0x04);
|
const isEos = !!(lastPacket.endPage.headerType & 0x04);
|
||||||
if (isEos) {
|
if (isEos) {
|
||||||
// The page is marked as the last page of the logical bitstream, so we won't find anything beyond it
|
// The page is marked as the last page of the logical bitstream, so we won't find anything beyond it
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, search for the next page belonging to the same bitstream
|
// Otherwise, search for the next page belonging to the same bitstream
|
||||||
@@ -356,16 +387,16 @@ export class OggDemuxer extends Demuxer {
|
|||||||
let slice = this.reader.requestSliceRange(currentPos, MIN_PAGE_HEADER_SIZE, MAX_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 instanceof Promise) slice = await slice;
|
||||||
if (!slice) {
|
if (!slice) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPage = readPageHeader(slice);
|
const nextPage = readPageHeader(slice);
|
||||||
if (!nextPage) {
|
if (!nextPage) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextPage.serialNumber === lastPacket.endPage.serialNumber) {
|
if (nextPage.serialNumber === lastPacket.endPage.serialNumber) {
|
||||||
return { startPage: nextPage, startSegmentIndex: 0 };
|
return res.set({ startPage: nextPage, startSegmentIndex: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPos = nextPage.headerStartPos + nextPage.totalSize;
|
currentPos = nextPage.headerStartPos + nextPage.totalSize;
|
||||||
@@ -387,12 +418,6 @@ export class OggDemuxer extends Demuxer {
|
|||||||
return this.tracks;
|
return this.tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
async computeDuration() {
|
|
||||||
const tracks = await this.getTracks();
|
|
||||||
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
|
|
||||||
return Math.max(0, ...trackDurations);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMetadataTags() {
|
async getMetadataTags() {
|
||||||
await this.readMetadata();
|
await this.readMetadata();
|
||||||
return this.metadataTags;
|
return this.metadataTags;
|
||||||
@@ -409,9 +434,8 @@ type EncodedPacketMetadata = {
|
|||||||
|
|
||||||
class OggAudioTrackBacking implements InputAudioTrackBacking {
|
class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||||
internalSampleRate: number;
|
internalSampleRate: number;
|
||||||
encodedPacketToMetadata = new WeakMap<EncodedPacket, EncodedPacketMetadata>();
|
|
||||||
sequentialScanCache: EncodedPacketMetadata[] = [];
|
sequentialScanCache: EncodedPacketMetadata[] = [];
|
||||||
sequentialScanMutex = new AsyncMutex();
|
sequentialScanMutex = new AsyncMutex4();
|
||||||
|
|
||||||
constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) {
|
constructor(public bitstream: LogicalBitstream, public demuxer: OggDemuxer) {
|
||||||
// Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different
|
// Opus always uses a fixed sample rate for its internal calculations, even if the actual rate is different
|
||||||
@@ -469,15 +493,6 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFirstTimestamp() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async computeDuration() {
|
|
||||||
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
|
|
||||||
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
granulePositionToTimestampInSamples(granulePosition: number) {
|
granulePositionToTimestampInSamples(granulePosition: number) {
|
||||||
if (this.bitstream.codecInfo.codec === 'opus') {
|
if (this.bitstream.codecInfo.codec === 'opus') {
|
||||||
assert(this.bitstream.codecInfo.opusInfo);
|
assert(this.bitstream.codecInfo.opusInfo);
|
||||||
@@ -514,21 +529,28 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
packet.data.byteLength,
|
packet.data.byteLength,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.encodedPacketToMetadata.set(encodedPacket, {
|
encodedPacket._internal = {
|
||||||
packet,
|
packet,
|
||||||
timestampInSamples: additional.timestampInSamples,
|
timestampInSamples: additional.timestampInSamples,
|
||||||
durationInSamples,
|
durationInSamples,
|
||||||
vorbisLastBlockSize: additional.vorbisLastBlocksize,
|
vorbisLastBlockSize: additional.vorbisLastBlocksize,
|
||||||
vorbisBlockSize,
|
vorbisBlockSize,
|
||||||
});
|
} satisfies EncodedPacketMetadata;
|
||||||
|
|
||||||
return encodedPacket;
|
return encodedPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFirstPacket(options: PacketRetrievalOptions) {
|
async getFirstPacket(res: ResultValue<EncodedPacket | null>, options: PacketRetrievalOptions): Promise<Yo> {
|
||||||
assert(this.bitstream.lastMetadataPacket);
|
assert(this.bitstream.lastMetadataPacket);
|
||||||
const packetPosition = await this.demuxer.findNextPacketStart(this.bitstream.lastMetadataPacket);
|
|
||||||
|
const positionResult = new ResultValue<PacketStart | null>();
|
||||||
|
let promise = this.demuxer.findNextPacketStart(positionResult, this.bitstream.lastMetadataPacket);
|
||||||
|
if (positionResult.pending) await promise;
|
||||||
|
|
||||||
|
const packetPosition = positionResult.value;
|
||||||
|
|
||||||
if (!packetPosition) {
|
if (!packetPosition) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestampInSamples = 0;
|
let timestampInSamples = 0;
|
||||||
@@ -537,66 +559,84 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
timestampInSamples -= this.bitstream.codecInfo.opusInfo.preSkip;
|
timestampInSamples -= this.bitstream.codecInfo.opusInfo.preSkip;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packet = await this.demuxer.readPacket(packetPosition.startPage, packetPosition.startSegmentIndex);
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
|
promise = this.demuxer.readPacket(packetResult, packetPosition.startPage, packetPosition.startSegmentIndex);
|
||||||
|
if (packetResult.pending) await promise;
|
||||||
|
|
||||||
return this.createEncodedPacketFromOggPacket(
|
return res.set(this.createEncodedPacketFromOggPacket(
|
||||||
packet,
|
packetResult.value,
|
||||||
{
|
{
|
||||||
timestampInSamples,
|
timestampInSamples,
|
||||||
vorbisLastBlocksize: null,
|
vorbisLastBlocksize: null,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNextPacket(prevPacket: EncodedPacket, options: PacketRetrievalOptions) {
|
async getNextPacket(
|
||||||
const prevMetadata = this.encodedPacketToMetadata.get(prevPacket);
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
prevPacket: EncodedPacket,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
|
const prevMetadata = prevPacket._internal as EncodedPacketMetadata | undefined;
|
||||||
if (!prevMetadata) {
|
if (!prevMetadata) {
|
||||||
throw new Error('Packet was not created from this track.');
|
throw new Error('Packet was not created from this track.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const packetPosition = await this.demuxer.findNextPacketStart(prevMetadata.packet);
|
const positionResult = new ResultValue<PacketStart | null>();
|
||||||
|
let promise = this.demuxer.findNextPacketStart(positionResult, prevMetadata.packet);
|
||||||
|
if (positionResult.pending) await promise;
|
||||||
|
|
||||||
|
const packetPosition = positionResult.value;
|
||||||
if (!packetPosition) {
|
if (!packetPosition) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples;
|
const timestampInSamples = prevMetadata.timestampInSamples + prevMetadata.durationInSamples;
|
||||||
|
|
||||||
const packet = await this.demuxer.readPacket(
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
packetPosition.startPage,
|
promise = this.demuxer.readPacket(packetResult, packetPosition.startPage, packetPosition.startSegmentIndex);
|
||||||
packetPosition.startSegmentIndex,
|
if (packetResult.pending) await promise;
|
||||||
);
|
|
||||||
|
|
||||||
return this.createEncodedPacketFromOggPacket(
|
return res.set(this.createEncodedPacketFromOggPacket(
|
||||||
packet,
|
packetResult.value,
|
||||||
{
|
{
|
||||||
timestampInSamples,
|
timestampInSamples,
|
||||||
vorbisLastBlocksize: prevMetadata.vorbisBlockSize,
|
vorbisLastBlocksize: prevMetadata.vorbisBlockSize,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPacket(timestamp: number, options: PacketRetrievalOptions) {
|
async getPacket(
|
||||||
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
timestamp: number,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
if (this.demuxer.reader.fileSize === null) {
|
if (this.demuxer.reader.fileSize === null) {
|
||||||
// No file size known, can't do binary search, but fall back to sequential algo instead
|
// No file size known, can't do binary search, but fall back to sequential algo instead
|
||||||
return this.getPacketSequential(timestamp, options);
|
return this.getPacketSequential(res, timestamp, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
||||||
if (timestampInSamples === 0) {
|
if (timestampInSamples === 0) {
|
||||||
// Fast path for timestamp 0 - avoids binary search when playing back from the start
|
// Fast path for timestamp 0 - avoids binary search when playing back from the start
|
||||||
return this.getFirstPacket(options);
|
return this.getFirstPacket(res, options);
|
||||||
}
|
}
|
||||||
if (timestampInSamples < 0) {
|
if (timestampInSamples < 0) {
|
||||||
// There's nothing here
|
// There's nothing here
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(this.bitstream.lastMetadataPacket);
|
assert(this.bitstream.lastMetadataPacket);
|
||||||
const startPosition = await this.demuxer.findNextPacketStart(this.bitstream.lastMetadataPacket);
|
|
||||||
|
const positionResult = new ResultValue<PacketStart | null>();
|
||||||
|
const promise = this.demuxer.findNextPacketStart(positionResult, this.bitstream.lastMetadataPacket);
|
||||||
|
if (positionResult.pending) await promise;
|
||||||
|
|
||||||
|
const startPosition = positionResult.value;
|
||||||
if (!startPosition) {
|
if (!startPosition) {
|
||||||
return null;
|
return res.set(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lowPage = startPosition.startPage;
|
let lowPage = startPosition.startPage;
|
||||||
@@ -778,7 +818,12 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
endPage,
|
endPage,
|
||||||
endSegmentIndex,
|
endSegmentIndex,
|
||||||
};
|
};
|
||||||
const nextPosition = await this.demuxer.findNextPacketStart(pseudopacket);
|
|
||||||
|
positionResult.reset();
|
||||||
|
const promise = this.demuxer.findNextPacketStart(positionResult, pseudopacket);
|
||||||
|
if (positionResult.pending) await promise;
|
||||||
|
|
||||||
|
const nextPosition = positionResult.value;
|
||||||
|
|
||||||
if (nextPosition) {
|
if (nextPosition) {
|
||||||
// Let's rewind a single step (packet) - this previous packet ensures that we'll correctly compute
|
// Let's rewind a single step (packet) - this previous packet ensures that we'll correctly compute
|
||||||
@@ -827,12 +872,18 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
let lastEncodedPacket: EncodedPacket | null = null;
|
let lastEncodedPacket: EncodedPacket | null = null;
|
||||||
let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null;
|
let lastEncodedPacketMetadata: EncodedPacketMetadata | null = null;
|
||||||
|
|
||||||
|
const packetResult = new ResultValue<Packet | null>();
|
||||||
|
|
||||||
// Alright, now it's time for the final, granular seek: We keep iterating over packets until we've found the
|
// 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.
|
// one with the correct timestamp - i.e., the last one with a timestamp <= the timestamp we're looking for.
|
||||||
while (currentPage !== null) {
|
while (currentPage !== null) {
|
||||||
assert(currentSegmentIndex !== null);
|
assert(currentSegmentIndex !== null);
|
||||||
|
|
||||||
const packet = await this.demuxer.readPacket(currentPage, currentSegmentIndex);
|
packetResult.reset();
|
||||||
|
let promise = this.demuxer.readPacket(packetResult, currentPage, currentSegmentIndex);
|
||||||
|
if (packetResult.pending) await promise;
|
||||||
|
|
||||||
|
const packet = packetResult.value;
|
||||||
if (!packet) {
|
if (!packet) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -852,7 +903,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
);
|
);
|
||||||
assert(encodedPacket);
|
assert(encodedPacket);
|
||||||
|
|
||||||
let encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket);
|
let encodedPacketMetadata = encodedPacket._internal as EncodedPacketMetadata | undefined;
|
||||||
assert(encodedPacketMetadata);
|
assert(encodedPacketMetadata);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -877,7 +928,7 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
);
|
);
|
||||||
assert(encodedPacket);
|
assert(encodedPacket);
|
||||||
|
|
||||||
encodedPacketMetadata = this.encodedPacketToMetadata.get(encodedPacket);
|
encodedPacketMetadata = encodedPacket._internal as EncodedPacketMetadata | undefined;
|
||||||
assert(encodedPacketMetadata);
|
assert(encodedPacketMetadata);
|
||||||
} else {
|
} else {
|
||||||
currentTimestampInSamples += encodedPacketMetadata.durationInSamples;
|
currentTimestampInSamples += encodedPacketMetadata.durationInSamples;
|
||||||
@@ -899,7 +950,11 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPosition = await this.demuxer.findNextPacketStart(packet);
|
positionResult.reset();
|
||||||
|
promise = this.demuxer.findNextPacketStart(positionResult, packet);
|
||||||
|
if (positionResult.pending) await promise;
|
||||||
|
|
||||||
|
const nextPosition = positionResult.value;
|
||||||
if (!nextPosition) {
|
if (!nextPosition) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -908,77 +963,89 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
|||||||
currentSegmentIndex = nextPosition.startSegmentIndex;
|
currentSegmentIndex = nextPosition.startSegmentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastEncodedPacket;
|
return res.set(lastEncodedPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A slower but simpler and sequential algorithm for finding a packet in a file
|
// A slower but simpler and sequential algorithm for finding a packet in a file
|
||||||
async getPacketSequential(timestamp: number, options: PacketRetrievalOptions) {
|
async getPacketSequential(
|
||||||
const release = await this.sequentialScanMutex.acquire(); // Requires exclusivity because we write to a cache
|
res: ResultValue<EncodedPacket | null>,
|
||||||
|
timestamp: number,
|
||||||
|
options: PacketRetrievalOptions,
|
||||||
|
): Promise<Yo> {
|
||||||
|
using lock = this.sequentialScanMutex.lock(); // Requires exclusivity because we write to a cache
|
||||||
|
if (lock.pending) await lock.ready;
|
||||||
|
|
||||||
try {
|
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
||||||
const timestampInSamples = roundIfAlmostInteger(timestamp * this.internalSampleRate);
|
timestamp = timestampInSamples / this.internalSampleRate;
|
||||||
timestamp = timestampInSamples / this.internalSampleRate;
|
|
||||||
|
|
||||||
const index = binarySearchLessOrEqual(
|
const index = binarySearchLessOrEqual(
|
||||||
this.sequentialScanCache,
|
this.sequentialScanCache,
|
||||||
timestampInSamples,
|
timestampInSamples,
|
||||||
x => x.timestampInSamples,
|
x => x.timestampInSamples,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = new ResultValue<EncodedPacket | null>();
|
||||||
|
let currentPacket: EncodedPacket | null;
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
// We don't need to start from the beginning, we can start at a previous scan point
|
||||||
|
const cacheEntry = this.sequentialScanCache[index]!;
|
||||||
|
currentPacket = this.createEncodedPacketFromOggPacket(
|
||||||
|
cacheEntry.packet,
|
||||||
|
{
|
||||||
|
timestampInSamples: cacheEntry.timestampInSamples,
|
||||||
|
vorbisLastBlocksize: cacheEntry.vorbisLastBlockSize,
|
||||||
|
},
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
const promise = this.getFirstPacket(result, options);
|
||||||
|
if (result.pending) await promise;
|
||||||
|
|
||||||
let currentPacket: EncodedPacket | null;
|
currentPacket = result.value;
|
||||||
if (index !== -1) {
|
|
||||||
// We don't need to start from the beginning, we can start at a previous scan point
|
|
||||||
const cacheEntry = this.sequentialScanCache[index]!;
|
|
||||||
currentPacket = this.createEncodedPacketFromOggPacket(
|
|
||||||
cacheEntry.packet,
|
|
||||||
{
|
|
||||||
timestampInSamples: cacheEntry.timestampInSamples,
|
|
||||||
vorbisLastBlocksize: cacheEntry.vorbisLastBlockSize,
|
|
||||||
},
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
currentPacket = await this.getFirstPacket(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (currentPacket && currentPacket.timestamp < timestamp) {
|
|
||||||
const nextPacket = await this.getNextPacket(currentPacket, options);
|
|
||||||
if (!nextPacket || nextPacket.timestamp > timestamp) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPacket = nextPacket;
|
|
||||||
i++;
|
|
||||||
|
|
||||||
if (i === 100) {
|
|
||||||
// Add "checkpoints" every once in a while to speed up subsequent random accesses
|
|
||||||
i = 0;
|
|
||||||
const metadata = this.encodedPacketToMetadata.get(currentPacket);
|
|
||||||
assert(metadata);
|
|
||||||
|
|
||||||
if (this.sequentialScanCache.length > 0) {
|
|
||||||
// If we reach this case, we must be at the end of the cache
|
|
||||||
assert(last(this.sequentialScanCache)!.timestampInSamples <= metadata.timestampInSamples);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sequentialScanCache.push(metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentPacket;
|
|
||||||
} finally {
|
|
||||||
release();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (currentPacket && currentPacket.timestamp < timestamp) {
|
||||||
|
result.reset();
|
||||||
|
const promise = this.getNextPacket(result, currentPacket, options);
|
||||||
|
if (result.pending) await promise;
|
||||||
|
|
||||||
|
const nextPacket = result.value;
|
||||||
|
if (!nextPacket || nextPacket.timestamp > timestamp) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPacket = nextPacket;
|
||||||
|
i++;
|
||||||
|
|
||||||
|
if (i === 100) {
|
||||||
|
// Add "checkpoints" every once in a while to speed up subsequent random accesses
|
||||||
|
i = 0;
|
||||||
|
const metadata = currentPacket._internal as EncodedPacketMetadata | undefined;
|
||||||
|
assert(metadata);
|
||||||
|
|
||||||
|
if (this.sequentialScanCache.length > 0) {
|
||||||
|
// If we reach this case, we must be at the end of the cache
|
||||||
|
assert(last(this.sequentialScanCache)!.timestampInSamples <= metadata.timestampInSamples);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sequentialScanCache.push(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.set(currentPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
getKeyPacket(timestamp: number, options: PacketRetrievalOptions) {
|
getKeyPacket(res: ResultValue<EncodedPacket | null>, timestamp: number, options: PacketRetrievalOptions) {
|
||||||
return this.getPacket(timestamp, options);
|
// Correct since only audio codecs are supported
|
||||||
|
return this.getPacket(res, timestamp, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextKeyPacket(packet: EncodedPacket, options: PacketRetrievalOptions) {
|
getNextKeyPacket(res: ResultValue<EncodedPacket | null>, packet: EncodedPacket, options: PacketRetrievalOptions) {
|
||||||
return this.getNextPacket(packet, options);
|
// Correct since only audio codecs are supported
|
||||||
|
return this.getNextPacket(res, packet, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-3
@@ -3,7 +3,7 @@ import { Input } from '../../src/input.js';
|
|||||||
import { BufferSource, FilePathSource } from '../../src/source.js';
|
import { BufferSource, FilePathSource } from '../../src/source.js';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { ADTS, ALL_FORMATS, MP3, MP4, QTFF, WAVE, WEBM } from '../../src/input-format.js';
|
import { ADTS, ALL_FORMATS, MP3, MP4, OGG, QTFF, WAVE, WEBM } from '../../src/input-format.js';
|
||||||
import { PacketReader } from '../../src/cursors.js';
|
import { PacketReader } from '../../src/cursors.js';
|
||||||
import { InputAudioTrack, InputTrack } from '../../src/input-track.js';
|
import { InputAudioTrack, InputTrack } from '../../src/input-track.js';
|
||||||
import { assert } from '../../src/misc.js';
|
import { assert } from '../../src/misc.js';
|
||||||
@@ -50,6 +50,9 @@ const testBasicPacketReading = async (track: InputTrack) => {
|
|||||||
const testSyncPacketReading = (track: InputTrack) => {
|
const testSyncPacketReading = (track: InputTrack) => {
|
||||||
const reader = new PacketReader(track);
|
const reader = new PacketReader(track);
|
||||||
|
|
||||||
|
const seeked = reader.readAt(1) as EncodedPacket | null;
|
||||||
|
expect(seeked).toBeInstanceOf(EncodedPacket);
|
||||||
|
|
||||||
let current = reader.readFirst() as EncodedPacket | null;
|
let current = reader.readFirst() as EncodedPacket | null;
|
||||||
expect(current).toBeInstanceOf(EncodedPacket);
|
expect(current).toBeInstanceOf(EncodedPacket);
|
||||||
|
|
||||||
@@ -261,7 +264,7 @@ test('ADTS demuxing', async () => {
|
|||||||
await testBasicPacketReading(audioTrack);
|
await testBasicPacketReading(audioTrack);
|
||||||
|
|
||||||
const duration = await audioTrack.computeDuration();
|
const duration = await audioTrack.computeDuration();
|
||||||
expect(duration).toBeGreaterThan(0);
|
expect(duration).toBeCloseTo(4.992);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ADTS sync reading', async () => {
|
test('ADTS sync reading', async () => {
|
||||||
@@ -274,5 +277,42 @@ test('ADTS sync reading', async () => {
|
|||||||
assert(audioTrack);
|
assert(audioTrack);
|
||||||
|
|
||||||
const count = testSyncPacketReading(audioTrack);
|
const count = testSyncPacketReading(audioTrack);
|
||||||
expect(count).toBeGreaterThan(0);
|
expect(count).toBe(234);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ogg demuxing', async () => {
|
||||||
|
using input = new Input({
|
||||||
|
source: new FilePathSource(path.join(__dirname, '../public/beach-party.ogg')),
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await input.getFormat()).toBe(OGG);
|
||||||
|
expect(await input.getMimeType()).toBe('audio/ogg; codecs="vorbis"');
|
||||||
|
|
||||||
|
const tracks = await input.getTracks();
|
||||||
|
expect(tracks).toHaveLength(1);
|
||||||
|
const audioTrack = tracks[0] as InputAudioTrack;
|
||||||
|
expect(audioTrack).toBeInstanceOf(InputAudioTrack);
|
||||||
|
|
||||||
|
expect(audioTrack.codec).toBe('vorbis');
|
||||||
|
expect(audioTrack.numberOfChannels).toBeGreaterThan(0);
|
||||||
|
expect(audioTrack.sampleRate).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await testBasicPacketReading(audioTrack);
|
||||||
|
|
||||||
|
const duration = await audioTrack.computeDuration();
|
||||||
|
expect(duration).toBeCloseTo(57.325714285714284);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ogg sync reading', async () => {
|
||||||
|
using input = new Input({
|
||||||
|
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/beach-party.ogg'))),
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const audioTrack = await input.getPrimaryAudioTrack();
|
||||||
|
assert(audioTrack);
|
||||||
|
|
||||||
|
const count = testSyncPacketReading(audioTrack);
|
||||||
|
expect(count).toBe(5041);
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user