Migrate Ogg demuxer to new packet system, improve Ogg demuxer performance

This commit is contained in:
Vanilagy
2025-12-22 22:52:26 +01:00
parent 0768f6137c
commit bc1511a201
4 changed files with 249 additions and 142 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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.