openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,076 lines (1,075 loc) • 36.3 kB
JavaScript
import { o as __toESM } from "./chunk-CNf5ZN-e.js";
import { t as require_src } from "./src-6PhJB9jV.js";
import { n as TrackType } from "./types-Dw42w_cH.js";
import { C as UINT64_BE, D as lib_exports, E as Uint8ArrayType, O as textDecode, T as UINT8, _ as UINT16_BE, d as INT32_BE, g as StringType, h as INT8, l as INT16_BE, o as makeUnexpectedFileContentError, t as BasicParser, u as INT24_BE, x as UINT32_BE, y as UINT24_BE } from "./BasicParser-CSYy5xVJ.js";
import { a as getBit, d as uint8ArrayToHex } from "./Util--vOO_7DL.js";
import { i as FourCcToken } from "./APEv2Parser-Drjv2OIw.js";
import { t as Genres } from "./ID3v1Parser-BDoQ48nd.js";
//#region node_modules/music-metadata/lib/mp4/AtomToken.js
var import_src = /* @__PURE__ */ __toESM(require_src(), 1);
const debug$2 = (0, import_src.default)("music-metadata:parser:MP4:atom");
var Mp4ContentError = class extends makeUnexpectedFileContentError("MP4") {};
const Header = {
len: 8,
get: (buf, off) => {
const length = UINT32_BE.get(buf, off);
if (length < 0) throw new Mp4ContentError("Invalid atom header length");
return {
length: BigInt(length),
name: new StringType(4, "latin1").get(buf, off + 4)
};
},
put: (buf, off, hdr) => {
UINT32_BE.put(buf, off, Number(hdr.length));
return FourCcToken.put(buf, off + 4, hdr.name);
}
};
/**
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html#//apple_ref/doc/uid/TP40000939-CH203-38190
*/
const ExtendedSize = UINT64_BE;
const ftyp = {
len: 4,
get: (buf, off) => {
return { type: new StringType(4, "ascii").get(buf, off) };
}
};
/**
* Base class for 'fixed' length atoms.
* In some cases these atoms are longer then the sum of the described fields.
* Issue: https://github.com/Borewit/music-metadata/issues/120
*/
var FixedLengthAtom = class {
/**
*
* @param {number} len Length as specified in the size field
* @param {number} expLen Total length of sum of specified fields in the standard
* @param atomId Atom ID
*/
constructor(len, expLen, atomId) {
if (len < expLen) throw new Mp4ContentError(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`);
if (len > expLen) debug$2(`Warning: atom ${atomId} expected to be ${expLen}, but was actually ${len} bytes long.`);
this.len = len;
}
};
/**
* Timestamp stored in seconds since Mac Epoch (1 January 1904)
*/
const SecondsSinceMacEpoch = {
len: 4,
get: (buf, off) => {
const secondsSinceUnixEpoch = UINT32_BE.get(buf, off) - 2082844800;
return /* @__PURE__ */ new Date(secondsSinceUnixEpoch * 1e3);
}
};
/**
* Token: Media Header Atom
* Ref:
* - https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-SW34
* - https://wiki.multimedia.cx/index.php/QuickTime_container#mdhd
*/
var MdhdAtom = class extends FixedLengthAtom {
constructor(len) {
super(len, 24, "mdhd");
}
get(buf, off) {
return {
version: UINT8.get(buf, off + 0),
flags: UINT24_BE.get(buf, off + 1),
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
timeScale: UINT32_BE.get(buf, off + 12),
duration: UINT32_BE.get(buf, off + 16),
language: UINT16_BE.get(buf, off + 20),
quality: UINT16_BE.get(buf, off + 22)
};
}
};
/**
* Token: Movie Header Atom
*/
var MvhdAtom = class extends FixedLengthAtom {
constructor(len) {
super(len, 100, "mvhd");
}
get(buf, off) {
return {
version: UINT8.get(buf, off),
flags: UINT24_BE.get(buf, off + 1),
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
timeScale: UINT32_BE.get(buf, off + 12),
duration: UINT32_BE.get(buf, off + 16),
preferredRate: UINT32_BE.get(buf, off + 20),
preferredVolume: UINT16_BE.get(buf, off + 24),
previewTime: UINT32_BE.get(buf, off + 72),
previewDuration: UINT32_BE.get(buf, off + 76),
posterTime: UINT32_BE.get(buf, off + 80),
selectionTime: UINT32_BE.get(buf, off + 84),
selectionDuration: UINT32_BE.get(buf, off + 88),
currentTime: UINT32_BE.get(buf, off + 92),
nextTrackID: UINT32_BE.get(buf, off + 96)
};
}
};
/**
* Data Atom Structure
*/
var DataAtom = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
return {
type: {
set: UINT8.get(buf, off + 0),
type: UINT24_BE.get(buf, off + 1)
},
locale: UINT24_BE.get(buf, off + 4),
value: new Uint8ArrayType(this.len - 8).get(buf, off + 8)
};
}
};
/**
* Data Atom Structure
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW31
*/
var NameAtom = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
return {
version: UINT8.get(buf, off),
flags: UINT24_BE.get(buf, off + 1),
name: new StringType(this.len - 4, "utf-8").get(buf, off + 4)
};
}
};
/**
* Track Header Atoms structure (`tkhd`)
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
*/
var TrackHeaderAtom = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
return {
version: UINT8.get(buf, off),
flags: UINT24_BE.get(buf, off + 1),
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
trackId: UINT32_BE.get(buf, off + 12),
duration: UINT32_BE.get(buf, off + 20),
layer: UINT16_BE.get(buf, off + 24),
alternateGroup: UINT16_BE.get(buf, off + 26),
volume: UINT16_BE.get(buf, off + 28)
};
}
};
/**
* Atom: Sample Description Atom ('stsd')
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
*/
const stsdHeader = {
len: 8,
get: (buf, off) => {
return {
version: UINT8.get(buf, off),
flags: UINT24_BE.get(buf, off + 1),
numberOfEntries: UINT32_BE.get(buf, off + 4)
};
}
};
/**
* Atom: Sample Description Atom ('stsd')
* Ref: https://developer.apple.com/documentation/quicktime-file-format/sample_description_atom
*/
var SampleDescriptionTable = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
const descrLen = this.len - 12;
return {
dataFormat: FourCcToken.get(buf, off),
dataReferenceIndex: UINT16_BE.get(buf, off + 10),
description: descrLen > 0 ? new Uint8ArrayType(descrLen).get(buf, off + 12) : void 0
};
}
};
/**
* Atom: Sample-description Atom ('stsd')
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
*/
var StsdAtom = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
const header = stsdHeader.get(buf, off);
off += stsdHeader.len;
const table = [];
for (let n = 0; n < header.numberOfEntries; ++n) {
const size = UINT32_BE.get(buf, off);
off += UINT32_BE.len;
table.push(new SampleDescriptionTable(size - UINT32_BE.len).get(buf, off));
off += size;
}
return {
header,
table
};
}
};
/**
* Common Sound Sample Description (version & revision)
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-57317
*/
const SoundSampleDescriptionVersion = {
len: 8,
get(buf, off) {
return {
version: INT16_BE.get(buf, off),
revision: INT16_BE.get(buf, off + 2),
vendor: INT32_BE.get(buf, off + 4)
};
}
};
/**
* Sound Sample Description (Version 0)
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-130736
*/
const SoundSampleDescriptionV0 = {
len: 12,
get(buf, off) {
return {
numAudioChannels: INT16_BE.get(buf, off + 0),
sampleSize: INT16_BE.get(buf, off + 2),
compressionId: INT16_BE.get(buf, off + 4),
packetSize: INT16_BE.get(buf, off + 6),
sampleRate: UINT16_BE.get(buf, off + 8) + UINT16_BE.get(buf, off + 10) / 1e4
};
}
};
var SimpleTableAtom = class {
constructor(len, token) {
this.len = len;
this.token = token;
}
get(buf, off) {
const nrOfEntries = INT32_BE.get(buf, off + 4);
return {
version: INT8.get(buf, off + 0),
flags: INT24_BE.get(buf, off + 1),
numberOfEntries: nrOfEntries,
entries: readTokenTable(buf, this.token, off + 8, this.len - 8, nrOfEntries)
};
}
};
const TimeToSampleToken = {
len: 8,
get(buf, off) {
return {
count: INT32_BE.get(buf, off + 0),
duration: INT32_BE.get(buf, off + 4)
};
}
};
/**
* Time-to-sample('stts') atom.
* Store duration information for a media’s samples.
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25696
*/
var SttsAtom = class extends SimpleTableAtom {
constructor(len) {
super(len, TimeToSampleToken);
}
};
const SampleToChunkToken = {
len: 12,
get(buf, off) {
return {
firstChunk: INT32_BE.get(buf, off),
samplesPerChunk: INT32_BE.get(buf, off + 4),
sampleDescriptionId: INT32_BE.get(buf, off + 8)
};
}
};
/**
* Sample-to-Chunk ('stsc') atom interface
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25706
*/
var StscAtom = class extends SimpleTableAtom {
constructor(len) {
super(len, SampleToChunkToken);
}
};
/**
* Sample-size ('stsz') atom
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25710
*/
var StszAtom = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
const nrOfEntries = INT32_BE.get(buf, off + 8);
return {
version: INT8.get(buf, off),
flags: INT24_BE.get(buf, off + 1),
sampleSize: INT32_BE.get(buf, off + 4),
numberOfEntries: nrOfEntries,
entries: readTokenTable(buf, INT32_BE, off + 12, this.len - 12, nrOfEntries)
};
}
};
/**
* Chunk offset atom, 'stco'
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25715
*/
var StcoAtom = class extends SimpleTableAtom {
constructor(len) {
super(len, INT32_BE);
this.len = len;
}
};
/**
* Token used to decode text-track from 'mdat' atom (raw data stream)
*/
var ChapterText = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
return new StringType(INT16_BE.get(buf, off + 0), "utf-8").get(buf, off + 2);
}
};
function readTokenTable(buf, token, off, remainingLen, numberOfEntries) {
debug$2(`remainingLen=${remainingLen}, numberOfEntries=${numberOfEntries} * token-len=${token.len}`);
if (remainingLen === 0) return [];
if (remainingLen !== numberOfEntries * token.len) throw new Mp4ContentError("mismatch number-of-entries with remaining atom-length");
const entries = [];
for (let n = 0; n < numberOfEntries; ++n) {
entries.push(token.get(buf, off));
off += token.len;
}
return entries;
}
/**
* Sample-size ('tfhd') TrackFragmentHeaderBox
*/
var TrackFragmentHeaderBox = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
const flagOffset = off + 1;
const header = {
version: INT8.get(buf, off),
flags: {
baseDataOffsetPresent: getBit(buf, flagOffset + 2, 0),
sampleDescriptionIndexPresent: getBit(buf, flagOffset + 2, 1),
defaultSampleDurationPresent: getBit(buf, flagOffset + 2, 3),
defaultSampleSizePresent: getBit(buf, flagOffset + 2, 4),
defaultSampleFlagsPresent: getBit(buf, flagOffset + 2, 5),
defaultDurationIsEmpty: getBit(buf, flagOffset, 0),
defaultBaseIsMoof: getBit(buf, flagOffset, 1)
},
trackId: UINT32_BE.get(buf, 4)
};
let dynOffset = 8;
if (header.flags.baseDataOffsetPresent) {
header.baseDataOffset = UINT64_BE.get(buf, dynOffset);
dynOffset += 8;
}
if (header.flags.sampleDescriptionIndexPresent) {
header.sampleDescriptionIndex = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleDurationPresent) {
header.defaultSampleDuration = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleSizePresent) {
header.defaultSampleSize = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleFlagsPresent) header.defaultSampleFlags = UINT32_BE.get(buf, dynOffset);
return header;
}
};
/**
* Sample-size ('trun') TrackRunBox
*/
var TrackRunBox = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
const flagOffset = off + 1;
const trun = {
version: INT8.get(buf, off),
flags: {
dataOffsetPresent: getBit(buf, flagOffset + 2, 0),
firstSampleFlagsPresent: getBit(buf, flagOffset + 2, 2),
sampleDurationPresent: getBit(buf, flagOffset + 1, 0),
sampleSizePresent: getBit(buf, flagOffset + 1, 1),
sampleFlagsPresent: getBit(buf, flagOffset + 1, 2),
sampleCompositionTimeOffsetsPresent: getBit(buf, flagOffset + 1, 3)
},
sampleCount: UINT32_BE.get(buf, off + 4),
samples: []
};
let dynOffset = off + 8;
if (trun.flags.dataOffsetPresent) {
trun.dataOffset = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.firstSampleFlagsPresent) {
trun.firstSampleFlags = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
for (let n = 0; n < trun.sampleCount; ++n) {
if (dynOffset >= this.len) {
debug$2("TrackRunBox size mismatch");
break;
}
const sample = {};
if (trun.flags.sampleDurationPresent) {
sample.sampleDuration = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleSizePresent) {
sample.sampleSize = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleFlagsPresent) {
sample.sampleFlags = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleCompositionTimeOffsetsPresent) {
sample.sampleCompositionTimeOffset = UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
trun.samples.push(sample);
}
return trun;
}
};
/**
* HandlerBox (`hdlr`)
*/
var HandlerBox = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
off + 1;
const charTypeToken = new StringType(4, "utf-8");
return {
version: INT8.get(buf, off),
flags: UINT24_BE.get(buf, off + 1),
componentType: charTypeToken.get(buf, off + 4),
handlerType: charTypeToken.get(buf, off + 8),
componentName: new StringType(this.len - 28, "utf-8").get(buf, off + 28)
};
}
};
/**
* Chapter Track Reference Box (`chap`)
*/
var ChapterTrackReferenceBox = class {
constructor(len) {
this.len = len;
}
get(buf, off) {
let dynOffset = 0;
const trackIds = [];
while (dynOffset < this.len) {
trackIds.push(UINT32_BE.get(buf, off + dynOffset));
dynOffset += 4;
}
return trackIds;
}
};
//#endregion
//#region node_modules/music-metadata/lib/mp4/Atom.js
const debug$1 = (0, import_src.default)("music-metadata:parser:MP4:Atom");
var Atom = class Atom {
static async readAtom(tokenizer, dataHandler, parent, remaining) {
const offset = tokenizer.position;
debug$1(`Reading next token on offset=${offset}...`);
const header = await tokenizer.readToken(Header);
const extended = header.length === 1n;
if (extended) header.length = await tokenizer.readToken(ExtendedSize);
const atomBean = new Atom(header, extended, parent);
const payloadLength = atomBean.getPayloadLength(remaining);
debug$1(`parse atom name=${atomBean.atomPath}, extended=${atomBean.extended}, offset=${offset}, len=${atomBean.header.length}`);
await atomBean.readData(tokenizer, dataHandler, payloadLength);
return atomBean;
}
constructor(header, extended, parent) {
this.header = header;
this.extended = extended;
this.parent = parent;
this.children = [];
this.atomPath = (this.parent ? `${this.parent.atomPath}.` : "") + this.header.name;
}
getHeaderLength() {
return this.extended ? 16 : 8;
}
getPayloadLength(remaining) {
return (this.header.length === 0n ? remaining : Number(this.header.length)) - this.getHeaderLength();
}
async readAtoms(tokenizer, dataHandler, size) {
while (size > 0) {
const atomBean = await Atom.readAtom(tokenizer, dataHandler, this, size);
this.children.push(atomBean);
size -= atomBean.header.length === 0n ? size : Number(atomBean.header.length);
}
}
async readData(tokenizer, dataHandler, remaining) {
switch (this.header.name) {
case "moov":
case "udta":
case "mdia":
case "minf":
case "stbl":
case "<id>":
case "ilst":
case "tref":
case "moof": return this.readAtoms(tokenizer, dataHandler, this.getPayloadLength(remaining));
case "meta": {
const paddingLength = (await tokenizer.peekToken(Header)).name === "hdlr" ? 0 : 4;
await tokenizer.ignore(paddingLength);
return this.readAtoms(tokenizer, dataHandler, this.getPayloadLength(remaining) - paddingLength);
}
default: return dataHandler(this, remaining);
}
}
};
//#endregion
//#region node_modules/music-metadata/lib/mp4/MP4Parser.js
const debug = (0, import_src.default)("music-metadata:parser:MP4");
const tagFormat = "iTunes";
const encoderDict = {
raw: {
lossy: false,
format: "raw"
},
MAC3: {
lossy: true,
format: "MACE 3:1"
},
MAC6: {
lossy: true,
format: "MACE 6:1"
},
ima4: {
lossy: true,
format: "IMA 4:1"
},
ulaw: {
lossy: true,
format: "uLaw 2:1"
},
alaw: {
lossy: true,
format: "uLaw 2:1"
},
Qclp: {
lossy: true,
format: "QUALCOMM PureVoice"
},
".mp3": {
lossy: true,
format: "MPEG-1 layer 3"
},
alac: {
lossy: false,
format: "ALAC"
},
"ac-3": {
lossy: true,
format: "AC-3"
},
mp4a: {
lossy: true,
format: "MPEG-4/AAC"
},
mp4s: {
lossy: true,
format: "MP4S"
},
c608: {
lossy: true,
format: "CEA-608"
},
c708: {
lossy: true,
format: "CEA-708"
}
};
function distinct(value, index, self) {
return self.indexOf(value) === index;
}
var MP4Parser = class MP4Parser extends BasicParser {
constructor() {
super(...arguments);
this.tracks = /* @__PURE__ */ new Map();
this.hasVideoTrack = false;
this.hasAudioTrack = true;
this.atomParsers = {
/**
* Parse movie header (mvhd) atom
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
*/
mvhd: async (len) => {
const mvhd = await this.tokenizer.readToken(new MvhdAtom(len));
this.metadata.setFormat("creationTime", mvhd.creationTime);
this.metadata.setFormat("modificationTime", mvhd.modificationTime);
},
chap: async (len) => {
const td = this.getTrackDescription();
const trackIds = [];
while (len >= UINT32_BE.len) {
trackIds.push(await this.tokenizer.readNumber(UINT32_BE));
len -= UINT32_BE.len;
}
td.chapterList = trackIds;
},
/**
* Parse mdat atom.
* Will scan for chapters
*/
mdat: async (len) => {
if (this.options.includeChapters) {
const trackWithChapters = [...this.tracks.values()].filter((track) => track.chapterList);
if (trackWithChapters.length === 1) {
const chapterTrackIds = trackWithChapters[0].chapterList;
const chapterTracks = [...this.tracks.values()].filter((track) => chapterTrackIds.indexOf(track.header.trackId) !== -1);
if (chapterTracks.length === 1) return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
}
}
await this.tokenizer.ignore(len);
},
ftyp: async (len) => {
const types = [];
while (len > 0) {
const ftype = await this.tokenizer.readToken(ftyp);
len -= ftyp.len;
const value = ftype.type.replace(/\W/g, "");
if (value.length > 0) types.push(value);
}
debug(`ftyp: ${types.join("/")}`);
const x = types.filter(distinct).join("/");
this.metadata.setFormat("container", x);
},
/**
* Parse sample description atom
*/
stsd: async (len) => {
const stsd = await this.tokenizer.readToken(new StsdAtom(len));
const trackDescription = this.getTrackDescription();
trackDescription.soundSampleDescription = stsd.table.map((dfEntry) => this.parseSoundSampleDescription(dfEntry));
},
/**
* Parse sample-sizes atom ('stsz')
*/
stsz: async (len) => {
const stsz = await this.tokenizer.readToken(new StszAtom(len));
const td = this.getTrackDescription();
td.sampleSize = stsz.sampleSize;
td.sampleSizeTable = stsz.entries;
},
date: async (len) => {
const date = await this.tokenizer.readToken(new StringType(len, "utf-8"));
await this.addTag("date", date);
}
};
}
static read_BE_Integer(array, signed) {
const integerType = (signed ? "INT" : "UINT") + array.length * 8 + (array.length > 1 ? "_BE" : "");
const token = lib_exports[integerType];
if (!token) throw new Mp4ContentError(`Token for integer type not found: "${integerType}"`);
return Number(token.get(array, 0));
}
async parse() {
this.hasVideoTrack = false;
this.hasAudioTrack = true;
this.tracks.clear();
let remainingFileSize = this.tokenizer.fileInfo.size || 0;
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
try {
if ((await this.tokenizer.peekToken(Header)).name === "\0\0\0\0") {
const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
debug(errMsg);
this.addWarning(errMsg);
break;
}
} catch (error) {
if (error instanceof Error) {
const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
debug(errMsg);
this.addWarning(errMsg);
} else throw error;
break;
}
const rootAtom = await Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
}
const formatList = [];
this.tracks.forEach((track) => {
const trackFormats = [];
track.soundSampleDescription.forEach((ssd) => {
const streamInfo = {};
const encoderInfo = encoderDict[ssd.dataFormat];
if (encoderInfo) {
trackFormats.push(encoderInfo.format);
streamInfo.codecName = encoderInfo.format;
} else streamInfo.codecName = `<${ssd.dataFormat}>`;
if (ssd.description) {
const { description } = ssd;
if (description.sampleRate > 0) {
streamInfo.type = TrackType.audio;
streamInfo.audio = {
samplingFrequency: description.sampleRate,
bitDepth: description.sampleSize,
channels: description.numAudioChannels
};
}
}
this.metadata.addStreamInfo(streamInfo);
});
if (trackFormats.length >= 1) formatList.push(trackFormats.join("/"));
});
if (formatList.length > 0) this.metadata.setFormat("codec", formatList.filter(distinct).join("+"));
const audioTracks = [...this.tracks.values()].filter((track) => {
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
});
for (const audioTrack of audioTracks) {
if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
audioTrack.sampleRate = audioTrack.media.header.timeScale;
if (audioTrack.media.header.duration > 0) {
debug("Using duration defined on audio track");
audioTrack.samples = audioTrack.media.header.duration;
audioTrack.duration = audioTrack.samples / audioTrack.sampleRate;
}
if (audioTrack.fragments.length > 0) {
debug("Calculate duration defined in track fragments");
let totalTimeUnits = 0;
audioTrack.sizeInBytes = 0;
for (const fragment of audioTrack.fragments) for (const sample of fragment.trackRun.samples) {
const dur = sample.sampleDuration ?? fragment.header.defaultSampleDuration ?? 0;
const size = sample.sampleSize ?? fragment.header.defaultSampleSize ?? 0;
if (dur === 0) throw new Error("Missing sampleDuration and no defaultSampleDuration in track fragment header");
if (size === 0) throw new Error("Missing sampleSize and no defaultSampleSize in track fragment header");
totalTimeUnits += dur;
audioTrack.sizeInBytes += size;
}
if (!audioTrack.samples) audioTrack.samples = totalTimeUnits;
if (!audioTrack.duration) audioTrack.duration = totalTimeUnits / audioTrack.sampleRate;
} else if (audioTrack.sampleSizeTable.length > 0) audioTrack.sizeInBytes = audioTrack.sampleSizeTable.reduce((sum, n) => sum + n, 0);
}
const ssd = audioTrack.soundSampleDescription[0];
if (ssd.description && audioTrack.media.header) {
this.metadata.setFormat("sampleRate", ssd.description.sampleRate);
this.metadata.setFormat("bitsPerSample", ssd.description.sampleSize);
this.metadata.setFormat("numberOfChannels", ssd.description.numAudioChannels);
if (audioTrack.media.header.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) audioTrack.duration = audioTrack.timeToSampleTable.map((ttstEntry) => ttstEntry.count * ttstEntry.duration).reduce((total, sampleSize) => total + sampleSize) / ssd.description.sampleRate;
}
const encoderInfo = encoderDict[ssd.dataFormat];
if (encoderInfo) this.metadata.setFormat("lossless", !encoderInfo.lossy);
}
if (audioTracks.length >= 1) {
const firstAudioTrack = audioTracks[0];
if (firstAudioTrack.duration) {
this.metadata.setFormat("duration", firstAudioTrack.duration);
if (firstAudioTrack.sizeInBytes) this.metadata.setFormat("bitrate", 8 * firstAudioTrack.sizeInBytes / firstAudioTrack.duration);
}
}
this.metadata.setFormat("hasAudio", this.hasAudioTrack);
this.metadata.setFormat("hasVideo", this.hasVideoTrack);
}
async handleAtom(atom, remaining) {
if (atom.parent) switch (atom.parent.header.name) {
case "ilst":
case "<id>": return this.parseMetadataItemData(atom);
case "moov":
switch (atom.header.name) {
case "trak": return this.parseTrackBox(atom);
case "udta": return this.parseTrackBox(atom);
}
break;
case "moof": switch (atom.header.name) {
case "traf": return this.parseTrackFragmentBox(atom);
}
}
if (this.atomParsers[atom.header.name]) return this.atomParsers[atom.header.name](remaining);
debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
await this.tokenizer.ignore(remaining);
}
getTrackDescription() {
const tracks = [...this.tracks.values()];
return tracks[tracks.length - 1];
}
async addTag(id, value) {
await this.metadata.addTag(tagFormat, id, value);
}
addWarning(message) {
debug(`Warning: ${message}`);
this.metadata.addWarning(message);
}
/**
* Parse data of Meta-item-list-atom (item of 'ilst' atom)
* @param metaAtom
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
*/
parseMetadataItemData(metaAtom) {
let tagKey = metaAtom.header.name;
return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
const payLoadLength = child.getPayloadLength(remaining);
switch (child.header.name) {
case "data": return this.parseValueAtom(tagKey, child);
case "name":
case "mean":
case "rate": {
const name = await this.tokenizer.readToken(new NameAtom(payLoadLength));
tagKey += `:${name.name}`;
break;
}
default: {
const uint8Array = await this.tokenizer.readToken(new Uint8ArrayType(payLoadLength));
this.addWarning(`Unsupported meta-item: ${tagKey}[${child.header.name}] => value=${uint8ArrayToHex(uint8Array)} ascii=${textDecode(uint8Array, "ascii")}`);
}
}
}, metaAtom.getPayloadLength(0));
}
async parseValueAtom(tagKey, metaAtom) {
const dataAtom = await this.tokenizer.readToken(new DataAtom(Number(metaAtom.header.length) - Header.len));
if (dataAtom.type.set !== 0) throw new Mp4ContentError(`Unsupported type-set != 0: ${dataAtom.type.set}`);
switch (dataAtom.type.type) {
case 0:
switch (tagKey) {
case "trkn":
case "disk": {
const num = UINT8.get(dataAtom.value, 3);
const of = UINT8.get(dataAtom.value, 5);
await this.addTag(tagKey, `${num}/${of}`);
break;
}
case "gnre": {
const genreStr = Genres[UINT8.get(dataAtom.value, 1) - 1];
await this.addTag(tagKey, genreStr);
break;
}
case "rate": {
const rate = textDecode(dataAtom.value, "ascii");
await this.addTag(tagKey, rate);
break;
}
default: debug(`unknown proprietary value type for: ${metaAtom.atomPath}`);
}
break;
case 1:
case 18:
await this.addTag(tagKey, textDecode(dataAtom.value));
break;
case 13:
if (this.options.skipCovers) break;
await this.addTag(tagKey, {
format: "image/jpeg",
data: Uint8Array.from(dataAtom.value)
});
break;
case 14:
if (this.options.skipCovers) break;
await this.addTag(tagKey, {
format: "image/png",
data: Uint8Array.from(dataAtom.value)
});
break;
case 21:
await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true));
break;
case 22:
await this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false));
break;
case 65:
await this.addTag(tagKey, UINT8.get(dataAtom.value, 0));
break;
case 66:
await this.addTag(tagKey, UINT16_BE.get(dataAtom.value, 0));
break;
case 67:
await this.addTag(tagKey, UINT32_BE.get(dataAtom.value, 0));
break;
default: this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
}
}
async parseTrackBox(trakBox) {
const track = {
media: {},
fragments: []
};
await trakBox.readAtoms(this.tokenizer, async (child, remaining) => {
const payLoadLength = child.getPayloadLength(remaining);
switch (child.header.name) {
case "chap":
track.chapterList = await this.tokenizer.readToken(new ChapterTrackReferenceBox(remaining));
break;
case "tkhd":
track.header = await this.tokenizer.readToken(new TrackHeaderAtom(payLoadLength));
break;
case "hdlr":
track.handler = await this.tokenizer.readToken(new HandlerBox(payLoadLength));
track.isAudio = () => track.handler.handlerType === "audi" || track.handler.handlerType === "soun";
track.isVideo = () => track.handler.handlerType === "vide";
if (track.isAudio()) this.hasAudioTrack = true;
else if (track.isVideo()) this.hasVideoTrack = true;
break;
case "mdhd": {
const mdhd_data = await this.tokenizer.readToken(new MdhdAtom(payLoadLength));
track.media.header = mdhd_data;
break;
}
case "stco":
track.chunkOffsetTable = (await this.tokenizer.readToken(new StcoAtom(payLoadLength))).entries;
break;
case "stsc":
track.sampleToChunkTable = (await this.tokenizer.readToken(new StscAtom(payLoadLength))).entries;
break;
case "stsd":
track.soundSampleDescription = (await this.tokenizer.readToken(new StsdAtom(payLoadLength))).table.map((dfEntry) => this.parseSoundSampleDescription(dfEntry));
break;
case "stts":
track.timeToSampleTable = (await this.tokenizer.readToken(new SttsAtom(payLoadLength))).entries;
break;
case "stsz": {
const stsz = await this.tokenizer.readToken(new StszAtom(payLoadLength));
track.sampleSize = stsz.sampleSize;
track.sampleSizeTable = stsz.entries;
break;
}
case "dinf":
case "vmhd":
case "smhd":
debug(`Ignoring: ${child.header.name}`);
await this.tokenizer.ignore(payLoadLength);
break;
default:
debug(`Unexpected track box: ${child.header.name}`);
await this.tokenizer.ignore(payLoadLength);
}
}, trakBox.getPayloadLength(0));
this.tracks.set(track.header.trackId, track);
}
parseTrackFragmentBox(trafBox) {
let tfhd;
return trafBox.readAtoms(this.tokenizer, async (child, remaining) => {
const payLoadLength = child.getPayloadLength(remaining);
switch (child.header.name) {
case "tfhd": {
const fragmentHeaderBox = new TrackFragmentHeaderBox(child.getPayloadLength(remaining));
tfhd = await this.tokenizer.readToken(fragmentHeaderBox);
break;
}
case "tfdt":
await this.tokenizer.ignore(payLoadLength);
break;
case "trun": {
const trackRunBox = new TrackRunBox(payLoadLength);
const trun = await this.tokenizer.readToken(trackRunBox);
if (tfhd) this.tracks.get(tfhd.trackId)?.fragments.push({
header: tfhd,
trackRun: trun
});
break;
}
default:
debug(`Unexpected box: ${child.header.name}`);
await this.tokenizer.ignore(payLoadLength);
}
}, trafBox.getPayloadLength(0));
}
/**
* @param sampleDescription
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
*/
parseSoundSampleDescription(sampleDescription) {
const ssd = {
dataFormat: sampleDescription.dataFormat,
dataReferenceIndex: sampleDescription.dataReferenceIndex
};
let offset = 0;
if (sampleDescription.description) {
const version = SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
offset += SoundSampleDescriptionVersion.len;
if (version.version === 0 || version.version === 1) ssd.description = SoundSampleDescriptionV0.get(sampleDescription.description, offset);
else debug(`Warning: sound-sample-description ${version} not implemented`);
}
return ssd;
}
async parseChapterTrack(chapterTrack, track, len) {
if (!chapterTrack.sampleSize) {
if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length) throw new Error("Expected equal chunk-offset-table & sample-size-table length.");
}
const chapters = [];
for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
const start = chapterTrack.timeToSampleTable.slice(0, i).reduce((acc, cur) => acc + cur.duration, 0);
const nextChunkLen = chapterTrack.chunkOffsetTable[i] - this.tokenizer.position;
const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
len -= nextChunkLen + sampleSize;
if (len < 0) throw new Mp4ContentError("Chapter chunk exceeding token length");
await this.tokenizer.ignore(nextChunkLen);
const title = await this.tokenizer.readToken(new ChapterText(sampleSize));
debug(`Chapter ${i + 1}: ${title}`);
const chapter = {
title,
timeScale: chapterTrack.media.header ? chapterTrack.media.header.timeScale : 0,
start,
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
};
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${track.header.duration}`);
chapters.push(chapter);
}
this.metadata.setFormat("chapters", chapters);
await this.tokenizer.ignore(len);
}
findSampleOffset(track, chapterOffset) {
let chunkIndex = 0;
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) ++chunkIndex;
return this.getChunkDuration(chunkIndex + 1, track);
}
getChunkDuration(chunkId, track) {
let ttsi = 0;
let ttsc = track.timeToSampleTable[ttsi].count;
let ttsd = track.timeToSampleTable[ttsi].duration;
let curChunkId = 1;
let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
let totalDuration = 0;
while (curChunkId < chunkId) {
const nrOfSamples = Math.min(ttsc, samplesPerChunk);
totalDuration += nrOfSamples * ttsd;
ttsc -= nrOfSamples;
samplesPerChunk -= nrOfSamples;
if (samplesPerChunk === 0) {
++curChunkId;
samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
} else {
++ttsi;
ttsc = track.timeToSampleTable[ttsi].count;
ttsd = track.timeToSampleTable[ttsi].duration;
}
}
return totalDuration;
}
getSamplesPerChunk(chunkId, stcTable) {
for (let i = 0; i < stcTable.length - 1; ++i) if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) return stcTable[i].samplesPerChunk;
return stcTable[stcTable.length - 1].samplesPerChunk;
}
};
//#endregion
export { MP4Parser };