music-metadata 11.10.2 → 11.10.4

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.
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright © 2025 Borewit
3
+ Copyright © 2026 Borewit
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
@@ -2,14 +2,12 @@ import { BasicParser } from '../common/BasicParser.js';
2
2
  import { Atom } from './Atom.js';
3
3
  export declare class MP4Parser extends BasicParser {
4
4
  private static read_BE_Integer;
5
- private audioLengthInBytes;
6
5
  private tracks;
7
6
  private hasVideoTrack;
8
7
  private hasAudioTrack;
9
8
  parse(): Promise<void>;
10
9
  handleAtom(atom: Atom, remaining: number): Promise<void>;
11
10
  private getTrackDescription;
12
- private calculateBitRate;
13
11
  private addTag;
14
12
  private addWarning;
15
13
  /**
@@ -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 { ChapterTrackReferenceBox, Mp4ContentError, } from './AtomToken.js';
7
+ import { ChapterTrackReferenceBox, Mp4ContentError } from './AtomToken.js';
8
8
  import { TrackType } from '../type.js';
9
9
  import { uint8ArrayToHex } from 'uint8array-extras';
10
10
  import { textDecode } from '@borewit/text-codec';
@@ -93,7 +93,6 @@ function distinct(value, index, self) {
93
93
  export class MP4Parser extends BasicParser {
94
94
  constructor() {
95
95
  super(...arguments);
96
- this.audioLengthInBytes = 0;
97
96
  this.tracks = new Map();
98
97
  this.hasVideoTrack = false;
99
98
  this.hasAudioTrack = true;
@@ -121,8 +120,6 @@ export class MP4Parser extends BasicParser {
121
120
  * Will scan for chapters
122
121
  */
123
122
  mdat: async (len) => {
124
- this.audioLengthInBytes += len;
125
- this.calculateBitRate();
126
123
  if (this.options.includeChapters) {
127
124
  const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList);
128
125
  if (trackWithChapters.length === 1) {
@@ -183,7 +180,6 @@ export class MP4Parser extends BasicParser {
183
180
  async parse() {
184
181
  this.hasVideoTrack = false;
185
182
  this.hasAudioTrack = true;
186
- this.audioLengthInBytes = 0;
187
183
  this.tracks.clear();
188
184
  let remainingFileSize = this.tokenizer.fileInfo.size || 0;
189
185
  while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
@@ -246,28 +242,42 @@ export class MP4Parser extends BasicParser {
246
242
  const audioTracks = [...this.tracks.values()].filter(track => {
247
243
  return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
248
244
  });
249
- if (audioTracks.length >= 1) {
250
- const audioTrack = audioTracks[0];
245
+ // Calculate duration and bitrate of audio tracks
246
+ for (const audioTrack of audioTracks) {
251
247
  if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
248
+ audioTrack.sampleRate = audioTrack.media.header.timeScale;
252
249
  if (audioTrack.media.header.duration > 0) {
253
250
  debug('Using duration defined on audio track');
254
- const duration = audioTrack.media.header.duration / audioTrack.media.header.timeScale; // calculate duration in seconds
255
- this.metadata.setFormat('duration', duration);
251
+ audioTrack.samples = audioTrack.media.header.duration;
252
+ audioTrack.duration = audioTrack.samples / audioTrack.sampleRate;
256
253
  }
257
- else if (audioTrack.fragments.length > 0) {
254
+ if (audioTrack.fragments.length > 0) {
258
255
  debug('Calculate duration defined in track fragments');
259
256
  let totalTimeUnits = 0;
257
+ audioTrack.sizeInBytes = 0;
260
258
  for (const fragment of audioTrack.fragments) {
261
- const defaultDuration = fragment.header.defaultSampleDuration;
262
259
  for (const sample of fragment.trackRun.samples) {
263
- const dur = sample.sampleDuration ?? defaultDuration;
264
- if (dur == null) {
265
- throw new Error("Missing sampleDuration and no default_sample_duration in tfhd");
260
+ const dur = sample.sampleDuration ?? fragment.header.defaultSampleDuration ?? 0;
261
+ const size = sample.sampleSize ?? fragment.header.defaultSampleSize ?? 0;
262
+ if (dur === 0) {
263
+ throw new Error("Missing sampleDuration and no defaultSampleDuration in track fragment header");
264
+ }
265
+ if (size === 0) {
266
+ throw new Error("Missing sampleSize and no defaultSampleSize in track fragment header");
266
267
  }
267
268
  totalTimeUnits += dur;
269
+ audioTrack.sizeInBytes += size;
268
270
  }
269
271
  }
270
- this.metadata.setFormat('duration', totalTimeUnits / audioTrack.media.header.timeScale);
272
+ if (!audioTrack.samples) {
273
+ audioTrack.samples = totalTimeUnits;
274
+ }
275
+ if (!audioTrack.duration) {
276
+ audioTrack.duration = totalTimeUnits / audioTrack.sampleRate;
277
+ }
278
+ }
279
+ else if (audioTrack.sampleSizeTable.length > 0) {
280
+ audioTrack.sizeInBytes = audioTrack.sampleSizeTable.reduce((sum, n) => sum + n, 0);
271
281
  }
272
282
  }
273
283
  const ssd = audioTrack.soundSampleDescription[0];
@@ -279,15 +289,22 @@ export class MP4Parser extends BasicParser {
279
289
  const totalSampleSize = audioTrack.timeToSampleTable
280
290
  .map(ttstEntry => ttstEntry.count * ttstEntry.duration)
281
291
  .reduce((total, sampleSize) => total + sampleSize);
282
- const duration = totalSampleSize / ssd.description.sampleRate;
283
- this.metadata.setFormat('duration', duration);
292
+ audioTrack.duration = totalSampleSize / ssd.description.sampleRate;
284
293
  }
285
294
  }
286
295
  const encoderInfo = encoderDict[ssd.dataFormat];
287
296
  if (encoderInfo) {
288
297
  this.metadata.setFormat('lossless', !encoderInfo.lossy);
289
298
  }
290
- this.calculateBitRate();
299
+ }
300
+ if (audioTracks.length >= 1) {
301
+ const firstAudioTrack = audioTracks[0];
302
+ if (firstAudioTrack.duration) {
303
+ this.metadata.setFormat('duration', firstAudioTrack.duration);
304
+ if (firstAudioTrack.sizeInBytes) {
305
+ this.metadata.setFormat('bitrate', 8 * firstAudioTrack.sizeInBytes / firstAudioTrack.duration);
306
+ }
307
+ }
291
308
  }
292
309
  this.metadata.setFormat('hasAudio', this.hasAudioTrack);
293
310
  this.metadata.setFormat('hasVideo', this.hasVideoTrack);
@@ -325,11 +342,6 @@ export class MP4Parser extends BasicParser {
325
342
  const tracks = [...this.tracks.values()];
326
343
  return tracks[tracks.length - 1];
327
344
  }
328
- calculateBitRate() {
329
- if (this.audioLengthInBytes && this.metadata.format.duration) {
330
- this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
331
- }
332
- }
333
345
  async addTag(id, value) {
334
346
  await this.metadata.addTag(tagFormat, id, value);
335
347
  }
@@ -455,15 +467,13 @@ export class MP4Parser extends BasicParser {
455
467
  break;
456
468
  case 'hdlr': // TrackHeaderBox
457
469
  track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength));
458
- switch (track.handler.handlerType) {
459
- case 'audi':
460
- debug('Contains audio track');
461
- this.hasAudioTrack = true;
462
- break;
463
- case 'vide':
464
- debug('Contains video track');
465
- this.hasVideoTrack = true;
466
- break;
470
+ track.isAudio = () => track.handler.handlerType === 'audi' || track.handler.handlerType === 'soun';
471
+ track.isVideo = () => track.handler.handlerType === 'vide';
472
+ if (track.isAudio()) {
473
+ this.hasAudioTrack = true;
474
+ }
475
+ else if (track.isVideo()) {
476
+ this.hasVideoTrack = true;
467
477
  }
468
478
  break;
469
479
  case 'mdhd': { // Parse media header (mdhd) box
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.10.2",
4
+ "version": "11.10.4",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -106,27 +106,27 @@
106
106
  "update-biome": "yarn add -D --exact @biomejs/biome && npx @biomejs/biome migrate --write"
107
107
  },
108
108
  "dependencies": {
109
- "@borewit/text-codec": "^0.2.0",
109
+ "@borewit/text-codec": "^0.2.1",
110
110
  "@tokenizer/token": "^0.3.0",
111
111
  "content-type": "^1.0.5",
112
112
  "debug": "^4.4.3",
113
- "file-type": "^21.1.1",
113
+ "file-type": "^21.2.0",
114
114
  "media-typer": "^1.1.0",
115
115
  "strtok3": "^10.3.4",
116
- "token-types": "^6.1.1",
116
+ "token-types": "^6.1.2",
117
117
  "uint8array-extras": "^1.5.0"
118
118
  },
119
119
  "devDependencies": {
120
- "@biomejs/biome": "2.3.5",
121
- "@types/chai": "^5.2.2",
120
+ "@biomejs/biome": "2.3.10",
121
+ "@types/chai": "^5.2.3",
122
122
  "@types/chai-as-promised": "^8.0.2",
123
123
  "@types/content-type": "^1.1.9",
124
124
  "@types/debug": "^4.1.12",
125
125
  "@types/media-typer": "^1.1.3",
126
126
  "@types/mocha": "^10.0.10",
127
- "@types/node": "^24.5.0",
127
+ "@types/node": "^25.0.3",
128
128
  "c8": "^10.1.3",
129
- "chai": "^6.2.1",
129
+ "chai": "^6.2.2",
130
130
  "chai-as-promised": "^8.0.2",
131
131
  "del-cli": "^7.0.0",
132
132
  "mime": "^4.1.0",