music-metadata 11.4.0 → 11.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { type MediaType } from 'media-typer';
2
2
  import { type INativeMetadataCollector } from './common/MetadataCollector.js';
3
- import type { IAudioMetadata, IOptions, ParserType } from './type.js';
3
+ import { type IAudioMetadata, type IOptions, type ParserType } from './type.js';
4
4
  import type { ITokenizer } from 'strtok3';
5
5
  export interface IParserLoader {
6
6
  /**
@@ -3,6 +3,7 @@ import ContentType from 'content-type';
3
3
  import { parse as mimeTypeParse } from 'media-typer';
4
4
  import initDebug from 'debug';
5
5
  import { MetadataCollector } from './common/MetadataCollector.js';
6
+ import { TrackType } from './type.js';
6
7
  import { mpegParserLoader } from './mpeg/MpegLoader.js';
7
8
  import { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
8
9
  import { apeParserLoader } from './apev2/Apev2Loader.js';
@@ -89,6 +90,14 @@ export class ParserFactory {
89
90
  const parser = new ParserImpl(metadata, tokenizer, opts ?? {});
90
91
  debug(`Parser ${parserLoader.parserType} loaded`);
91
92
  await parser.parse();
93
+ if (metadata.format.trackInfo) {
94
+ if (metadata.format.hasAudio === undefined) {
95
+ metadata.setFormat('hasAudio', !!metadata.format.trackInfo.find(track => track.type === TrackType.audio));
96
+ }
97
+ if (metadata.format.hasVideo === undefined) {
98
+ metadata.setFormat('hasVideo', !!metadata.format.trackInfo.find(track => track.type === TrackType.video));
99
+ }
100
+ }
92
101
  return metadata.toCommonMetadata();
93
102
  }
94
103
  /**
@@ -38,6 +38,7 @@ export class AIFFParser extends BasicParser {
38
38
  throw new AiffContentError(`Unsupported AIFF type: ${type}`);
39
39
  }
40
40
  this.metadata.setFormat('lossless', !this.isCompressed);
41
+ this.metadata.setAudioOnly();
41
42
  try {
42
43
  while (!this.tokenizer.fileInfo.size || this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
43
44
  debug(`Reading AIFF chunk at offset=${this.tokenizer.position}`);
@@ -96,6 +96,7 @@ export class APEv2Parser extends BasicParser {
96
96
  this.ape.descriptor = descriptor;
97
97
  const lenExp = descriptor.descriptorBytes - DescriptorParser.len;
98
98
  const header = await (lenExp > 0 ? this.parseDescriptorExpansion(lenExp) : this.parseHeader());
99
+ this.metadata.setAudioOnly();
99
100
  await this.tokenizer.ignore(header.forwardBytes);
100
101
  return this.tryParseApeHeader();
101
102
  }
@@ -24,6 +24,7 @@ export interface INativeMetadataCollector extends IWarningCollector {
24
24
  setFormat(key: FormatId, value: AnyTagValue): void;
25
25
  addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
26
26
  addStreamInfo(streamInfo: ITrackInfo): void;
27
+ setAudioOnly(): void;
27
28
  }
28
29
  /**
29
30
  * Provided to the parser to uodate the metadata result.
@@ -51,6 +52,7 @@ export declare class MetadataCollector implements INativeMetadataCollector {
51
52
  hasAny(): boolean;
52
53
  addStreamInfo(streamInfo: ITrackInfo): void;
53
54
  setFormat(key: FormatId, value: AnyTagValue): void;
55
+ setAudioOnly(): void;
54
56
  addTag(tagType: TagType, tagId: string, value: AnyTagValue): Promise<void>;
55
57
  addWarning(warning: string): void;
56
58
  postMap(tagType: TagType | 'artificial', tag: IGenericTag): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { TrackTypeValueToKeyMap } from '../type.js';
1
+ import { TrackTypeValueToKeyMap, } from '../type.js';
2
2
  import initDebug from 'debug';
3
3
  import { isSingleton, isUnique } from './GenericTagTypes.js';
4
4
  import { CombinedTagMapper } from './CombinedTagMapper.js';
@@ -61,6 +61,10 @@ export class MetadataCollector {
61
61
  this.opts.observer({ metadata: this, tag: { type: 'format', id: key, value } });
62
62
  }
63
63
  }
64
+ setAudioOnly() {
65
+ this.setFormat('hasAudio', true);
66
+ this.setFormat('hasVideo', false);
67
+ }
64
68
  async addTag(tagType, tagId, value) {
65
69
  debug(`tag ${tagType}.${tagId} = ${value}`);
66
70
  if (!this.native[tagType]) {
package/lib/core.js CHANGED
@@ -69,10 +69,7 @@ export function parseFromTokenizer(tokenizer, options) {
69
69
  export function orderTags(nativeTags) {
70
70
  const tags = {};
71
71
  for (const { id, value } of nativeTags) {
72
- if (!tags[id]) {
73
- tags[id] = [];
74
- }
75
- tags[id].push(value);
72
+ (tags[id] || (tags[id] = [])).push(value);
76
73
  }
77
74
  return tags;
78
75
  }
@@ -20,6 +20,7 @@ export class DsdiffParser extends BasicParser {
20
20
  const header = await this.tokenizer.readToken(ChunkHeader64);
21
21
  if (header.chunkID !== 'FRM8')
22
22
  throw new DsdiffContentParseError('Unexpected chunk-ID');
23
+ this.metadata.setAudioOnly();
23
24
  const type = (await this.tokenizer.readToken(FourCcToken)).trim();
24
25
  switch (type) {
25
26
  case 'DSD':
@@ -18,6 +18,7 @@ export class DsfParser extends AbstractID3Parser {
18
18
  throw new DsdContentParseError('Invalid chunk signature');
19
19
  this.metadata.setFormat('container', 'DSF');
20
20
  this.metadata.setFormat('lossless', true);
21
+ this.metadata.setAudioOnly();
21
22
  const dsdChunk = await this.tokenizer.readToken(DsdChunk);
22
23
  if (dsdChunk.metadataPointer === BigInt(0)) {
23
24
  debug("No ID3v2 tag present");
@@ -34,6 +34,7 @@ export class FlacParser extends AbstractID3Parser {
34
34
  if (fourCC.toString() !== 'fLaC') {
35
35
  throw new FlacContentError('Invalid FLAC preamble');
36
36
  }
37
+ this.metadata.setAudioOnly();
37
38
  let blockHeader;
38
39
  do {
39
40
  // Read block header
package/lib/mp4/Atom.js CHANGED
@@ -43,7 +43,6 @@ export class Atom {
43
43
  // "Container" atoms, contains nested atoms
44
44
  case 'moov': // The Movie Atom: contains other atoms
45
45
  case 'udta': // User defined atom
46
- case 'trak':
47
46
  case 'mdia': // Media atom
48
47
  case 'minf': // Media Information Atom
49
48
  case 'stbl': // The Sample Table Atom
@@ -121,7 +121,6 @@ export declare const Header: IToken<IAtomHeader>;
121
121
  */
122
122
  export declare const ExtendedSize: IToken<bigint>;
123
123
  export declare const ftyp: IGetToken<IAtomFtyp>;
124
- export declare const tkhd: IGetToken<IAtomFtyp>;
125
124
  /**
126
125
  * Token: Movie Header Atom
127
126
  */
@@ -269,7 +268,7 @@ export interface ITrackHeaderAtom extends IVersionAndFlags {
269
268
  volume: number;
270
269
  }
271
270
  /**
272
- * Track Header Atoms structure
271
+ * Track Header Atoms structure (`tkhd`)
273
272
  * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
274
273
  */
275
274
  export declare class TrackHeaderAtom implements IGetToken<ITrackHeaderAtom> {
@@ -462,4 +461,27 @@ export declare class TrackRunBox implements IGetToken<ITrackRunBox> {
462
461
  constructor(len: number);
463
462
  get(buf: Uint8Array, off: number): ITrackRunBox;
464
463
  }
464
+ export interface IHandlerBox {
465
+ version: number;
466
+ flags: number;
467
+ componentType: string;
468
+ handlerType: string;
469
+ componentName: string;
470
+ }
471
+ /**
472
+ * HandlerBox (`hdlr`)
473
+ */
474
+ export declare class HandlerBox implements IGetToken<IHandlerBox> {
475
+ len: number;
476
+ constructor(len: number);
477
+ get(buf: Uint8Array, off: number): IHandlerBox;
478
+ }
479
+ /**
480
+ * Chapter Track Reference Box (`chap`)
481
+ */
482
+ export declare class ChapterTrackReferenceBox implements IGetToken<number[]> {
483
+ len: number;
484
+ constructor(len: number);
485
+ get(buf: Uint8Array, off: number): number[];
486
+ }
465
487
  export {};
@@ -34,14 +34,6 @@ export const ftyp = {
34
34
  };
35
35
  }
36
36
  };
37
- export const tkhd = {
38
- len: 4,
39
- get: (buf, off) => {
40
- return {
41
- type: new Token.StringType(4, 'ascii').get(buf, off)
42
- };
43
- }
44
- };
45
37
  /**
46
38
  * Token: Movie Header Atom
47
39
  */
@@ -174,7 +166,7 @@ export class NameAtom {
174
166
  }
175
167
  }
176
168
  /**
177
- * Track Header Atoms structure
169
+ * Track Header Atoms structure (`tkhd`)
178
170
  * Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
179
171
  */
180
172
  export class TrackHeaderAtom {
@@ -494,4 +486,40 @@ export class TrackRunBox {
494
486
  return trun;
495
487
  }
496
488
  }
489
+ /**
490
+ * HandlerBox (`hdlr`)
491
+ */
492
+ export class HandlerBox {
493
+ constructor(len) {
494
+ this.len = len;
495
+ }
496
+ get(buf, off) {
497
+ const _flagOffset = off + 1;
498
+ const charTypeToken = new Token.StringType(4, 'utf-8');
499
+ return {
500
+ version: Token.INT8.get(buf, off),
501
+ flags: Token.UINT24_BE.get(buf, off + 1),
502
+ componentType: charTypeToken.get(buf, off + 4),
503
+ handlerType: charTypeToken.get(buf, off + 8),
504
+ componentName: new Token.StringType(this.len - 28, 'utf-8').get(buf, off + 28),
505
+ };
506
+ }
507
+ }
508
+ /**
509
+ * Chapter Track Reference Box (`chap`)
510
+ */
511
+ export class ChapterTrackReferenceBox {
512
+ constructor(len) {
513
+ this.len = len;
514
+ }
515
+ get(buf, off) {
516
+ let dynOffset = 0;
517
+ const trackIds = [];
518
+ while (dynOffset < this.len) {
519
+ trackIds.push(Token.UINT32_BE.get(buf, off + dynOffset));
520
+ dynOffset += 4;
521
+ }
522
+ return trackIds;
523
+ }
524
+ }
497
525
  //# sourceMappingURL=AtomToken.js.map
@@ -4,7 +4,8 @@ export declare class MP4Parser extends BasicParser {
4
4
  private static read_BE_Integer;
5
5
  private audioLengthInBytes;
6
6
  private tracks;
7
- private getTrackById;
7
+ private hasVideoTrack;
8
+ private hasAudioTrack;
8
9
  parse(): Promise<void>;
9
10
  handleAtom(atom: Atom, remaining: number): Promise<void>;
10
11
  private getTrackDescription;
@@ -18,6 +19,7 @@ export declare class MP4Parser extends BasicParser {
18
19
  */
19
20
  private parseMetadataItemData;
20
21
  private parseValueAtom;
22
+ private parseTrackBox;
21
23
  private parseTrackFragmentBox;
22
24
  private atomParsers;
23
25
  /**
@@ -4,7 +4,7 @@ import { BasicParser } from '../common/BasicParser.js';
4
4
  import { Genres } from '../id3v1/ID3v1Parser.js';
5
5
  import { Atom } from './Atom.js';
6
6
  import * as AtomToken from './AtomToken.js';
7
- import { Mp4ContentError } from './AtomToken.js';
7
+ import { ChapterTrackReferenceBox, Mp4ContentError, } from './AtomToken.js';
8
8
  import { TrackType } from '../type.js';
9
9
  import { uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras';
10
10
  const debug = initDebug('music-metadata:parser:MP4');
@@ -92,7 +92,9 @@ function distinct(value, index, self) {
92
92
  export class MP4Parser extends BasicParser {
93
93
  constructor() {
94
94
  super(...arguments);
95
- this.tracks = [];
95
+ this.tracks = new Map();
96
+ this.hasVideoTrack = false;
97
+ this.hasAudioTrack = true;
96
98
  this.atomParsers = {
97
99
  /**
98
100
  * Parse movie header (mvhd) atom
@@ -103,19 +105,6 @@ export class MP4Parser extends BasicParser {
103
105
  this.metadata.setFormat('creationTime', mvhd.creationTime);
104
106
  this.metadata.setFormat('modificationTime', mvhd.modificationTime);
105
107
  },
106
- /**
107
- * Parse media header (mdhd) atom
108
- * Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615
109
- */
110
- mdhd: async (len) => {
111
- const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(len));
112
- // this.parse_mxhd(mdhd_data, this.currentTrack);
113
- const td = this.getTrackDescription();
114
- td.creationTime = mdhd_data.creationTime;
115
- td.modificationTime = mdhd_data.modificationTime;
116
- td.timeScale = mdhd_data.timeScale;
117
- td.duration = mdhd_data.duration;
118
- },
119
108
  chap: async (len) => {
120
109
  const td = this.getTrackDescription();
121
110
  const trackIds = [];
@@ -125,11 +114,6 @@ export class MP4Parser extends BasicParser {
125
114
  }
126
115
  td.chapterList = trackIds;
127
116
  },
128
- tkhd: async (len) => {
129
- const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
130
- track.fragments = [];
131
- this.tracks.push(track);
132
- },
133
117
  /**
134
118
  * Parse mdat atom.
135
119
  * Will scan for chapters
@@ -138,10 +122,10 @@ export class MP4Parser extends BasicParser {
138
122
  this.audioLengthInBytes = len;
139
123
  this.calculateBitRate();
140
124
  if (this.options.includeChapters) {
141
- const trackWithChapters = this.tracks.filter(track => track.chapterList);
125
+ const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList);
142
126
  if (trackWithChapters.length === 1) {
143
127
  const chapterTrackIds = trackWithChapters[0].chapterList;
144
- const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
128
+ const chapterTracks = [...this.tracks.values()].filter(track => chapterTrackIds.indexOf(track.header.trackId) !== -1);
145
129
  if (chapterTracks.length === 1) {
146
130
  return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
147
131
  }
@@ -171,20 +155,6 @@ export class MP4Parser extends BasicParser {
171
155
  const trackDescription = this.getTrackDescription();
172
156
  trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
173
157
  },
174
- /**
175
- * sample-to-Chunk Atoms
176
- */
177
- stsc: async (len) => {
178
- const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
179
- this.getTrackDescription().sampleToChunkTable = stsc.entries;
180
- },
181
- /**
182
- * time-to-sample table
183
- */
184
- stts: async (len) => {
185
- const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
186
- this.getTrackDescription().timeToSampleTable = stts.entries;
187
- },
188
158
  /**
189
159
  * Parse sample-sizes atom ('stsz')
190
160
  */
@@ -194,13 +164,6 @@ export class MP4Parser extends BasicParser {
194
164
  td.sampleSize = stsz.sampleSize;
195
165
  td.sampleSizeTable = stsz.entries;
196
166
  },
197
- /**
198
- * Parse chunk-offset atom ('stco')
199
- */
200
- stco: async (len) => {
201
- const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
202
- this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
203
- },
204
167
  date: async (len) => {
205
168
  const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
206
169
  await this.addTag('date', date);
@@ -215,11 +178,10 @@ export class MP4Parser extends BasicParser {
215
178
  }
216
179
  return Number(token.get(array, 0));
217
180
  }
218
- getTrackById(trackId) {
219
- return this.tracks.find(track => track.trackId === trackId);
220
- }
221
181
  async parse() {
222
- this.tracks = [];
182
+ this.hasVideoTrack = false;
183
+ this.hasAudioTrack = true;
184
+ this.tracks.clear();
223
185
  let remainingFileSize = this.tokenizer.fileInfo.size || 0;
224
186
  while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
225
187
  try {
@@ -278,15 +240,15 @@ export class MP4Parser extends BasicParser {
278
240
  if (formatList.length > 0) {
279
241
  this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
280
242
  }
281
- const audioTracks = this.tracks.filter(track => {
243
+ const audioTracks = [...this.tracks.values()].filter(track => {
282
244
  return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
283
245
  });
284
246
  if (audioTracks.length >= 1) {
285
247
  const audioTrack = audioTracks[0];
286
- if (audioTrack.timeScale > 0) {
287
- if (audioTrack.duration > 0) {
248
+ if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
249
+ if (audioTrack.media.header.duration > 0) {
288
250
  debug('Using duration defined on audio track');
289
- const duration = audioTrack.duration / audioTrack.timeScale; // calculate duration in seconds
251
+ const duration = audioTrack.media.header.duration / audioTrack.media.header.timeScale; // calculate duration in seconds
290
252
  this.metadata.setFormat('duration', duration);
291
253
  }
292
254
  else if (audioTrack.fragments.length > 0) {
@@ -302,15 +264,15 @@ export class MP4Parser extends BasicParser {
302
264
  totalTimeUnits += dur;
303
265
  }
304
266
  }
305
- this.metadata.setFormat('duration', totalTimeUnits / audioTrack.timeScale);
267
+ this.metadata.setFormat('duration', totalTimeUnits / audioTrack.media.header.timeScale);
306
268
  }
307
269
  }
308
270
  const ssd = audioTrack.soundSampleDescription[0];
309
- if (ssd.description) {
271
+ if (ssd.description && audioTrack.media.header) {
310
272
  this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
311
273
  this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
312
274
  this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
313
- if (audioTrack.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
275
+ if (audioTrack.media.header.timeScale === 0 && audioTrack.timeToSampleTable.length > 0) {
314
276
  const totalSampleSize = audioTrack.timeToSampleTable
315
277
  .map(ttstEntry => ttstEntry.count * ttstEntry.duration)
316
278
  .reduce((total, sampleSize) => total + sampleSize);
@@ -324,6 +286,8 @@ export class MP4Parser extends BasicParser {
324
286
  }
325
287
  this.calculateBitRate();
326
288
  }
289
+ this.metadata.setFormat('hasAudio', this.hasAudioTrack);
290
+ this.metadata.setFormat('hasVideo', this.hasVideoTrack);
327
291
  }
328
292
  async handleAtom(atom, remaining) {
329
293
  if (atom.parent) {
@@ -331,6 +295,12 @@ export class MP4Parser extends BasicParser {
331
295
  case 'ilst':
332
296
  case '<id>':
333
297
  return this.parseMetadataItemData(atom);
298
+ case 'moov':
299
+ switch (atom.header.name) {
300
+ case 'trak':
301
+ return this.parseTrackBox(atom);
302
+ }
303
+ break;
334
304
  case 'moof':
335
305
  switch (atom.header.name) {
336
306
  case 'traf':
@@ -346,7 +316,9 @@ export class MP4Parser extends BasicParser {
346
316
  await this.tokenizer.ignore(remaining);
347
317
  }
348
318
  getTrackDescription() {
349
- return this.tracks[this.tracks.length - 1];
319
+ // ToDo: pick the right track, not the last track!!!!
320
+ const tracks = [...this.tracks.values()];
321
+ return tracks[tracks.length - 1];
350
322
  }
351
323
  calculateBitRate() {
352
324
  if (this.audioLengthInBytes && this.metadata.format.duration) {
@@ -459,6 +431,82 @@ export class MP4Parser extends BasicParser {
459
431
  this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
460
432
  }
461
433
  }
434
+ async parseTrackBox(trakBox) {
435
+ // @ts-ignore
436
+ const track = {
437
+ media: {},
438
+ fragments: []
439
+ };
440
+ await trakBox.readAtoms(this.tokenizer, async (child, remaining) => {
441
+ const payLoadLength = child.getPayloadLength(remaining);
442
+ switch (child.header.name) {
443
+ case 'chap': {
444
+ const chap = await this.tokenizer.readToken(new ChapterTrackReferenceBox(remaining));
445
+ track.chapterList = chap;
446
+ break;
447
+ }
448
+ case 'tkhd': // TrackHeaderBox
449
+ track.header = await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(payLoadLength));
450
+ break;
451
+ case 'hdlr': // TrackHeaderBox
452
+ track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength));
453
+ switch (track.handler.handlerType) {
454
+ case 'audi':
455
+ debug('Contains audio track');
456
+ this.hasAudioTrack = true;
457
+ break;
458
+ case 'vide':
459
+ debug('Contains video track');
460
+ this.hasVideoTrack = true;
461
+ break;
462
+ }
463
+ break;
464
+ case 'mdhd': { // Parse media header (mdhd) box
465
+ const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(payLoadLength));
466
+ track.media.header = mdhd_data;
467
+ break;
468
+ }
469
+ case 'stco': {
470
+ const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(payLoadLength));
471
+ track.chunkOffsetTable = stco.entries; // remember chunk offsets
472
+ break;
473
+ }
474
+ case 'stsc': { // sample-to-Chunk box
475
+ const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(payLoadLength));
476
+ track.sampleToChunkTable = stsc.entries;
477
+ break;
478
+ }
479
+ case 'stsd': { // sample description box
480
+ const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(payLoadLength));
481
+ track.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
482
+ break;
483
+ }
484
+ case 'stts': { // time-to-sample table
485
+ const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(payLoadLength));
486
+ track.timeToSampleTable = stts.entries;
487
+ break;
488
+ }
489
+ case 'stsz': {
490
+ const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(payLoadLength));
491
+ track.sampleSize = stsz.sampleSize;
492
+ track.sampleSizeTable = stsz.entries;
493
+ break;
494
+ }
495
+ case 'dinf':
496
+ case 'vmhd':
497
+ case 'smhd':
498
+ debug(`Ignoring: ${child.header.name}`);
499
+ await this.tokenizer.ignore(payLoadLength);
500
+ break;
501
+ default: {
502
+ debug(`Unexpected track box: ${child.header.name}`);
503
+ await this.tokenizer.ignore(payLoadLength);
504
+ }
505
+ }
506
+ }, trakBox.getPayloadLength(0));
507
+ // Register track
508
+ this.tracks.set(track.header.trackId, track);
509
+ }
462
510
  parseTrackFragmentBox(trafBox) {
463
511
  let tfhd;
464
512
  return trafBox.readAtoms(this.tokenizer, async (child, remaining) => {
@@ -476,7 +524,7 @@ export class MP4Parser extends BasicParser {
476
524
  const trackRunBox = new AtomToken.TrackRunBox(payLoadLength);
477
525
  const trun = await this.tokenizer.readToken(trackRunBox);
478
526
  if (tfhd) {
479
- const track = this.getTrackById(tfhd.trackId);
527
+ const track = this.tracks.get(tfhd.trackId);
480
528
  track?.fragments.push({ header: tfhd, trackRun: trun });
481
529
  }
482
530
  break;
@@ -532,11 +580,11 @@ export class MP4Parser extends BasicParser {
532
580
  debug(`Chapter ${i + 1}: ${title}`);
533
581
  const chapter = {
534
582
  title,
535
- timeScale: chapterTrack.timeScale,
583
+ timeScale: chapterTrack.media.header ? chapterTrack.media.header.timeScale : 0,
536
584
  start,
537
585
  sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
538
586
  };
539
- debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
587
+ debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${track.header.duration}`); // ToDo, use media duration if required!!!
540
588
  chapters.push(chapter);
541
589
  }
542
590
  this.metadata.setFormat('chapters', chapters);
@@ -256,6 +256,7 @@ export class MpegParser extends AbstractID3Parser {
256
256
  */
257
257
  async postId3v2Parse() {
258
258
  this.metadata.setFormat('lossless', false);
259
+ this.metadata.setAudioOnly();
259
260
  try {
260
261
  let quit = false;
261
262
  while (!quit) {
@@ -24,6 +24,7 @@ export class MusepackParser extends AbstractID3Parser {
24
24
  throw new MusepackContentError('Invalid signature prefix');
25
25
  }
26
26
  }
27
+ this.metadata.setAudioOnly();
27
28
  return mpcParser.parse();
28
29
  }
29
30
  }
@@ -27,6 +27,7 @@ export class OpusParser extends VorbisParser {
27
27
  throw new OpusContentError("Illegal ogg/Opus magic-signature");
28
28
  this.metadata.setFormat('sampleRate', this.idHeader.inputSampleRate);
29
29
  this.metadata.setFormat('numberOfChannels', this.idHeader.channelCount);
30
+ this.metadata.setAudioOnly();
30
31
  }
31
32
  async parseFullPage(pageData) {
32
33
  const magicSignature = new Token.StringType(8, 'ascii').get(pageData, 0);
@@ -27,6 +27,7 @@ export class SpeexParser extends VorbisParser {
27
27
  if (speexHeader.bitrate !== -1) {
28
28
  this.metadata.setFormat('bitrate', speexHeader.bitrate);
29
29
  }
30
+ this.metadata.setAudioOnly();
30
31
  }
31
32
  }
32
33
  //# sourceMappingURL=SpeexParser.js.map
@@ -34,6 +34,7 @@ export class TheoraParser {
34
34
  this.metadata.setFormat('codec', 'Theora');
35
35
  const idHeader = IdentificationHeader.get(pageData, 0);
36
36
  this.metadata.setFormat('bitrate', idHeader.nombr);
37
+ this.metadata.setAudioOnly();
37
38
  }
38
39
  }
39
40
  //# sourceMappingURL=TheoraParser.js.map
@@ -92,6 +92,7 @@ export class VorbisParser {
92
92
  */
93
93
  parseFirstPage(_header, pageData) {
94
94
  this.metadata.setFormat('codec', 'Vorbis I');
95
+ this.metadata.setAudioOnly();
95
96
  debug('Parse first page');
96
97
  // Parse Vorbis common header
97
98
  const commonHeader = CommonHeader.get(pageData, 0);
package/lib/type.d.ts CHANGED
@@ -370,7 +370,7 @@ export interface IRatio {
370
370
  */
371
371
  dB: number;
372
372
  }
373
- export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain';
373
+ export type FormatId = 'container' | 'duration' | 'bitrate' | 'sampleRate' | 'bitsPerSample' | 'codec' | 'tool' | 'codecProfile' | 'lossless' | 'numberOfChannels' | 'numberOfSamples' | 'audioMD5' | 'chapters' | 'modificationTime' | 'creationTime' | 'trackPeakLevel' | 'trackGain' | 'albumGain' | 'hasAudio' | 'hasVideo';
374
374
  export interface IAudioTrack {
375
375
  samplingFrequency?: number;
376
376
  outputSamplingFrequency?: number;
@@ -470,6 +470,14 @@ export interface IFormat {
470
470
  readonly trackGain?: number;
471
471
  readonly trackPeakLevel?: number;
472
472
  readonly albumGain?: number;
473
+ /**
474
+ * Indicates if the audio files contains an audio stream
475
+ */
476
+ hasAudio?: boolean;
477
+ /**
478
+ * Indicates if the media files contains a video stream
479
+ */
480
+ hasVideo?: boolean;
473
481
  }
474
482
  export interface ITag {
475
483
  id: string;
@@ -31,6 +31,7 @@ export class WaveParser extends BasicParser {
31
31
  debug(`pos=${this.tokenizer.position}, parse: chunkID=${riffHeader.chunkID}`);
32
32
  if (riffHeader.chunkID !== 'RIFF')
33
33
  return; // Not RIFF format
34
+ this.metadata.setAudioOnly();
34
35
  return this.parseRiffChunk(riffHeader.chunkSize).catch(err => {
35
36
  if (!(err instanceof strtok3.EndOfStreamError)) {
36
37
  throw err;
@@ -18,6 +18,7 @@ export class WavPackParser extends BasicParser {
18
18
  this.audioDataSize = 0;
19
19
  }
20
20
  async parse() {
21
+ this.metadata.setAudioOnly();
21
22
  this.audioDataSize = 0;
22
23
  // First parse all WavPack blocks
23
24
  await this.parseWavPackBlocks();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "music-metadata",
3
3
  "description": "Music metadata parser for Node.js, supporting virtual any audio and tag format.",
4
- "version": "11.4.0",
4
+ "version": "11.5.0",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -110,18 +110,18 @@
110
110
  "file-type": "^21.0.0",
111
111
  "media-typer": "^1.1.0",
112
112
  "strtok3": "^10.3.1",
113
- "token-types": "^6.0.0",
113
+ "token-types": "^6.0.3",
114
114
  "uint8array-extras": "^1.4.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@biomejs/biome": "2.0.5",
117
+ "@biomejs/biome": "2.0.6",
118
118
  "@types/chai": "^5.2.2",
119
119
  "@types/chai-as-promised": "^8.0.2",
120
120
  "@types/content-type": "^1.1.9",
121
121
  "@types/debug": "^4.1.12",
122
122
  "@types/media-typer": "^1.1.3",
123
123
  "@types/mocha": "^10.0.10",
124
- "@types/node": "^24.0.3",
124
+ "@types/node": "^24.0.4",
125
125
  "c8": "^10.1.3",
126
126
  "chai": "^5.2.0",
127
127
  "chai-as-promised": "^8.0.1",