securityos/node_modules/music-metadata/lib/matroska/MatroskaParser.js

238 lines
9.7 KiB
JavaScript
Raw Normal View History

2024-09-06 15:32:35 +00:00
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MatroskaParser = void 0;
const token_types_1 = require("token-types");
const debug_1 = require("debug");
const BasicParser_1 = require("../common/BasicParser");
const types_1 = require("./types");
const matroskaDtd = require("./MatroskaDtd");
const debug = (0, debug_1.default)('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
*/
class MatroskaParser extends BasicParser_1.BasicParser {
constructor() {
super();
this.padding = 0;
this.parserMap = new Map();
this.ebmlMaxIDLength = 4;
this.ebmlMaxSizeLength = 8;
this.parserMap.set(types_1.DataType.uint, e => this.readUint(e));
this.parserMap.set(types_1.DataType.string, e => this.readString(e));
this.parserMap.set(types_1.DataType.binary, e => this.readBuffer(e));
this.parserMap.set(types_1.DataType.uid, async (e) => await this.readUint(e) === 1);
this.parserMap.set(types_1.DataType.bool, e => this.readFlag(e));
this.parserMap.set(types_1.DataType.float, e => this.readFloat(e));
}
/**
* Initialize parser with output (metadata), input (tokenizer) & parsing options (options).
* @param {INativeMetadataCollector} metadata Output
* @param {ITokenizer} tokenizer Input
* @param {IOptions} options Parsing options
*/
init(metadata, tokenizer, options) {
super.init(metadata, tokenizer, options);
return this;
}
async parse() {
const matroska = await this.parseContainer(matroskaDtd.elements, this.tokenizer.fileInfo.size, []);
this.metadata.setFormat('container', `EBML/${matroska.ebml.docType}`);
if (matroska.segment) {
const info = matroska.segment.info;
if (info) {
const timecodeScale = info.timecodeScale ? info.timecodeScale : 1000000;
if (typeof info.duration === 'number') {
const duration = info.duration * timecodeScale / 1000000000;
this.addTag('segment:title', info.title);
this.metadata.setFormat('duration', duration);
}
}
const audioTracks = matroska.segment.tracks;
if (audioTracks && 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 => {
return entry.trackType === types_1.TrackType.audio.valueOf();
})
.reduce((acc, cur) => {
if (!acc) {
return cur;
}
if (!acc.flagDefault && cur.flagDefault) {
return cur;
}
if (cur.trackNumber && 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);
}
if (matroska.segment.tags) {
matroska.segment.tags.tag.forEach(tag => {
const target = tag.target;
const targetType = (target === null || target === void 0 ? void 0 : target.targetTypeValue) ? types_1.TargetType[target.targetTypeValue] : ((target === null || target === void 0 ? void 0 : target.targetType) ? target.targetType : 'track');
tag.simpleTags.forEach(simpleTag => {
const value = simpleTag.string ? simpleTag.string : simpleTag.binary;
this.addTag(`${targetType}:${simpleTag.name}`, value);
});
});
}
if (matroska.segment.attachments) {
matroska.segment.attachments.attachedFiles
.filter(file => file.mimeType.startsWith('image/'))
.map(file => {
return {
data: file.data,
format: file.mimeType,
description: file.description,
name: file.name
};
}).forEach(picture => {
this.addTag('picture', picture);
});
}
}
}
}
async parseContainer(container, posDone, path) {
const tree = {};
while (this.tokenizer.position < posDone) {
let element;
try {
element = await this.readElement();
}
catch (error) {
if (error.message === 'End-Of-Stream') {
break;
}
throw error;
}
const type = container[element.id];
if (type) {
debug(`Element: name=${type.name}, container=${!!type.container}`);
if (type.container) {
const res = await this.parseContainer(type.container, element.len >= 0 ? this.tokenizer.position + element.len : -1, path.concat([type.name]));
if (type.multiple) {
if (!tree[type.name]) {
tree[type.name] = [];
}
tree[type.name].push(res);
}
else {
tree[type.name] = res;
}
}
else {
tree[type.name] = await this.parserMap.get(type.value)(element);
}
}
else {
switch (element.id) {
case 0xec: // void
this.padding += element.len;
await this.tokenizer.ignore(element.len);
break;
default:
debug(`parseEbml: path=${path.join('/')}, unknown element: id=${element.id.toString(16)}`);
this.padding += element.len;
await this.tokenizer.ignore(element.len);
}
}
}
return tree;
}
async readVintData(maxLength) {
const msb = await this.tokenizer.peekNumber(token_types_1.UINT8);
let mask = 0x80;
let oc = 1;
// Calculate VINT_WIDTH
while ((msb & mask) === 0) {
if (oc > maxLength) {
throw new Error('VINT value exceeding maximum size');
}
++oc;
mask >>= 1;
}
const id = Buffer.alloc(oc);
await this.tokenizer.readBuffer(id);
return id;
}
async readElement() {
const id = await this.readVintData(this.ebmlMaxIDLength);
const lenField = await this.readVintData(this.ebmlMaxSizeLength);
lenField[0] ^= 0x80 >> (lenField.length - 1);
const nrLen = Math.min(6, lenField.length); // JavaScript can max read 6 bytes integer
return {
id: id.readUIntBE(0, id.length),
len: lenField.readUIntBE(lenField.length - nrLen, nrLen)
};
}
isMaxValue(vintData) {
if (vintData.length === this.ebmlMaxSizeLength) {
for (let n = 1; n < this.ebmlMaxSizeLength; ++n) {
if (vintData[n] !== 0xff)
return false;
}
return true;
}
return false;
}
async readFloat(e) {
switch (e.len) {
case 0:
return 0.0;
case 4:
return this.tokenizer.readNumber(token_types_1.Float32_BE);
case 8:
return this.tokenizer.readNumber(token_types_1.Float64_BE);
case 10:
return this.tokenizer.readNumber(token_types_1.Float64_BE);
default:
throw new Error(`Invalid IEEE-754 float length: ${e.len}`);
}
}
async readFlag(e) {
return (await this.readUint(e)) === 1;
}
async readUint(e) {
const buf = await this.readBuffer(e);
const nrLen = Math.min(6, e.len); // JavaScript can max read 6 bytes integer
return buf.readUIntBE(e.len - nrLen, nrLen);
}
async readString(e) {
const rawString = await this.tokenizer.readToken(new token_types_1.StringType(e.len, 'utf-8'));
return rawString.replace(/\00.*$/g, '');
}
async readBuffer(e) {
const buf = Buffer.alloc(e.len);
await this.tokenizer.readBuffer(buf);
return buf;
}
addTag(tagId, value) {
this.metadata.addTag('matroska', tagId, value);
}
}
exports.MatroskaParser = MatroskaParser;