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.
- package/lib/mp4/MP4Parser.d.ts +0 -2
- package/lib/mp4/MP4Parser.js +42 -30
- package/package.json +3 -3
package/lib/mp4/MP4Parser.d.ts
CHANGED
|
@@ -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
|
/**
|
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
251
|
+
audioTrack.samples = audioTrack.media.header.duration;
|
|
252
|
+
audioTrack.duration = audioTrack.samples / audioTrack.sampleRate;
|
|
254
253
|
}
|
|
255
|
-
|
|
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 ??
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|