music-metadata 11.10.1 → 11.10.3

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.
@@ -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';
@@ -120,8 +120,6 @@ export class MP4Parser extends BasicParser {
120
120
  * Will scan for chapters
121
121
  */
122
122
  mdat: async (len) => {
123
- this.audioLengthInBytes = len;
124
- this.calculateBitRate();
125
123
  if (this.options.includeChapters) {
126
124
  const trackWithChapters = [...this.tracks.values()].filter(track => track.chapterList);
127
125
  if (trackWithChapters.length === 1) {
@@ -244,28 +242,42 @@ export class MP4Parser extends BasicParser {
244
242
  const audioTracks = [...this.tracks.values()].filter(track => {
245
243
  return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
246
244
  });
247
- if (audioTracks.length >= 1) {
248
- const audioTrack = audioTracks[0];
245
+ // Calculate duration and bitrate of audio tracks
246
+ for (const audioTrack of audioTracks) {
249
247
  if (audioTrack.media.header && audioTrack.media.header.timeScale > 0) {
248
+ audioTrack.sampleRate = audioTrack.media.header.timeScale;
250
249
  if (audioTrack.media.header.duration > 0) {
251
250
  debug('Using duration defined on audio track');
252
- const duration = audioTrack.media.header.duration / audioTrack.media.header.timeScale; // calculate duration in seconds
253
- this.metadata.setFormat('duration', duration);
251
+ audioTrack.samples = audioTrack.media.header.duration;
252
+ audioTrack.duration = audioTrack.samples / audioTrack.sampleRate;
254
253
  }
255
- else if (audioTrack.fragments.length > 0) {
254
+ if (audioTrack.fragments.length > 0) {
256
255
  debug('Calculate duration defined in track fragments');
257
256
  let totalTimeUnits = 0;
257
+ audioTrack.sizeInBytes = 0;
258
258
  for (const fragment of audioTrack.fragments) {
259
- const defaultDuration = fragment.header.defaultSampleDuration;
260
259
  for (const sample of fragment.trackRun.samples) {
261
- const dur = sample.sampleDuration ?? defaultDuration;
262
- if (dur == null) {
263
- 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");
264
267
  }
265
268
  totalTimeUnits += dur;
269
+ audioTrack.sizeInBytes += size;
266
270
  }
267
271
  }
268
- 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);
269
281
  }
270
282
  }
271
283
  const ssd = audioTrack.soundSampleDescription[0];
@@ -277,15 +289,22 @@ export class MP4Parser extends BasicParser {
277
289
  const totalSampleSize = audioTrack.timeToSampleTable
278
290
  .map(ttstEntry => ttstEntry.count * ttstEntry.duration)
279
291
  .reduce((total, sampleSize) => total + sampleSize);
280
- const duration = totalSampleSize / ssd.description.sampleRate;
281
- this.metadata.setFormat('duration', duration);
292
+ audioTrack.duration = totalSampleSize / ssd.description.sampleRate;
282
293
  }
283
294
  }
284
295
  const encoderInfo = encoderDict[ssd.dataFormat];
285
296
  if (encoderInfo) {
286
297
  this.metadata.setFormat('lossless', !encoderInfo.lossy);
287
298
  }
288
- 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
+ }
289
308
  }
290
309
  this.metadata.setFormat('hasAudio', this.hasAudioTrack);
291
310
  this.metadata.setFormat('hasVideo', this.hasVideoTrack);
@@ -323,11 +342,6 @@ export class MP4Parser extends BasicParser {
323
342
  const tracks = [...this.tracks.values()];
324
343
  return tracks[tracks.length - 1];
325
344
  }
326
- calculateBitRate() {
327
- if (this.audioLengthInBytes && this.metadata.format.duration) {
328
- this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
329
- }
330
- }
331
345
  async addTag(id, value) {
332
346
  await this.metadata.addTag(tagFormat, id, value);
333
347
  }
@@ -453,15 +467,13 @@ export class MP4Parser extends BasicParser {
453
467
  break;
454
468
  case 'hdlr': // TrackHeaderBox
455
469
  track.handler = await this.tokenizer.readToken(new AtomToken.HandlerBox(payLoadLength));
456
- switch (track.handler.handlerType) {
457
- case 'audi':
458
- debug('Contains audio track');
459
- this.hasAudioTrack = true;
460
- break;
461
- case 'vide':
462
- debug('Contains video track');
463
- this.hasVideoTrack = true;
464
- 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;
465
477
  }
466
478
  break;
467
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.1",
4
+ "version": "11.10.3",
5
5
  "author": {
6
6
  "name": "Borewit",
7
7
  "url": "https://github.com/Borewit"
@@ -110,14 +110,14 @@
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.0",
113
+ "file-type": "^21.1.1",
114
114
  "media-typer": "^1.1.0",
115
115
  "strtok3": "^10.3.4",
116
116
  "token-types": "^6.1.1",
117
117
  "uint8array-extras": "^1.5.0"
118
118
  },
119
119
  "devDependencies": {
120
- "@biomejs/biome": "2.3.5",
120
+ "@biomejs/biome": "2.3.7",
121
121
  "@types/chai": "^5.2.2",
122
122
  "@types/chai-as-promised": "^8.0.2",
123
123
  "@types/content-type": "^1.1.9",