music-metadata 7.11.7 → 7.12.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.
- package/LICENSE.txt +9 -0
- package/README.md +434 -432
- package/lib/ParserFactory.d.ts +48 -49
- package/lib/ParserFactory.js +252 -251
- package/lib/aiff/AiffParser.d.ts +14 -15
- package/lib/aiff/AiffParser.js +84 -85
- package/lib/aiff/AiffToken.d.ts +22 -22
- package/lib/aiff/AiffToken.js +43 -43
- package/lib/apev2/APEv2Parser.d.ts +30 -30
- package/lib/apev2/APEv2Parser.js +161 -162
- package/lib/apev2/APEv2TagMapper.d.ts +4 -4
- package/lib/apev2/APEv2TagMapper.js +86 -86
- package/lib/apev2/APEv2Token.d.ts +100 -100
- package/lib/apev2/APEv2Token.js +126 -126
- package/lib/asf/AsfObject.d.ts +319 -319
- package/lib/asf/AsfObject.js +384 -384
- package/lib/asf/AsfParser.d.ts +17 -17
- package/lib/asf/AsfParser.js +135 -135
- package/lib/asf/AsfTagMapper.d.ts +7 -7
- package/lib/asf/AsfTagMapper.js +95 -95
- package/lib/asf/AsfUtil.d.ts +13 -13
- package/lib/asf/AsfUtil.js +40 -40
- package/lib/asf/GUID.d.ts +84 -86
- package/lib/asf/GUID.js +121 -123
- package/lib/common/BasicParser.d.ts +17 -17
- package/lib/common/BasicParser.js +18 -18
- package/lib/common/CaseInsensitiveTagMap.d.ts +10 -10
- package/lib/common/CaseInsensitiveTagMap.js +21 -21
- package/lib/common/CombinedTagMapper.d.ts +19 -19
- package/lib/common/CombinedTagMapper.js +51 -51
- package/lib/common/FourCC.d.ts +6 -6
- package/lib/common/FourCC.js +28 -28
- package/lib/common/GenericTagMapper.d.ts +51 -51
- package/lib/common/GenericTagMapper.js +55 -55
- package/lib/common/GenericTagTypes.d.ts +33 -33
- package/lib/common/GenericTagTypes.js +131 -131
- package/lib/common/MetadataCollector.d.ts +76 -76
- package/lib/common/MetadataCollector.js +275 -275
- package/lib/common/RandomFileReader.d.ts +22 -20
- package/lib/common/RandomFileReader.js +34 -37
- package/lib/common/RandomUint8ArrayReader.d.ts +18 -18
- package/lib/common/RandomUint8ArrayReader.js +25 -25
- package/lib/common/Util.d.ts +57 -58
- package/lib/common/Util.js +157 -162
- package/lib/core.d.ts +48 -48
- package/lib/core.js +90 -90
- package/lib/dsdiff/DsdiffParser.d.ts +14 -14
- package/lib/dsdiff/DsdiffParser.js +143 -143
- package/lib/dsdiff/DsdiffToken.d.ts +9 -9
- package/lib/dsdiff/DsdiffToken.js +21 -21
- package/lib/dsf/DsfChunk.d.ts +86 -86
- package/lib/dsf/DsfChunk.js +54 -54
- package/lib/dsf/DsfParser.d.ts +9 -9
- package/lib/dsf/DsfParser.js +56 -56
- package/lib/flac/FlacParser.d.ts +28 -28
- package/lib/flac/FlacParser.js +175 -175
- package/lib/id3v1/ID3v1Parser.d.ts +13 -13
- package/lib/id3v1/ID3v1Parser.js +134 -134
- package/lib/id3v1/ID3v1TagMap.d.ts +4 -4
- package/lib/id3v1/ID3v1TagMap.js +22 -22
- package/lib/id3v2/AbstractID3Parser.d.ts +17 -17
- package/lib/id3v2/AbstractID3Parser.js +60 -60
- package/lib/id3v2/FrameParser.d.ts +32 -32
- package/lib/id3v2/FrameParser.js +329 -329
- package/lib/id3v2/ID3v22TagMapper.d.ts +9 -9
- package/lib/id3v2/ID3v22TagMapper.js +55 -55
- package/lib/id3v2/ID3v24TagMapper.d.ts +14 -14
- package/lib/id3v2/ID3v24TagMapper.js +193 -193
- package/lib/id3v2/ID3v2Parser.d.ts +29 -29
- package/lib/id3v2/ID3v2Parser.js +184 -194
- package/lib/id3v2/ID3v2Token.d.ts +73 -73
- package/lib/id3v2/ID3v2Token.js +106 -106
- package/lib/iff/index.d.ts +33 -33
- package/lib/iff/index.js +19 -19
- package/lib/index.d.ts +45 -45
- package/lib/index.js +74 -74
- package/lib/lyrics3/Lyrics3.d.ts +3 -3
- package/lib/lyrics3/Lyrics3.js +17 -17
- package/lib/matroska/MatroskaDtd.d.ts +8 -8
- package/lib/matroska/MatroskaDtd.js +279 -279
- package/lib/matroska/MatroskaParser.d.ts +37 -37
- package/lib/matroska/MatroskaParser.js +235 -235
- package/lib/matroska/MatroskaTagMapper.d.ts +4 -4
- package/lib/matroska/MatroskaTagMapper.js +35 -35
- package/lib/matroska/types.d.ts +175 -175
- package/lib/matroska/types.js +33 -32
- package/lib/mp4/Atom.d.ts +16 -16
- package/lib/mp4/Atom.js +70 -70
- package/lib/mp4/AtomToken.d.ts +395 -395
- package/lib/mp4/AtomToken.js +406 -406
- package/lib/mp4/MP4Parser.d.ts +30 -30
- package/lib/mp4/MP4Parser.js +511 -511
- package/lib/mp4/MP4TagMapper.d.ts +5 -5
- package/lib/mp4/MP4TagMapper.js +115 -115
- package/lib/mpeg/ExtendedLameHeader.d.ts +27 -27
- package/lib/mpeg/ExtendedLameHeader.js +31 -31
- package/lib/mpeg/MpegParser.d.ts +49 -49
- package/lib/mpeg/MpegParser.js +524 -529
- package/lib/mpeg/ReplayGainDataFormat.d.ts +55 -55
- package/lib/mpeg/ReplayGainDataFormat.js +69 -69
- package/lib/mpeg/XingTag.d.ts +45 -45
- package/lib/mpeg/XingTag.js +69 -69
- package/lib/musepack/index.d.ts +5 -5
- package/lib/musepack/index.js +32 -32
- package/lib/musepack/sv7/BitReader.d.ts +13 -13
- package/lib/musepack/sv7/BitReader.js +54 -54
- package/lib/musepack/sv7/MpcSv7Parser.d.ts +8 -8
- package/lib/musepack/sv7/MpcSv7Parser.js +46 -46
- package/lib/musepack/sv7/StreamVersion7.d.ts +28 -28
- package/lib/musepack/sv7/StreamVersion7.js +41 -41
- package/lib/musepack/sv8/MpcSv8Parser.d.ts +6 -6
- package/lib/musepack/sv8/MpcSv8Parser.js +55 -55
- package/lib/musepack/sv8/StreamVersion8.d.ts +40 -40
- package/lib/musepack/sv8/StreamVersion8.js +80 -80
- package/lib/ogg/Ogg.d.ts +72 -72
- package/lib/ogg/Ogg.js +2 -2
- package/lib/ogg/OggParser.d.ts +23 -23
- package/lib/ogg/OggParser.js +126 -126
- package/lib/ogg/opus/Opus.d.ts +48 -48
- package/lib/ogg/opus/Opus.js +28 -28
- package/lib/ogg/opus/OpusParser.d.ts +25 -25
- package/lib/ogg/opus/OpusParser.js +56 -56
- package/lib/ogg/speex/Speex.d.ts +36 -36
- package/lib/ogg/speex/Speex.js +31 -31
- package/lib/ogg/speex/SpeexParser.d.ts +22 -22
- package/lib/ogg/speex/SpeexParser.js +35 -35
- package/lib/ogg/theora/Theora.d.ts +20 -20
- package/lib/ogg/theora/Theora.js +23 -23
- package/lib/ogg/theora/TheoraParser.d.ts +28 -28
- package/lib/ogg/theora/TheoraParser.js +44 -44
- package/lib/ogg/vorbis/Vorbis.d.ts +69 -79
- package/lib/ogg/vorbis/Vorbis.js +78 -78
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +12 -12
- package/lib/ogg/vorbis/VorbisDecoder.js +32 -32
- package/lib/ogg/vorbis/VorbisParser.d.ts +36 -36
- package/lib/ogg/vorbis/VorbisParser.js +128 -128
- package/lib/ogg/vorbis/VorbisTagMapper.d.ts +7 -7
- package/lib/ogg/vorbis/VorbisTagMapper.js +132 -132
- package/lib/riff/RiffChunk.d.ts +16 -16
- package/lib/riff/RiffChunk.js +32 -32
- package/lib/riff/RiffInfoTagMap.d.ts +10 -10
- package/lib/riff/RiffInfoTagMap.js +37 -37
- package/lib/type.d.ts +592 -599
- package/lib/type.js +5 -13
- package/lib/wav/BwfChunk.d.ts +17 -0
- package/lib/wav/BwfChunk.js +28 -0
- package/lib/wav/WaveChunk.d.ts +64 -64
- package/lib/wav/WaveChunk.js +65 -65
- package/lib/wav/WaveParser.d.ts +24 -24
- package/lib/wav/WaveParser.js +156 -144
- package/lib/wavpack/WavPackParser.d.ts +14 -14
- package/lib/wavpack/WavPackParser.js +99 -105
- package/lib/wavpack/WavPackToken.d.ts +64 -64
- package/lib/wavpack/WavPackToken.js +76 -76
- package/package.json +150 -142
package/lib/mp4/MP4Parser.js
CHANGED
|
@@ -1,511 +1,511 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MP4Parser = void 0;
|
|
4
|
-
const initDebug = require("debug");
|
|
5
|
-
const Token = require("token-types");
|
|
6
|
-
const BasicParser_1 = require("../common/BasicParser");
|
|
7
|
-
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
|
8
|
-
const type_1 = require("../type");
|
|
9
|
-
const Atom_1 = require("./Atom");
|
|
10
|
-
const AtomToken = require("./AtomToken");
|
|
11
|
-
const debug = initDebug('music-metadata:parser:MP4');
|
|
12
|
-
const tagFormat = 'iTunes';
|
|
13
|
-
const encoderDict = {
|
|
14
|
-
raw: {
|
|
15
|
-
lossy: false,
|
|
16
|
-
format: 'raw'
|
|
17
|
-
},
|
|
18
|
-
MAC3: {
|
|
19
|
-
lossy: true,
|
|
20
|
-
format: 'MACE 3:1'
|
|
21
|
-
},
|
|
22
|
-
MAC6: {
|
|
23
|
-
lossy: true,
|
|
24
|
-
format: 'MACE 6:1'
|
|
25
|
-
},
|
|
26
|
-
ima4: {
|
|
27
|
-
lossy: true,
|
|
28
|
-
format: 'IMA 4:1'
|
|
29
|
-
},
|
|
30
|
-
ulaw: {
|
|
31
|
-
lossy: true,
|
|
32
|
-
format: 'uLaw 2:1'
|
|
33
|
-
},
|
|
34
|
-
alaw: {
|
|
35
|
-
lossy: true,
|
|
36
|
-
format: 'uLaw 2:1'
|
|
37
|
-
},
|
|
38
|
-
Qclp: {
|
|
39
|
-
lossy: true,
|
|
40
|
-
format: 'QUALCOMM PureVoice'
|
|
41
|
-
},
|
|
42
|
-
'.mp3': {
|
|
43
|
-
lossy: true,
|
|
44
|
-
format: 'MPEG-1 layer 3'
|
|
45
|
-
},
|
|
46
|
-
alac: {
|
|
47
|
-
lossy: false,
|
|
48
|
-
format: 'ALAC'
|
|
49
|
-
},
|
|
50
|
-
'ac-3': {
|
|
51
|
-
lossy: true,
|
|
52
|
-
format: 'AC-3'
|
|
53
|
-
},
|
|
54
|
-
mp4a: {
|
|
55
|
-
lossy: true,
|
|
56
|
-
format: 'MPEG-4/AAC'
|
|
57
|
-
},
|
|
58
|
-
mp4s: {
|
|
59
|
-
lossy: true,
|
|
60
|
-
format: 'MP4S'
|
|
61
|
-
},
|
|
62
|
-
// Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87
|
|
63
|
-
c608: {
|
|
64
|
-
lossy: true,
|
|
65
|
-
format: 'CEA-608'
|
|
66
|
-
},
|
|
67
|
-
c708: {
|
|
68
|
-
lossy: true,
|
|
69
|
-
format: 'CEA-708'
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
function distinct(value, index, self) {
|
|
73
|
-
return self.indexOf(value) === index;
|
|
74
|
-
}
|
|
75
|
-
/*
|
|
76
|
-
* Parser for the MP4 (MPEG-4 Part 14) container format
|
|
77
|
-
* Standard: ISO/IEC 14496-14
|
|
78
|
-
* supporting:
|
|
79
|
-
* - QuickTime container
|
|
80
|
-
* - MP4 File Format
|
|
81
|
-
* - 3GPP file format
|
|
82
|
-
* - 3GPP2 file format
|
|
83
|
-
*
|
|
84
|
-
* MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension.
|
|
85
|
-
* Support for Apple iTunes tags as found in a M4A/M4V files.
|
|
86
|
-
* Ref:
|
|
87
|
-
* https://en.wikipedia.org/wiki/ISO_base_media_file_format
|
|
88
|
-
* https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
|
|
89
|
-
* http://atomicparsley.sourceforge.net/mpeg-4files.html
|
|
90
|
-
* https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
|
|
91
|
-
* https://wiki.multimedia.cx/index.php/QuickTime_container
|
|
92
|
-
*/
|
|
93
|
-
class MP4Parser extends BasicParser_1.BasicParser {
|
|
94
|
-
constructor() {
|
|
95
|
-
super(...arguments);
|
|
96
|
-
this.atomParsers = {
|
|
97
|
-
/**
|
|
98
|
-
* Parse movie header (mvhd) atom
|
|
99
|
-
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
|
|
100
|
-
*/
|
|
101
|
-
mvhd: async (len) => {
|
|
102
|
-
const
|
|
103
|
-
this.metadata.setFormat('creationTime',
|
|
104
|
-
this.metadata.setFormat('modificationTime',
|
|
105
|
-
},
|
|
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
|
-
chap: async (len) => {
|
|
120
|
-
const td = this.getTrackDescription();
|
|
121
|
-
const trackIds = [];
|
|
122
|
-
while (len >= Token.UINT32_BE.len) {
|
|
123
|
-
trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE));
|
|
124
|
-
len -= Token.UINT32_BE.len;
|
|
125
|
-
}
|
|
126
|
-
td.chapterList = trackIds;
|
|
127
|
-
},
|
|
128
|
-
tkhd: async (len) => {
|
|
129
|
-
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
|
130
|
-
this.tracks.push(track);
|
|
131
|
-
},
|
|
132
|
-
/**
|
|
133
|
-
* Parse mdat atom.
|
|
134
|
-
* Will scan for chapters
|
|
135
|
-
*/
|
|
136
|
-
mdat: async (len) => {
|
|
137
|
-
this.audioLengthInBytes = len;
|
|
138
|
-
this.calculateBitRate();
|
|
139
|
-
if (this.options.includeChapters) {
|
|
140
|
-
const trackWithChapters = this.tracks.filter(track => track.chapterList);
|
|
141
|
-
if (trackWithChapters.length === 1) {
|
|
142
|
-
const chapterTrackIds = trackWithChapters[0].chapterList;
|
|
143
|
-
const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
|
|
144
|
-
if (chapterTracks.length === 1) {
|
|
145
|
-
return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
await this.tokenizer.ignore(len);
|
|
150
|
-
},
|
|
151
|
-
ftyp: async (len) => {
|
|
152
|
-
const types = [];
|
|
153
|
-
while (len > 0) {
|
|
154
|
-
const ftype = await this.tokenizer.readToken(AtomToken.ftyp);
|
|
155
|
-
len -= AtomToken.ftyp.len;
|
|
156
|
-
const value = ftype.type.replace(/\W/g, '');
|
|
157
|
-
if (value.length > 0) {
|
|
158
|
-
types.push(value); // unshift for backward compatibility
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
debug(`ftyp: ${types.join('/')}`);
|
|
162
|
-
const x = types.filter(distinct).join('/');
|
|
163
|
-
this.metadata.setFormat('container', x);
|
|
164
|
-
},
|
|
165
|
-
/**
|
|
166
|
-
* Parse sample description atom
|
|
167
|
-
*/
|
|
168
|
-
stsd: async (len) => {
|
|
169
|
-
const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len));
|
|
170
|
-
const trackDescription = this.getTrackDescription();
|
|
171
|
-
trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
172
|
-
},
|
|
173
|
-
/**
|
|
174
|
-
* sample-to-Chunk Atoms
|
|
175
|
-
*/
|
|
176
|
-
stsc: async (len) => {
|
|
177
|
-
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
|
|
178
|
-
this.getTrackDescription().sampleToChunkTable = stsc.entries;
|
|
179
|
-
},
|
|
180
|
-
/**
|
|
181
|
-
* time to sample
|
|
182
|
-
*/
|
|
183
|
-
stts: async (len) => {
|
|
184
|
-
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
|
|
185
|
-
this.getTrackDescription().timeToSampleTable = stts.entries;
|
|
186
|
-
},
|
|
187
|
-
/**
|
|
188
|
-
* Parse sample-sizes atom ('stsz')
|
|
189
|
-
*/
|
|
190
|
-
stsz: async (len) => {
|
|
191
|
-
const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len));
|
|
192
|
-
const td = this.getTrackDescription();
|
|
193
|
-
td.sampleSize = stsz.sampleSize;
|
|
194
|
-
td.sampleSizeTable = stsz.entries;
|
|
195
|
-
},
|
|
196
|
-
/**
|
|
197
|
-
* Parse chunk-offset atom ('stco')
|
|
198
|
-
*/
|
|
199
|
-
stco: async (len) => {
|
|
200
|
-
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
|
|
201
|
-
this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
|
|
202
|
-
},
|
|
203
|
-
date: async (len) => {
|
|
204
|
-
const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
|
|
205
|
-
this.addTag('date', date);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
static read_BE_Integer(array, signed) {
|
|
210
|
-
const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : '');
|
|
211
|
-
const token = Token[integerType];
|
|
212
|
-
if (!token) {
|
|
213
|
-
throw new Error('Token for integer type not found: "' + integerType + '"');
|
|
214
|
-
}
|
|
215
|
-
return Number(token.get(array, 0));
|
|
216
|
-
}
|
|
217
|
-
async parse() {
|
|
218
|
-
this.tracks = [];
|
|
219
|
-
let remainingFileSize = this.tokenizer.fileInfo.size;
|
|
220
|
-
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
|
|
221
|
-
try {
|
|
222
|
-
const token = await this.tokenizer.peekToken(AtomToken.Header);
|
|
223
|
-
if (token.name === '\0\0\0\0') {
|
|
224
|
-
const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
|
|
225
|
-
debug(errMsg);
|
|
226
|
-
this.addWarning(errMsg);
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
|
|
232
|
-
debug(errMsg);
|
|
233
|
-
this.addWarning(errMsg);
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
const rootAtom = await Atom_1.Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
|
|
237
|
-
remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
|
|
238
|
-
}
|
|
239
|
-
// Post process metadata
|
|
240
|
-
const formatList = [];
|
|
241
|
-
this.tracks.forEach(track => {
|
|
242
|
-
const trackFormats = [];
|
|
243
|
-
track.soundSampleDescription.forEach(ssd => {
|
|
244
|
-
const streamInfo = {};
|
|
245
|
-
const encoderInfo = encoderDict[ssd.dataFormat];
|
|
246
|
-
if (encoderInfo) {
|
|
247
|
-
trackFormats.push(encoderInfo.format);
|
|
248
|
-
streamInfo.codecName = encoderInfo.format;
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
streamInfo.codecName = `<${ssd.dataFormat}>`;
|
|
252
|
-
}
|
|
253
|
-
if (ssd.description) {
|
|
254
|
-
const { description } = ssd;
|
|
255
|
-
if (description.sampleRate > 0) {
|
|
256
|
-
streamInfo.type = type_1.TrackType.audio;
|
|
257
|
-
streamInfo.audio = {
|
|
258
|
-
samplingFrequency: description.sampleRate,
|
|
259
|
-
bitDepth: description.sampleSize,
|
|
260
|
-
channels: description.numAudioChannels
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this.metadata.addStreamInfo(streamInfo);
|
|
265
|
-
});
|
|
266
|
-
if (trackFormats.length >= 1) {
|
|
267
|
-
formatList.push(trackFormats.join('/'));
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
if (formatList.length > 0) {
|
|
271
|
-
this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
|
|
272
|
-
}
|
|
273
|
-
const audioTracks = this.tracks.filter(track => {
|
|
274
|
-
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
|
|
275
|
-
});
|
|
276
|
-
if (audioTracks.length >= 1) {
|
|
277
|
-
const audioTrack = audioTracks[0];
|
|
278
|
-
const duration = audioTrack.duration / audioTrack.timeScale;
|
|
279
|
-
this.metadata.setFormat('duration', duration); // calculate duration in seconds
|
|
280
|
-
const ssd = audioTrack.soundSampleDescription[0];
|
|
281
|
-
if (ssd.description) {
|
|
282
|
-
this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
|
|
283
|
-
this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
|
|
284
|
-
this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
|
|
285
|
-
}
|
|
286
|
-
const encoderInfo = encoderDict[ssd.dataFormat];
|
|
287
|
-
if (encoderInfo) {
|
|
288
|
-
this.metadata.setFormat('lossless', !encoderInfo.lossy);
|
|
289
|
-
}
|
|
290
|
-
this.calculateBitRate();
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
async handleAtom(atom, remaining) {
|
|
294
|
-
if (atom.parent) {
|
|
295
|
-
switch (atom.parent.header.name) {
|
|
296
|
-
case 'ilst':
|
|
297
|
-
case '<id>':
|
|
298
|
-
return this.parseMetadataItemData(atom);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
// const payloadLength = atom.getPayloadLength(remaining);
|
|
302
|
-
if (this.atomParsers[atom.header.name]) {
|
|
303
|
-
return this.atomParsers[atom.header.name](remaining);
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
|
|
307
|
-
await this.tokenizer.ignore(remaining);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
getTrackDescription() {
|
|
311
|
-
return this.tracks[this.tracks.length - 1];
|
|
312
|
-
}
|
|
313
|
-
calculateBitRate() {
|
|
314
|
-
if (this.audioLengthInBytes && this.metadata.format.duration) {
|
|
315
|
-
this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
addTag(id, value) {
|
|
319
|
-
this.metadata.addTag(tagFormat, id, value);
|
|
320
|
-
}
|
|
321
|
-
addWarning(message) {
|
|
322
|
-
debug('Warning: ' + message);
|
|
323
|
-
this.metadata.addWarning(message);
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Parse data of Meta-item-list-atom (item of 'ilst' atom)
|
|
327
|
-
* @param metaAtom
|
|
328
|
-
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
|
329
|
-
*/
|
|
330
|
-
parseMetadataItemData(metaAtom) {
|
|
331
|
-
let tagKey = metaAtom.header.name;
|
|
332
|
-
return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
333
|
-
const payLoadLength = child.getPayloadLength(remaining);
|
|
334
|
-
switch (child.header.name) {
|
|
335
|
-
case 'data': // value atom
|
|
336
|
-
return this.parseValueAtom(tagKey, child);
|
|
337
|
-
case 'name': // name atom (optional)
|
|
338
|
-
const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
|
339
|
-
tagKey += ':' + name.name;
|
|
340
|
-
break;
|
|
341
|
-
case 'mean': // name atom (optional)
|
|
342
|
-
const mean = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
|
343
|
-
// console.log(" %s[%s] = %s", tagKey, header.name, mean.name);
|
|
344
|
-
tagKey += ':' + mean.name;
|
|
345
|
-
break;
|
|
346
|
-
default:
|
|
347
|
-
const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength));
|
|
348
|
-
this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii'));
|
|
349
|
-
}
|
|
350
|
-
}, metaAtom.getPayloadLength(0));
|
|
351
|
-
}
|
|
352
|
-
async parseValueAtom(tagKey, metaAtom) {
|
|
353
|
-
const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len));
|
|
354
|
-
if (dataAtom.type.set !== 0) {
|
|
355
|
-
throw new Error('Unsupported type-set != 0: ' + dataAtom.type.set);
|
|
356
|
-
}
|
|
357
|
-
// Use well-known-type table
|
|
358
|
-
// Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
|
359
|
-
switch (dataAtom.type.type) {
|
|
360
|
-
case 0: // reserved: Reserved for use where no type needs to be indicated
|
|
361
|
-
switch (tagKey) {
|
|
362
|
-
case 'trkn':
|
|
363
|
-
case 'disk':
|
|
364
|
-
const num = Token.UINT8.get(dataAtom.value, 3);
|
|
365
|
-
const of = Token.UINT8.get(dataAtom.value, 5);
|
|
366
|
-
// console.log(" %s[data] = %s/%s", tagKey, num, of);
|
|
367
|
-
this.addTag(tagKey, num + '/' + of);
|
|
368
|
-
break;
|
|
369
|
-
case 'gnre':
|
|
370
|
-
const genreInt = Token.UINT8.get(dataAtom.value, 1);
|
|
371
|
-
const genreStr = ID3v1Parser_1.Genres[genreInt - 1];
|
|
372
|
-
// console.log(" %s[data] = %s", tagKey, genreStr);
|
|
373
|
-
this.addTag(tagKey, genreStr);
|
|
374
|
-
break;
|
|
375
|
-
default:
|
|
376
|
-
// console.log(" reserved-data: name=%s, len=%s, set=%s, type=%s, locale=%s, value{ hex=%s, ascii=%s }",
|
|
377
|
-
// header.name, header.length, dataAtom.type.set, dataAtom.type.type, dataAtom.locale, dataAtom.value.toString('hex'), dataAtom.value.toString('ascii'));
|
|
378
|
-
}
|
|
379
|
-
break;
|
|
380
|
-
case 1: // UTF-8: Without any count or NULL terminator
|
|
381
|
-
case 18: // Unknown: Found in m4b in combination with a '©gen' tag
|
|
382
|
-
this.addTag(tagKey, dataAtom.value.toString('utf-8'));
|
|
383
|
-
break;
|
|
384
|
-
case 13: // JPEG
|
|
385
|
-
if (this.options.skipCovers)
|
|
386
|
-
break;
|
|
387
|
-
this.addTag(tagKey, {
|
|
388
|
-
format: 'image/jpeg',
|
|
389
|
-
data: Buffer.from(dataAtom.value)
|
|
390
|
-
});
|
|
391
|
-
break;
|
|
392
|
-
case 14: // PNG
|
|
393
|
-
if (this.options.skipCovers)
|
|
394
|
-
break;
|
|
395
|
-
this.addTag(tagKey, {
|
|
396
|
-
format: 'image/png',
|
|
397
|
-
data: Buffer.from(dataAtom.value)
|
|
398
|
-
});
|
|
399
|
-
break;
|
|
400
|
-
case 21: // BE Signed Integer
|
|
401
|
-
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true));
|
|
402
|
-
break;
|
|
403
|
-
case 22: // BE Unsigned Integer
|
|
404
|
-
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false));
|
|
405
|
-
break;
|
|
406
|
-
case 65: // An 8-bit signed integer
|
|
407
|
-
this.addTag(tagKey, dataAtom.value.readInt8(0));
|
|
408
|
-
break;
|
|
409
|
-
case 66: // A big-endian 16-bit signed integer
|
|
410
|
-
this.addTag(tagKey, dataAtom.value.readInt16BE(0));
|
|
411
|
-
break;
|
|
412
|
-
case 67: // A big-endian 32-bit signed integer
|
|
413
|
-
this.addTag(tagKey, dataAtom.value.readInt32BE(0));
|
|
414
|
-
break;
|
|
415
|
-
default:
|
|
416
|
-
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* @param sampleDescription
|
|
421
|
-
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
|
422
|
-
*/
|
|
423
|
-
parseSoundSampleDescription(sampleDescription) {
|
|
424
|
-
const ssd = {
|
|
425
|
-
dataFormat: sampleDescription.dataFormat,
|
|
426
|
-
dataReferenceIndex: sampleDescription.dataReferenceIndex
|
|
427
|
-
};
|
|
428
|
-
let offset = 0;
|
|
429
|
-
const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
|
|
430
|
-
offset += AtomToken.SoundSampleDescriptionVersion.len;
|
|
431
|
-
if (version.version === 0 || version.version === 1) {
|
|
432
|
-
// Sound Sample Description (Version 0)
|
|
433
|
-
ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset);
|
|
434
|
-
}
|
|
435
|
-
else {
|
|
436
|
-
debug(`Warning: sound-sample-description ${version} not implemented`);
|
|
437
|
-
}
|
|
438
|
-
return ssd;
|
|
439
|
-
}
|
|
440
|
-
async parseChapterTrack(chapterTrack, track, len) {
|
|
441
|
-
if (!chapterTrack.sampleSize) {
|
|
442
|
-
if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length)
|
|
443
|
-
throw new Error('Expected equal chunk-offset-table & sample-size-table length.');
|
|
444
|
-
}
|
|
445
|
-
const chapters = [];
|
|
446
|
-
for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
|
|
447
|
-
const chunkOffset = chapterTrack.chunkOffsetTable[i];
|
|
448
|
-
const nextChunkLen = chunkOffset - this.tokenizer.position;
|
|
449
|
-
const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
|
|
450
|
-
len -= nextChunkLen + sampleSize;
|
|
451
|
-
if (len < 0)
|
|
452
|
-
throw new Error('Chapter chunk exceeding token length');
|
|
453
|
-
await this.tokenizer.ignore(nextChunkLen);
|
|
454
|
-
const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize));
|
|
455
|
-
debug(`Chapter ${i + 1}: ${title}`);
|
|
456
|
-
const chapter = {
|
|
457
|
-
title,
|
|
458
|
-
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
|
|
459
|
-
};
|
|
460
|
-
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
|
|
461
|
-
chapters.push(chapter);
|
|
462
|
-
}
|
|
463
|
-
this.metadata.setFormat('chapters', chapters);
|
|
464
|
-
await this.tokenizer.ignore(len);
|
|
465
|
-
}
|
|
466
|
-
findSampleOffset(track, chapterOffset) {
|
|
467
|
-
let totalDuration = 0;
|
|
468
|
-
track.timeToSampleTable.forEach(e => {
|
|
469
|
-
totalDuration += e.count * e.duration;
|
|
470
|
-
});
|
|
471
|
-
debug(`Total duration=${totalDuration}`);
|
|
472
|
-
let chunkIndex = 0;
|
|
473
|
-
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
|
|
474
|
-
++chunkIndex;
|
|
475
|
-
}
|
|
476
|
-
return this.getChunkDuration(chunkIndex + 1, track);
|
|
477
|
-
}
|
|
478
|
-
getChunkDuration(chunkId, track) {
|
|
479
|
-
let ttsi = 0;
|
|
480
|
-
let ttsc = track.timeToSampleTable[ttsi].count;
|
|
481
|
-
let ttsd = track.timeToSampleTable[ttsi].duration;
|
|
482
|
-
let curChunkId = 1;
|
|
483
|
-
let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
|
484
|
-
let totalDuration = 0;
|
|
485
|
-
while (curChunkId < chunkId) {
|
|
486
|
-
const nrOfSamples = Math.min(ttsc, samplesPerChunk);
|
|
487
|
-
totalDuration += nrOfSamples * ttsd;
|
|
488
|
-
ttsc -= nrOfSamples;
|
|
489
|
-
samplesPerChunk -= nrOfSamples;
|
|
490
|
-
if (samplesPerChunk === 0) {
|
|
491
|
-
++curChunkId;
|
|
492
|
-
samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
++ttsi;
|
|
496
|
-
ttsc = track.timeToSampleTable[ttsi].count;
|
|
497
|
-
ttsd = track.timeToSampleTable[ttsi].duration;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return totalDuration;
|
|
501
|
-
}
|
|
502
|
-
getSamplesPerChunk(chunkId, stcTable) {
|
|
503
|
-
for (let i = 0; i < stcTable.length - 1; ++i) {
|
|
504
|
-
if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) {
|
|
505
|
-
return stcTable[i].samplesPerChunk;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return stcTable[stcTable.length - 1].samplesPerChunk;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
exports.MP4Parser = MP4Parser;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MP4Parser = void 0;
|
|
4
|
+
const initDebug = require("debug");
|
|
5
|
+
const Token = require("token-types");
|
|
6
|
+
const BasicParser_1 = require("../common/BasicParser");
|
|
7
|
+
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
|
8
|
+
const type_1 = require("../type");
|
|
9
|
+
const Atom_1 = require("./Atom");
|
|
10
|
+
const AtomToken = require("./AtomToken");
|
|
11
|
+
const debug = initDebug('music-metadata:parser:MP4');
|
|
12
|
+
const tagFormat = 'iTunes';
|
|
13
|
+
const encoderDict = {
|
|
14
|
+
raw: {
|
|
15
|
+
lossy: false,
|
|
16
|
+
format: 'raw'
|
|
17
|
+
},
|
|
18
|
+
MAC3: {
|
|
19
|
+
lossy: true,
|
|
20
|
+
format: 'MACE 3:1'
|
|
21
|
+
},
|
|
22
|
+
MAC6: {
|
|
23
|
+
lossy: true,
|
|
24
|
+
format: 'MACE 6:1'
|
|
25
|
+
},
|
|
26
|
+
ima4: {
|
|
27
|
+
lossy: true,
|
|
28
|
+
format: 'IMA 4:1'
|
|
29
|
+
},
|
|
30
|
+
ulaw: {
|
|
31
|
+
lossy: true,
|
|
32
|
+
format: 'uLaw 2:1'
|
|
33
|
+
},
|
|
34
|
+
alaw: {
|
|
35
|
+
lossy: true,
|
|
36
|
+
format: 'uLaw 2:1'
|
|
37
|
+
},
|
|
38
|
+
Qclp: {
|
|
39
|
+
lossy: true,
|
|
40
|
+
format: 'QUALCOMM PureVoice'
|
|
41
|
+
},
|
|
42
|
+
'.mp3': {
|
|
43
|
+
lossy: true,
|
|
44
|
+
format: 'MPEG-1 layer 3'
|
|
45
|
+
},
|
|
46
|
+
alac: {
|
|
47
|
+
lossy: false,
|
|
48
|
+
format: 'ALAC'
|
|
49
|
+
},
|
|
50
|
+
'ac-3': {
|
|
51
|
+
lossy: true,
|
|
52
|
+
format: 'AC-3'
|
|
53
|
+
},
|
|
54
|
+
mp4a: {
|
|
55
|
+
lossy: true,
|
|
56
|
+
format: 'MPEG-4/AAC'
|
|
57
|
+
},
|
|
58
|
+
mp4s: {
|
|
59
|
+
lossy: true,
|
|
60
|
+
format: 'MP4S'
|
|
61
|
+
},
|
|
62
|
+
// Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87
|
|
63
|
+
c608: {
|
|
64
|
+
lossy: true,
|
|
65
|
+
format: 'CEA-608'
|
|
66
|
+
},
|
|
67
|
+
c708: {
|
|
68
|
+
lossy: true,
|
|
69
|
+
format: 'CEA-708'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function distinct(value, index, self) {
|
|
73
|
+
return self.indexOf(value) === index;
|
|
74
|
+
}
|
|
75
|
+
/*
|
|
76
|
+
* Parser for the MP4 (MPEG-4 Part 14) container format
|
|
77
|
+
* Standard: ISO/IEC 14496-14
|
|
78
|
+
* supporting:
|
|
79
|
+
* - QuickTime container
|
|
80
|
+
* - MP4 File Format
|
|
81
|
+
* - 3GPP file format
|
|
82
|
+
* - 3GPP2 file format
|
|
83
|
+
*
|
|
84
|
+
* MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension.
|
|
85
|
+
* Support for Apple iTunes tags as found in a M4A/M4V files.
|
|
86
|
+
* Ref:
|
|
87
|
+
* https://en.wikipedia.org/wiki/ISO_base_media_file_format
|
|
88
|
+
* https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
|
|
89
|
+
* http://atomicparsley.sourceforge.net/mpeg-4files.html
|
|
90
|
+
* https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
|
|
91
|
+
* https://wiki.multimedia.cx/index.php/QuickTime_container
|
|
92
|
+
*/
|
|
93
|
+
class MP4Parser extends BasicParser_1.BasicParser {
|
|
94
|
+
constructor() {
|
|
95
|
+
super(...arguments);
|
|
96
|
+
this.atomParsers = {
|
|
97
|
+
/**
|
|
98
|
+
* Parse movie header (mvhd) atom
|
|
99
|
+
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
|
|
100
|
+
*/
|
|
101
|
+
mvhd: async (len) => {
|
|
102
|
+
const mvhd = await this.tokenizer.readToken(new AtomToken.MvhdAtom(len));
|
|
103
|
+
this.metadata.setFormat('creationTime', mvhd.creationTime);
|
|
104
|
+
this.metadata.setFormat('modificationTime', mvhd.modificationTime);
|
|
105
|
+
},
|
|
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
|
+
chap: async (len) => {
|
|
120
|
+
const td = this.getTrackDescription();
|
|
121
|
+
const trackIds = [];
|
|
122
|
+
while (len >= Token.UINT32_BE.len) {
|
|
123
|
+
trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE));
|
|
124
|
+
len -= Token.UINT32_BE.len;
|
|
125
|
+
}
|
|
126
|
+
td.chapterList = trackIds;
|
|
127
|
+
},
|
|
128
|
+
tkhd: async (len) => {
|
|
129
|
+
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
|
130
|
+
this.tracks.push(track);
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* Parse mdat atom.
|
|
134
|
+
* Will scan for chapters
|
|
135
|
+
*/
|
|
136
|
+
mdat: async (len) => {
|
|
137
|
+
this.audioLengthInBytes = len;
|
|
138
|
+
this.calculateBitRate();
|
|
139
|
+
if (this.options.includeChapters) {
|
|
140
|
+
const trackWithChapters = this.tracks.filter(track => track.chapterList);
|
|
141
|
+
if (trackWithChapters.length === 1) {
|
|
142
|
+
const chapterTrackIds = trackWithChapters[0].chapterList;
|
|
143
|
+
const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
|
|
144
|
+
if (chapterTracks.length === 1) {
|
|
145
|
+
return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await this.tokenizer.ignore(len);
|
|
150
|
+
},
|
|
151
|
+
ftyp: async (len) => {
|
|
152
|
+
const types = [];
|
|
153
|
+
while (len > 0) {
|
|
154
|
+
const ftype = await this.tokenizer.readToken(AtomToken.ftyp);
|
|
155
|
+
len -= AtomToken.ftyp.len;
|
|
156
|
+
const value = ftype.type.replace(/\W/g, '');
|
|
157
|
+
if (value.length > 0) {
|
|
158
|
+
types.push(value); // unshift for backward compatibility
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
debug(`ftyp: ${types.join('/')}`);
|
|
162
|
+
const x = types.filter(distinct).join('/');
|
|
163
|
+
this.metadata.setFormat('container', x);
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Parse sample description atom
|
|
167
|
+
*/
|
|
168
|
+
stsd: async (len) => {
|
|
169
|
+
const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len));
|
|
170
|
+
const trackDescription = this.getTrackDescription();
|
|
171
|
+
trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* sample-to-Chunk Atoms
|
|
175
|
+
*/
|
|
176
|
+
stsc: async (len) => {
|
|
177
|
+
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
|
|
178
|
+
this.getTrackDescription().sampleToChunkTable = stsc.entries;
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* time to sample
|
|
182
|
+
*/
|
|
183
|
+
stts: async (len) => {
|
|
184
|
+
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
|
|
185
|
+
this.getTrackDescription().timeToSampleTable = stts.entries;
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Parse sample-sizes atom ('stsz')
|
|
189
|
+
*/
|
|
190
|
+
stsz: async (len) => {
|
|
191
|
+
const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len));
|
|
192
|
+
const td = this.getTrackDescription();
|
|
193
|
+
td.sampleSize = stsz.sampleSize;
|
|
194
|
+
td.sampleSizeTable = stsz.entries;
|
|
195
|
+
},
|
|
196
|
+
/**
|
|
197
|
+
* Parse chunk-offset atom ('stco')
|
|
198
|
+
*/
|
|
199
|
+
stco: async (len) => {
|
|
200
|
+
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
|
|
201
|
+
this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
|
|
202
|
+
},
|
|
203
|
+
date: async (len) => {
|
|
204
|
+
const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
|
|
205
|
+
this.addTag('date', date);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
static read_BE_Integer(array, signed) {
|
|
210
|
+
const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : '');
|
|
211
|
+
const token = Token[integerType];
|
|
212
|
+
if (!token) {
|
|
213
|
+
throw new Error('Token for integer type not found: "' + integerType + '"');
|
|
214
|
+
}
|
|
215
|
+
return Number(token.get(array, 0));
|
|
216
|
+
}
|
|
217
|
+
async parse() {
|
|
218
|
+
this.tracks = [];
|
|
219
|
+
let remainingFileSize = this.tokenizer.fileInfo.size;
|
|
220
|
+
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
|
|
221
|
+
try {
|
|
222
|
+
const token = await this.tokenizer.peekToken(AtomToken.Header);
|
|
223
|
+
if (token.name === '\0\0\0\0') {
|
|
224
|
+
const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
|
|
225
|
+
debug(errMsg);
|
|
226
|
+
this.addWarning(errMsg);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
|
|
232
|
+
debug(errMsg);
|
|
233
|
+
this.addWarning(errMsg);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
const rootAtom = await Atom_1.Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
|
|
237
|
+
remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
|
|
238
|
+
}
|
|
239
|
+
// Post process metadata
|
|
240
|
+
const formatList = [];
|
|
241
|
+
this.tracks.forEach(track => {
|
|
242
|
+
const trackFormats = [];
|
|
243
|
+
track.soundSampleDescription.forEach(ssd => {
|
|
244
|
+
const streamInfo = {};
|
|
245
|
+
const encoderInfo = encoderDict[ssd.dataFormat];
|
|
246
|
+
if (encoderInfo) {
|
|
247
|
+
trackFormats.push(encoderInfo.format);
|
|
248
|
+
streamInfo.codecName = encoderInfo.format;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
streamInfo.codecName = `<${ssd.dataFormat}>`;
|
|
252
|
+
}
|
|
253
|
+
if (ssd.description) {
|
|
254
|
+
const { description } = ssd;
|
|
255
|
+
if (description.sampleRate > 0) {
|
|
256
|
+
streamInfo.type = type_1.TrackType.audio;
|
|
257
|
+
streamInfo.audio = {
|
|
258
|
+
samplingFrequency: description.sampleRate,
|
|
259
|
+
bitDepth: description.sampleSize,
|
|
260
|
+
channels: description.numAudioChannels
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this.metadata.addStreamInfo(streamInfo);
|
|
265
|
+
});
|
|
266
|
+
if (trackFormats.length >= 1) {
|
|
267
|
+
formatList.push(trackFormats.join('/'));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
if (formatList.length > 0) {
|
|
271
|
+
this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
|
|
272
|
+
}
|
|
273
|
+
const audioTracks = this.tracks.filter(track => {
|
|
274
|
+
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
|
|
275
|
+
});
|
|
276
|
+
if (audioTracks.length >= 1) {
|
|
277
|
+
const audioTrack = audioTracks[0];
|
|
278
|
+
const duration = audioTrack.duration / audioTrack.timeScale;
|
|
279
|
+
this.metadata.setFormat('duration', duration); // calculate duration in seconds
|
|
280
|
+
const ssd = audioTrack.soundSampleDescription[0];
|
|
281
|
+
if (ssd.description) {
|
|
282
|
+
this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
|
|
283
|
+
this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
|
|
284
|
+
this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
|
|
285
|
+
}
|
|
286
|
+
const encoderInfo = encoderDict[ssd.dataFormat];
|
|
287
|
+
if (encoderInfo) {
|
|
288
|
+
this.metadata.setFormat('lossless', !encoderInfo.lossy);
|
|
289
|
+
}
|
|
290
|
+
this.calculateBitRate();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async handleAtom(atom, remaining) {
|
|
294
|
+
if (atom.parent) {
|
|
295
|
+
switch (atom.parent.header.name) {
|
|
296
|
+
case 'ilst':
|
|
297
|
+
case '<id>':
|
|
298
|
+
return this.parseMetadataItemData(atom);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// const payloadLength = atom.getPayloadLength(remaining);
|
|
302
|
+
if (this.atomParsers[atom.header.name]) {
|
|
303
|
+
return this.atomParsers[atom.header.name](remaining);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
|
|
307
|
+
await this.tokenizer.ignore(remaining);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
getTrackDescription() {
|
|
311
|
+
return this.tracks[this.tracks.length - 1];
|
|
312
|
+
}
|
|
313
|
+
calculateBitRate() {
|
|
314
|
+
if (this.audioLengthInBytes && this.metadata.format.duration) {
|
|
315
|
+
this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
addTag(id, value) {
|
|
319
|
+
this.metadata.addTag(tagFormat, id, value);
|
|
320
|
+
}
|
|
321
|
+
addWarning(message) {
|
|
322
|
+
debug('Warning: ' + message);
|
|
323
|
+
this.metadata.addWarning(message);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Parse data of Meta-item-list-atom (item of 'ilst' atom)
|
|
327
|
+
* @param metaAtom
|
|
328
|
+
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
|
329
|
+
*/
|
|
330
|
+
parseMetadataItemData(metaAtom) {
|
|
331
|
+
let tagKey = metaAtom.header.name;
|
|
332
|
+
return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
|
|
333
|
+
const payLoadLength = child.getPayloadLength(remaining);
|
|
334
|
+
switch (child.header.name) {
|
|
335
|
+
case 'data': // value atom
|
|
336
|
+
return this.parseValueAtom(tagKey, child);
|
|
337
|
+
case 'name': // name atom (optional)
|
|
338
|
+
const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
|
339
|
+
tagKey += ':' + name.name;
|
|
340
|
+
break;
|
|
341
|
+
case 'mean': // name atom (optional)
|
|
342
|
+
const mean = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
|
343
|
+
// console.log(" %s[%s] = %s", tagKey, header.name, mean.name);
|
|
344
|
+
tagKey += ':' + mean.name;
|
|
345
|
+
break;
|
|
346
|
+
default:
|
|
347
|
+
const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength));
|
|
348
|
+
this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii'));
|
|
349
|
+
}
|
|
350
|
+
}, metaAtom.getPayloadLength(0));
|
|
351
|
+
}
|
|
352
|
+
async parseValueAtom(tagKey, metaAtom) {
|
|
353
|
+
const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len));
|
|
354
|
+
if (dataAtom.type.set !== 0) {
|
|
355
|
+
throw new Error('Unsupported type-set != 0: ' + dataAtom.type.set);
|
|
356
|
+
}
|
|
357
|
+
// Use well-known-type table
|
|
358
|
+
// Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
|
359
|
+
switch (dataAtom.type.type) {
|
|
360
|
+
case 0: // reserved: Reserved for use where no type needs to be indicated
|
|
361
|
+
switch (tagKey) {
|
|
362
|
+
case 'trkn':
|
|
363
|
+
case 'disk':
|
|
364
|
+
const num = Token.UINT8.get(dataAtom.value, 3);
|
|
365
|
+
const of = Token.UINT8.get(dataAtom.value, 5);
|
|
366
|
+
// console.log(" %s[data] = %s/%s", tagKey, num, of);
|
|
367
|
+
this.addTag(tagKey, num + '/' + of);
|
|
368
|
+
break;
|
|
369
|
+
case 'gnre':
|
|
370
|
+
const genreInt = Token.UINT8.get(dataAtom.value, 1);
|
|
371
|
+
const genreStr = ID3v1Parser_1.Genres[genreInt - 1];
|
|
372
|
+
// console.log(" %s[data] = %s", tagKey, genreStr);
|
|
373
|
+
this.addTag(tagKey, genreStr);
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
// console.log(" reserved-data: name=%s, len=%s, set=%s, type=%s, locale=%s, value{ hex=%s, ascii=%s }",
|
|
377
|
+
// header.name, header.length, dataAtom.type.set, dataAtom.type.type, dataAtom.locale, dataAtom.value.toString('hex'), dataAtom.value.toString('ascii'));
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
case 1: // UTF-8: Without any count or NULL terminator
|
|
381
|
+
case 18: // Unknown: Found in m4b in combination with a '©gen' tag
|
|
382
|
+
this.addTag(tagKey, dataAtom.value.toString('utf-8'));
|
|
383
|
+
break;
|
|
384
|
+
case 13: // JPEG
|
|
385
|
+
if (this.options.skipCovers)
|
|
386
|
+
break;
|
|
387
|
+
this.addTag(tagKey, {
|
|
388
|
+
format: 'image/jpeg',
|
|
389
|
+
data: Buffer.from(dataAtom.value)
|
|
390
|
+
});
|
|
391
|
+
break;
|
|
392
|
+
case 14: // PNG
|
|
393
|
+
if (this.options.skipCovers)
|
|
394
|
+
break;
|
|
395
|
+
this.addTag(tagKey, {
|
|
396
|
+
format: 'image/png',
|
|
397
|
+
data: Buffer.from(dataAtom.value)
|
|
398
|
+
});
|
|
399
|
+
break;
|
|
400
|
+
case 21: // BE Signed Integer
|
|
401
|
+
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true));
|
|
402
|
+
break;
|
|
403
|
+
case 22: // BE Unsigned Integer
|
|
404
|
+
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false));
|
|
405
|
+
break;
|
|
406
|
+
case 65: // An 8-bit signed integer
|
|
407
|
+
this.addTag(tagKey, dataAtom.value.readInt8(0));
|
|
408
|
+
break;
|
|
409
|
+
case 66: // A big-endian 16-bit signed integer
|
|
410
|
+
this.addTag(tagKey, dataAtom.value.readInt16BE(0));
|
|
411
|
+
break;
|
|
412
|
+
case 67: // A big-endian 32-bit signed integer
|
|
413
|
+
this.addTag(tagKey, dataAtom.value.readInt32BE(0));
|
|
414
|
+
break;
|
|
415
|
+
default:
|
|
416
|
+
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* @param sampleDescription
|
|
421
|
+
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
|
422
|
+
*/
|
|
423
|
+
parseSoundSampleDescription(sampleDescription) {
|
|
424
|
+
const ssd = {
|
|
425
|
+
dataFormat: sampleDescription.dataFormat,
|
|
426
|
+
dataReferenceIndex: sampleDescription.dataReferenceIndex
|
|
427
|
+
};
|
|
428
|
+
let offset = 0;
|
|
429
|
+
const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
|
|
430
|
+
offset += AtomToken.SoundSampleDescriptionVersion.len;
|
|
431
|
+
if (version.version === 0 || version.version === 1) {
|
|
432
|
+
// Sound Sample Description (Version 0)
|
|
433
|
+
ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
debug(`Warning: sound-sample-description ${version} not implemented`);
|
|
437
|
+
}
|
|
438
|
+
return ssd;
|
|
439
|
+
}
|
|
440
|
+
async parseChapterTrack(chapterTrack, track, len) {
|
|
441
|
+
if (!chapterTrack.sampleSize) {
|
|
442
|
+
if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length)
|
|
443
|
+
throw new Error('Expected equal chunk-offset-table & sample-size-table length.');
|
|
444
|
+
}
|
|
445
|
+
const chapters = [];
|
|
446
|
+
for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
|
|
447
|
+
const chunkOffset = chapterTrack.chunkOffsetTable[i];
|
|
448
|
+
const nextChunkLen = chunkOffset - this.tokenizer.position;
|
|
449
|
+
const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
|
|
450
|
+
len -= nextChunkLen + sampleSize;
|
|
451
|
+
if (len < 0)
|
|
452
|
+
throw new Error('Chapter chunk exceeding token length');
|
|
453
|
+
await this.tokenizer.ignore(nextChunkLen);
|
|
454
|
+
const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize));
|
|
455
|
+
debug(`Chapter ${i + 1}: ${title}`);
|
|
456
|
+
const chapter = {
|
|
457
|
+
title,
|
|
458
|
+
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
|
|
459
|
+
};
|
|
460
|
+
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
|
|
461
|
+
chapters.push(chapter);
|
|
462
|
+
}
|
|
463
|
+
this.metadata.setFormat('chapters', chapters);
|
|
464
|
+
await this.tokenizer.ignore(len);
|
|
465
|
+
}
|
|
466
|
+
findSampleOffset(track, chapterOffset) {
|
|
467
|
+
let totalDuration = 0;
|
|
468
|
+
track.timeToSampleTable.forEach(e => {
|
|
469
|
+
totalDuration += e.count * e.duration;
|
|
470
|
+
});
|
|
471
|
+
debug(`Total duration=${totalDuration}`);
|
|
472
|
+
let chunkIndex = 0;
|
|
473
|
+
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
|
|
474
|
+
++chunkIndex;
|
|
475
|
+
}
|
|
476
|
+
return this.getChunkDuration(chunkIndex + 1, track);
|
|
477
|
+
}
|
|
478
|
+
getChunkDuration(chunkId, track) {
|
|
479
|
+
let ttsi = 0;
|
|
480
|
+
let ttsc = track.timeToSampleTable[ttsi].count;
|
|
481
|
+
let ttsd = track.timeToSampleTable[ttsi].duration;
|
|
482
|
+
let curChunkId = 1;
|
|
483
|
+
let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
|
484
|
+
let totalDuration = 0;
|
|
485
|
+
while (curChunkId < chunkId) {
|
|
486
|
+
const nrOfSamples = Math.min(ttsc, samplesPerChunk);
|
|
487
|
+
totalDuration += nrOfSamples * ttsd;
|
|
488
|
+
ttsc -= nrOfSamples;
|
|
489
|
+
samplesPerChunk -= nrOfSamples;
|
|
490
|
+
if (samplesPerChunk === 0) {
|
|
491
|
+
++curChunkId;
|
|
492
|
+
samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
++ttsi;
|
|
496
|
+
ttsc = track.timeToSampleTable[ttsi].count;
|
|
497
|
+
ttsd = track.timeToSampleTable[ttsi].duration;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return totalDuration;
|
|
501
|
+
}
|
|
502
|
+
getSamplesPerChunk(chunkId, stcTable) {
|
|
503
|
+
for (let i = 0; i < stcTable.length - 1; ++i) {
|
|
504
|
+
if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) {
|
|
505
|
+
return stcTable[i].samplesPerChunk;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return stcTable[stcTable.length - 1].samplesPerChunk;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
exports.MP4Parser = MP4Parser;
|