Migrate FLAC demuxer to new packet system, move computeDuration impl to Input

This commit is contained in:
Vanilagy
2025-12-22 23:16:05 +01:00
parent bc1511a201
commit 4f8c8836ec
4 changed files with 172 additions and 126 deletions
-11
View File
@@ -20,15 +20,4 @@ export abstract class Demuxer {
abstract getTracks(): Promise<InputTrack[]>; abstract getTracks(): Promise<InputTrack[]>;
abstract getMimeType(): Promise<string>; abstract getMimeType(): Promise<string>;
abstract getMetadataTags(): Promise<MetadataTags>; abstract getMetadataTags(): Promise<MetadataTags>;
async computeDuration(): Promise<number> {
const tracks = await this.getTracks();
if (tracks.length === 0) {
return 0;
}
// eslint-disable-next-line @typescript-eslint/await-thenable
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
return Math.max(...trackDurations);
}
} }
+77 -62
View File
@@ -13,11 +13,13 @@ import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { PacketRetrievalOptions } from '../media-sink'; import { PacketRetrievalOptions } from '../media-sink';
import { import {
assert, assert,
AsyncMutex, AsyncMutex4,
binarySearchLessOrEqual, binarySearchLessOrEqual,
Bitstream, Bitstream,
ResultValue,
textDecoder, textDecoder,
UNDETERMINED_LANGUAGE, UNDETERMINED_LANGUAGE,
Yo,
} from '../misc'; } from '../misc';
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet'; import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
import { import {
@@ -77,7 +79,7 @@ export class FlacDemuxer extends Demuxer {
lastLoadedPos: number | null = null; lastLoadedPos: number | null = null;
blockingBit: number | null = null; blockingBit: number | null = null;
readingMutex = new AsyncMutex(); readingMutex = new AsyncMutex4();
lastSampleLoaded = false; lastSampleLoaded = false;
constructor(input: Input) { constructor(input: Input) {
@@ -86,12 +88,6 @@ export class FlacDemuxer extends Demuxer {
this.reader = input._reader; this.reader = input._reader;
} }
override async computeDuration(): Promise<number> {
await this.readMetadata();
assert(this.track);
return this.track.computeDuration();
}
override async getMetadataTags(): Promise<MetadataTags> { override async getMetadataTags(): Promise<MetadataTags> {
await this.readMetadata(); await this.readMetadata();
return this.metadataTags; return this.metadataTags;
@@ -108,9 +104,9 @@ export class FlacDemuxer extends Demuxer {
} }
async readMetadata() { async readMetadata() {
return (this.metadataPromise ??= (async () => {
let currentPos = 4; // Skip 'fLaC' let currentPos = 4; // Skip 'fLaC'
return (this.metadataPromise ??= (async () => {
while ( while (
this.reader.fileSize === null this.reader.fileSize === null
|| currentPos < this.reader.fileSize || currentPos < this.reader.fileSize
@@ -261,13 +257,16 @@ export class FlacDemuxer extends Demuxer {
})()); })());
} }
async readNextFlacFrame({ async readNextFlacFrame(
res: ResultValue<NextFlacFrameResult | null>,
{
startPos, startPos,
isFirstPacket, isFirstPacket,
}: { }: {
startPos: number; startPos: number;
isFirstPacket: boolean; isFirstPacket: boolean;
}): Promise<NextFlacFrameResult | null> { },
): Promise<Yo> {
assert(this.audioInfo); assert(this.audioInfo);
// we expect that there are at least `minimumFrameSize` bytes left in the file // we expect that there are at least `minimumFrameSize` bytes left in the file
@@ -287,14 +286,15 @@ export class FlacDemuxer extends Demuxer {
const maximumSliceLength const maximumSliceLength
= this.audioInfo.maximumFrameSize + maximumHeaderSize; = this.audioInfo.maximumFrameSize + maximumHeaderSize;
const slice = await this.reader.requestSliceRange( let slice = this.reader.requestSliceRange(
startPos, startPos,
this.audioInfo.minimumFrameSize, this.audioInfo.minimumFrameSize,
maximumSliceLength, maximumSliceLength,
); );
if (slice instanceof Promise) slice = await slice;
if (!slice) { if (!slice) {
return null; return res.set(null);
} }
const frameHeader = this.readFlacFrameHeader({ const frameHeader = this.readFlacFrameHeader({
@@ -303,7 +303,7 @@ export class FlacDemuxer extends Demuxer {
}); });
if (!frameHeader) { if (!frameHeader) {
return null; return res.set(null);
} }
// We don't know exactly how long the packet is, we only know the `minimumFrameSize` and `maximumFrameSize` // We don't know exactly how long the packet is, we only know the `minimumFrameSize` and `maximumFrameSize`
@@ -317,13 +317,13 @@ export class FlacDemuxer extends Demuxer {
while (true) { while (true) {
// Reached end of the file, packet is over // Reached end of the file, packet is over
if (slice.filePos > slice.end - minimumHeaderLength) { if (slice.filePos > slice.end - minimumHeaderLength) {
return { return res.set({
num: frameHeader.num, num: frameHeader.num,
blockSize: frameHeader.blockSize, blockSize: frameHeader.blockSize,
sampleRate: frameHeader.sampleRate, sampleRate: frameHeader.sampleRate,
size: slice.end - startPos, size: slice.end - startPos,
isLastFrame: true, isLastFrame: true,
}; });
} }
const nextByte = readU8(slice); const nextByte = readU8(slice);
@@ -369,13 +369,13 @@ export class FlacDemuxer extends Demuxer {
} }
} }
return { return res.set({
num: frameHeader.num, num: frameHeader.num,
blockSize: frameHeader.blockSize, blockSize: frameHeader.blockSize,
sampleRate: frameHeader.sampleRate, sampleRate: frameHeader.sampleRate,
size: lengthIfNextFlacFrameHeaderIsLegit, size: lengthIfNextFlacFrameHeaderIsLegit,
isLastFrame: false, isLastFrame: false,
}; });
} }
} }
} }
@@ -483,21 +483,24 @@ export class FlacDemuxer extends Demuxer {
return { num, blockSize, sampleRate }; return { num, blockSize, sampleRate };
} }
async advanceReader() { async advanceReader(res: ResultValue<void>): Promise<Yo> {
await this.readMetadata();
assert(this.lastLoadedPos !== null); assert(this.lastLoadedPos !== null);
assert(this.audioInfo); assert(this.audioInfo);
const startPos = this.lastLoadedPos; const startPos = this.lastLoadedPos;
const frame = await this.readNextFlacFrame({
const frameResult = new ResultValue<NextFlacFrameResult | null>();
const promise = this.readNextFlacFrame(frameResult, {
startPos, startPos,
isFirstPacket: this.loadedSamples.length === 0, isFirstPacket: this.loadedSamples.length === 0,
}); });
if (frameResult.pending) await promise;
const frame = frameResult.value;
if (!frame) { if (!frame) {
// Unexpected case, failed to read next FLAC frame // Unexpected case, failed to read next FLAC frame
// handling gracefully // handling gracefully
this.lastSampleLoaded = true; this.lastSampleLoaded = true;
return; return res.set();
} }
const lastSample = this.loadedSamples[this.loadedSamples.length - 1]; const lastSample = this.loadedSamples[this.loadedSamples.length - 1];
@@ -517,8 +520,9 @@ export class FlacDemuxer extends Demuxer {
if (frame.isLastFrame) { if (frame.isLastFrame) {
this.lastSampleLoaded = true; this.lastSampleLoaded = true;
return;
} }
return res.set();
} }
} }
@@ -542,11 +546,6 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
return this.demuxer.audioInfo.numberOfChannels; return this.demuxer.audioInfo.numberOfChannels;
} }
async computeDuration() {
const lastPacket = await this.getPacket(Infinity, { metadataOnly: true });
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
}
getSampleRate() { getSampleRate() {
assert(this.demuxer.audioInfo); assert(this.demuxer.audioInfo);
return this.demuxer.audioInfo.sampleRate; return this.demuxer.audioInfo.sampleRate;
@@ -571,10 +570,6 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
}; };
} }
async getFirstTimestamp() {
return 0;
}
async getDecoderConfig(): Promise<AudioDecoderConfig | null> { async getDecoderConfig(): Promise<AudioDecoderConfig | null> {
assert(this.demuxer.audioInfo); assert(this.demuxer.audioInfo);
@@ -587,17 +582,20 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
} }
async getPacket( async getPacket(
res: ResultValue<EncodedPacket | null>,
timestamp: number, timestamp: number,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
assert(this.demuxer.audioInfo); assert(this.demuxer.audioInfo);
if (timestamp < 0) { if (timestamp < 0) {
throw new Error('Timestamp cannot be negative'); throw new Error('Timestamp cannot be negative');
} }
const release = await this.demuxer.readingMutex.acquire(); using lock = this.demuxer.readingMutex.lock();
if (lock.pending) await lock.ready;
const advanceResult = new ResultValue<void>();
try {
while (true) { while (true) {
const packetIndex = binarySearchLessOrEqual( const packetIndex = binarySearchLessOrEqual(
this.demuxer.loadedSamples, this.demuxer.loadedSamples,
@@ -605,7 +603,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
x => x.blockOffset / this.demuxer.audioInfo!.sampleRate, x => x.blockOffset / this.demuxer.audioInfo!.sampleRate,
); );
if (packetIndex === -1) { if (packetIndex === -1) {
await this.demuxer.advanceReader(); advanceResult.reset();
const promise = this.demuxer.advanceReader(advanceResult);
if (advanceResult.pending) await promise;
continue; continue;
} }
@@ -618,70 +619,78 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
if (sampleTimestamp + sampleDuration <= timestamp) { if (sampleTimestamp + sampleDuration <= timestamp) {
if (this.demuxer.lastSampleLoaded) { if (this.demuxer.lastSampleLoaded) {
return this.getPacketAtIndex( return this.getPacketAtIndex(
res,
this.demuxer.loadedSamples.length - 1, this.demuxer.loadedSamples.length - 1,
options, options,
); );
} }
await this.demuxer.advanceReader(); advanceResult.reset();
const promise = this.demuxer.advanceReader(advanceResult);
if (advanceResult.pending) await promise;
continue; continue;
} }
return this.getPacketAtIndex(packetIndex, options); return this.getPacketAtIndex(res, packetIndex, options);
}
} finally {
release();
} }
} }
async getNextPacket( async getNextPacket(
res: ResultValue<EncodedPacket | null>,
packet: EncodedPacket, packet: EncodedPacket,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
const release = await this.demuxer.readingMutex.acquire(); using lock = this.demuxer.readingMutex.lock();
try { if (lock.pending) await lock.ready;
const nextIndex = packet.sequenceNumber + 1; const nextIndex = packet.sequenceNumber + 1;
if ( if (
this.demuxer.lastSampleLoaded this.demuxer.lastSampleLoaded
&& nextIndex >= this.demuxer.loadedSamples.length && nextIndex >= this.demuxer.loadedSamples.length
) { ) {
return null; return res.set(null);
} }
const advanceResult = new ResultValue<void>();
// Ensure the next sample exists // Ensure the next sample exists
while ( while (
nextIndex >= this.demuxer.loadedSamples.length nextIndex >= this.demuxer.loadedSamples.length
&& !this.demuxer.lastSampleLoaded && !this.demuxer.lastSampleLoaded
) { ) {
await this.demuxer.advanceReader(); advanceResult.reset();
} const promise = this.demuxer.advanceReader(advanceResult);
return this.getPacketAtIndex(nextIndex, options); if (advanceResult.pending) await promise;
} finally {
release();
} }
return this.getPacketAtIndex(res, nextIndex, options);
} }
getKeyPacket( getKeyPacket(
res: ResultValue<EncodedPacket | null>,
timestamp: number, timestamp: number,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
return this.getPacket(timestamp, options); return this.getPacket(res, timestamp, options);
} }
getNextKeyPacket( getNextKeyPacket(
res: ResultValue<EncodedPacket | null>,
packet: EncodedPacket, packet: EncodedPacket,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
return this.getNextPacket(packet, options); return this.getNextPacket(res, packet, options);
} }
async getPacketAtIndex( async getPacketAtIndex(
res: ResultValue<EncodedPacket | null>,
sampleIndex: number, sampleIndex: number,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
const rawSample = this.demuxer.loadedSamples[sampleIndex]; const rawSample = this.demuxer.loadedSamples[sampleIndex];
if (!rawSample) { if (!rawSample) {
return null; return res.set(null);
} }
let data: Uint8Array; let data: Uint8Array;
@@ -695,7 +704,7 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
if (slice instanceof Promise) slice = await slice; if (slice instanceof Promise) slice = await slice;
if (!slice) { if (!slice) {
return null; // Data didn't fit into the rest of the file return res.set(null); // Data didn't fit into the rest of the file
} }
data = readBytes(slice, rawSample.byteSize); data = readBytes(slice, rawSample.byteSize);
@@ -704,27 +713,33 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
assert(this.demuxer.audioInfo); assert(this.demuxer.audioInfo);
const timestamp = rawSample.blockOffset / this.demuxer.audioInfo.sampleRate; const timestamp = rawSample.blockOffset / this.demuxer.audioInfo.sampleRate;
const duration = rawSample.blockSize / this.demuxer.audioInfo.sampleRate; const duration = rawSample.blockSize / this.demuxer.audioInfo.sampleRate;
return new EncodedPacket(
return res.set(new EncodedPacket(
data, data,
'key', 'key',
timestamp, timestamp,
duration, duration,
sampleIndex, sampleIndex,
rawSample.byteSize, rawSample.byteSize,
); ));
} }
async getFirstPacket( async getFirstPacket(
res: ResultValue<EncodedPacket | null>,
options: PacketRetrievalOptions, options: PacketRetrievalOptions,
): Promise<EncodedPacket | null> { ): Promise<Yo> {
const advanceResult = new ResultValue<void>();
// Ensure the next sample exists // Ensure the next sample exists
while ( while (
this.demuxer.loadedSamples.length === 0 this.demuxer.loadedSamples.length === 0
&& !this.demuxer.lastSampleLoaded && !this.demuxer.lastSampleLoaded
) { ) {
await this.demuxer.advanceReader(); advanceResult.reset();
const promise = this.demuxer.advanceReader(advanceResult);
if (advanceResult.pending) await promise;
} }
return this.getPacketAtIndex(0, options); return this.getPacketAtIndex(res, 0, options);
} }
} }
+7 -2
View File
@@ -118,8 +118,13 @@ export class Input<S extends Source = Source> implements Disposable {
* all tracks. * all tracks.
*/ */
async computeDuration() { async computeDuration() {
const demuxer = await this._getDemuxer(); const tracks = await this.getTracks();
return demuxer.computeDuration(); if (tracks.length === 0) {
return 0;
}
const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
return Math.max(...trackDurations);
} }
/** Returns the list of all tracks of this input file. */ /** Returns the list of all tracks of this input file. */
+51 -14
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, OGG, QTFF, WAVE, WEBM } from '../../src/input-format.js'; import { ADTS, ALL_FORMATS, FLAC, 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';
@@ -88,6 +88,19 @@ test('Regular ISOBMFF demuxing', async () => {
expect(await input.computeDuration()).toBeCloseTo(5.041666666666667); expect(await input.computeDuration()).toBeCloseTo(5.041666666666667);
}); });
test('Regular ISOBMFF sync reading', async () => {
using input = new Input({
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/trim-buck-bunny.mov'))),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
const count = testSyncPacketReading(videoTrack);
expect(count).toBe(121);
});
test('Fragmented ISOBMFF demuxing', async () => { test('Fragmented ISOBMFF demuxing', async () => {
using input = new Input({ using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/frag-buck-bunny.mp4')), source: new FilePathSource(path.join(__dirname, '../public/frag-buck-bunny.mp4')),
@@ -108,19 +121,6 @@ test('Fragmented ISOBMFF demuxing', async () => {
expect(await input.computeDuration()).toBeCloseTo(5); expect(await input.computeDuration()).toBeCloseTo(5);
}); });
test('Regular ISOBMFF sync reading', async () => {
using input = new Input({
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/trim-buck-bunny.mov'))),
formats: ALL_FORMATS,
});
const videoTrack = await input.getPrimaryVideoTrack();
assert(videoTrack);
const count = testSyncPacketReading(videoTrack);
expect(count).toBe(121);
});
test('Fragmented ISOBMFF sync reading', async () => { test('Fragmented ISOBMFF sync reading', async () => {
using input = new Input({ using input = new Input({
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/frag-buck-bunny.mp4'))), source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/frag-buck-bunny.mp4'))),
@@ -316,3 +316,40 @@ test('Ogg sync reading', async () => {
const count = testSyncPacketReading(audioTrack); const count = testSyncPacketReading(audioTrack);
expect(count).toBe(5041); expect(count).toBe(5041);
}); });
test('FLAC demuxing', async () => {
using input = new Input({
source: new FilePathSource(path.join(__dirname, '../public/sample.flac')),
formats: ALL_FORMATS,
});
expect(await input.getFormat()).toBe(FLAC);
expect(await input.getMimeType()).toBe('audio/flac');
const tracks = await input.getTracks();
expect(tracks).toHaveLength(1);
const audioTrack = tracks[0] as InputAudioTrack;
expect(audioTrack).toBeInstanceOf(InputAudioTrack);
expect(audioTrack.codec).toBe('flac');
expect(audioTrack.numberOfChannels).toBeGreaterThan(0);
expect(audioTrack.sampleRate).toBeGreaterThan(0);
await testBasicPacketReading(audioTrack);
const duration = await audioTrack.computeDuration();
expect(duration).toBeCloseTo(19.71428571428571);
});
test('FLAC sync reading', async () => {
using input = new Input({
source: new BufferSource(fs.readFileSync(path.join(__dirname, '../public/sample.flac'))),
formats: ALL_FORMATS,
});
const audioTrack = await input.getPrimaryAudioTrack();
assert(audioTrack);
const count = testSyncPacketReading(audioTrack);
expect(count).toBe(213);
});