music-metadata
Version:
Music metadata parser for Node.js, supporting virtual any audio and tag format.
144 lines (143 loc) • 7.6 kB
JavaScript
import initDebug from 'debug';
import { BasicParser } from '../common/BasicParser.js';
import { matroskaDtd } from './MatroskaDtd.js';
import { TargetType, TrackType } from './types.js';
import { EbmlIterator, ParseAction } from '../ebml/EbmlIterator.js';
const debug = initDebug('music-metadata:parser:matroska');
/**
* Extensible Binary Meta Language (EBML) parser
* https://en.wikipedia.org/wiki/Extensible_Binary_Meta_Language
* http://matroska.sourceforge.net/technical/specs/rfc/index.html
*
* WEBM VP8 AUDIO FILE
*/
export class MatroskaParser extends BasicParser {
constructor() {
super(...arguments);
this.seekHeadOffset = 0;
/**
* Use index to skip multiple segment/cluster elements at once.
* Significant performance impact
*/
this.flagUseIndexToSkipClusters = this.options.mkvUseIndex ?? false;
}
async parse() {
const containerSize = this.tokenizer.fileInfo.size ?? Number.MAX_SAFE_INTEGER;
const matroskaIterator = new EbmlIterator(this.tokenizer);
debug('Initializing DTD end MatroskaIterator');
await matroskaIterator.iterate(matroskaDtd, containerSize, {
startNext: (element) => {
switch (element.id) {
// case 0x1f43b675: // cluster
case 0x1c53bb6b: // Cueing Data
debug(`Skip element: name=${element.name}, id=0x${element.id.toString(16)}`);
return ParseAction.IgnoreElement;
case 0x1f43b675: // cluster
if (this.flagUseIndexToSkipClusters && this.seekHead) {
const index = this.seekHead.seek.find(index => index.position + this.seekHeadOffset > this.tokenizer.position);
if (index) {
// Go to next index position
const ignoreSize = index.position + this.seekHeadOffset - this.tokenizer.position;
debug(`Use index to go to next position, ignoring ${ignoreSize} bytes`);
this.tokenizer.ignore(ignoreSize);
return ParseAction.SkipElement;
}
}
return ParseAction.IgnoreElement;
default:
return ParseAction.ReadNext;
}
},
elementValue: async (element, value, offset) => {
debug(`Received: name=${element.name}, value=${value}`);
switch (element.id) {
case 0x4282: // docType
this.metadata.setFormat('container', `EBML/${value}`);
break;
case 0x114d9b74:
this.seekHead = value;
this.seekHeadOffset = offset;
break;
case 0x1549a966:
{ // Info (Segment Information)
const info = value;
const timecodeScale = info.timecodeScale ? info.timecodeScale : 1000000;
if (typeof info.duration === 'number') {
const duration = info.duration * timecodeScale / 1000000000;
await this.addTag('segment:title', info.title);
this.metadata.setFormat('duration', Number(duration));
}
}
break;
case 0x1654ae6b:
{ // tracks
const audioTracks = value;
if (audioTracks?.entries) {
audioTracks.entries.forEach(entry => {
const stream = {
codecName: entry.codecID.replace('A_', '').replace('V_', ''),
codecSettings: entry.codecSettings,
flagDefault: entry.flagDefault,
flagLacing: entry.flagLacing,
flagEnabled: entry.flagEnabled,
language: entry.language,
name: entry.name,
type: entry.trackType,
audio: entry.audio,
video: entry.video
};
this.metadata.addStreamInfo(stream);
});
const audioTrack = audioTracks.entries
.filter(entry => entry.trackType === TrackType.audio)
.reduce((acc, cur) => {
if (!acc)
return cur;
if (cur.flagDefault && !acc.flagDefault)
return cur;
if (cur.trackNumber < acc.trackNumber)
return cur;
return acc;
}, null);
if (audioTrack) {
this.metadata.setFormat('codec', audioTrack.codecID.replace('A_', ''));
this.metadata.setFormat('sampleRate', audioTrack.audio.samplingFrequency);
this.metadata.setFormat('numberOfChannels', audioTrack.audio.channels);
}
}
}
break;
case 0x1254c367:
{ // tags
const tags = value;
await Promise.all(tags.tag.map(async (tag) => {
const target = tag.target;
const targetType = target?.targetTypeValue ? TargetType[target.targetTypeValue] : (target?.targetType ? target.targetType : 'track');
await Promise.all(tag.simpleTags.map(async (simpleTag) => {
const value = simpleTag.string ? simpleTag.string : simpleTag.binary;
await this.addTag(`${targetType}:${simpleTag.name}`, value);
}));
}));
}
break;
case 0x1941a469:
{ // attachments
const attachments = value;
await Promise.all(attachments.attachedFiles
.filter(file => file.mimeType.startsWith('image/'))
.map(file => this.addTag('picture', {
data: file.data,
format: file.mimeType,
description: file.description,
name: file.name
})));
}
break;
}
}
});
}
async addTag(tagId, value) {
await this.metadata.addTag('matroska', tagId, value);
}
}