1
0
mirror of synced 2026-08-05 19:16:55 +00:00
Files
music-metadata/lib/ogg/OggParser.ts
Borewit 9f5f545136 Fix Ogg/Opus duration calculation.
Duration calculation requires to parse the entire Ogg/Opus file.
2025-11-15 19:24:15 +01:00

145 lines
5.3 KiB
TypeScript

import * as Token from 'token-types';
import { EndOfStreamError, type ITokenizer } from 'strtok3';
import initDebug from 'debug';
import { BasicParser } from '../common/BasicParser.js';
import { VorbisStream } from './vorbis/VorbisStream.js';
import { OpusStream } from './opus/OpusStream.js';
import { SpeexStream } from './speex/SpeexStream.js';
import { TheoraStream } from './theora/TheoraStream.js';
import type * as Ogg from './OggToken.js';
import { makeUnexpectedFileContentError } from '../ParseError.js';
import { type IPageConsumer, type IPageHeader, PageHeader, SegmentTable } from './OggToken.js';
import type { INativeMetadataCollector } from '../common/MetadataCollector.js';
import type { IOptions } from '../type.js';
import { FlacStream } from './flac/FlacStream.js';
export class OggContentError extends makeUnexpectedFileContentError('Ogg'){
}
const debug = initDebug('music-metadata:parser:ogg');
class OggStream {
private metadata: INativeMetadataCollector;
public streamSerial: number;
public pageNumber = 0;
public closed = false;
private options: IOptions;
public pageConsumer?: IPageConsumer;
constructor(metadata: INativeMetadataCollector, streamSerial: number, options: IOptions) {
this.metadata = metadata;
this.streamSerial = streamSerial;
this.options = options;
}
public async parsePage(tokenizer: ITokenizer, header: IPageHeader) {
this.pageNumber = header.pageSequenceNo;
debug('serial=%s page#=%s, Ogg.id=%s', header.streamSerialNumber, header.pageSequenceNo, header.capturePattern);
const segmentTable = await tokenizer.readToken<Ogg.ISegmentTable>(new SegmentTable(header));
debug('totalPageSize=%s', segmentTable.totalPageSize);
const pageData = await tokenizer.readToken<Uint8Array>(new Token.Uint8ArrayType(segmentTable.totalPageSize));
debug('firstPage=%s, lastPage=%s, continued=%s', header.headerType.firstPage, header.headerType.lastPage, header.headerType.continued);
if (header.headerType.firstPage) {
this.metadata.setFormat('container', 'Ogg');
const idData = pageData.subarray(0, 7); // Copy this portion
const asciiId = Array.from(idData)
.filter(b => b >= 32 && b <= 126) // Keep only printable ASCII
.map(b => String.fromCharCode(b))
.join('');
switch (asciiId) {
case 'vorbis': // Ogg/Vorbis
debug(`Set Ogg stream serial ${header.streamSerialNumber}, codec=Vorbis`);
this.pageConsumer = new VorbisStream(this.metadata, this.options);
break;
case 'OpusHea': // Ogg/Opus
debug('Set page consumer to Ogg/Opus');
this.pageConsumer = new OpusStream(this.metadata, this.options, tokenizer);
break;
case 'Speex ': // Ogg/Speex
debug('Set page consumer to Ogg/Speex');
this.pageConsumer = new SpeexStream(this.metadata, this.options, tokenizer);
break;
case 'fishead':
case 'theora': // Ogg/Theora
debug('Set page consumer to Ogg/Theora');
this.pageConsumer = new TheoraStream(this.metadata, this.options, tokenizer);
break;
case 'FLAC': // Ogg/Theora
debug('Set page consumer to Vorbis');
this.pageConsumer = new FlacStream(this.metadata, this.options, tokenizer);
break;
default:
throw new OggContentError(`Ogg codec not recognized (id=${asciiId}`);
}
}
if (header.headerType.lastPage) {
this.closed = true;
}
if (this.pageConsumer) {
await this.pageConsumer.parsePage(header, pageData);
} else throw new Error('pageConsumer should be initialized');
}
}
/**
* Parser for Ogg logical bitstream framing
*/
export class OggParser extends BasicParser {
private streams = new Map<number, OggStream>();
/**
* Parse page
* @returns {Promise<void>}
*/
public async parse(): Promise<void> {
this.streams = new Map<number, OggStream>();
let enfOfStream = false;
let header: IPageHeader;
try {
do {
header = await this.tokenizer.readToken<IPageHeader>(PageHeader);
if (header.capturePattern !== 'OggS') throw new OggContentError('Invalid Ogg capture pattern');
let stream = this.streams.get(header.streamSerialNumber);
if (!stream) {
stream = new OggStream(this.metadata, header.streamSerialNumber, this.options);
this.streams.set(header.streamSerialNumber, stream);
}
await stream.parsePage(this.tokenizer, header);
if (stream.pageNumber > 12 && !(this.options.duration && [...this.streams.values()].find(stream => stream.pageConsumer?.durationOnLastPage)) ) {
debug("Stop processing Ogg stream");
break;
}
} while (![...this.streams.values()].every(item => item.closed));
} catch(err) {
if (err instanceof EndOfStreamError) {
debug("Reached end-of-stream");
enfOfStream = true;
} else if (err instanceof OggContentError) {
this.metadata.addWarning(`Corrupt Ogg content at ${this.tokenizer.position}`);
} else throw err;
}
for (const stream of this.streams.values()) {
if (!stream.closed) {
this.metadata.addWarning(`End-of-stream reached before reaching last page in Ogg stream serial=${stream.streamSerial}`);
await stream.pageConsumer?.flush();
}
stream.pageConsumer?.calculateDuration(enfOfStream);
}
}
}