music-metadata 12.2.0 → 12.2.2
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/README.md +10 -0
- package/lib/ParserFactory.d.ts +1 -1
- package/lib/ParserFactory.js +4 -3
- package/lib/aiff/AiffParser.js +2 -5
- package/lib/common/Allocation.js +2 -2
- package/lib/common/CombinedTagMapper.js +0 -1
- package/lib/common/MetadataCollector.d.ts +5 -5
- package/lib/common/MetadataCollector.js +12 -4
- package/lib/dsdiff/DsdiffLoader.js +1 -1
- package/lib/dsdiff/DsdiffParser.js +2 -6
- package/lib/dsf/DsfParser.js +1 -1
- package/lib/ebml/EbmlIterator.d.ts +20 -15
- package/lib/ebml/EbmlIterator.js +75 -38
- package/lib/ebml/types.d.ts +36 -8
- package/lib/ebml/types.js +2 -1
- package/lib/flac/FlacParser.js +4 -12
- package/lib/id3v1/ID3v1Parser.d.ts +1 -1
- package/lib/id3v1/ID3v1Parser.js +2 -1
- package/lib/id3v2/EmbeddedID3v2.d.ts +5 -0
- package/lib/id3v2/EmbeddedID3v2.js +29 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +6 -1
- package/lib/matroska/MatroskaDtd.d.ts +630 -2
- package/lib/matroska/MatroskaDtd.js +57 -40
- package/lib/matroska/MatroskaParser.js +119 -55
- package/lib/matroska/types.d.ts +31 -106
- package/lib/mp4/MP4Parser.js +13 -3
- package/lib/mpeg/MpegParser.js +2 -2
- package/lib/ogg/OggStreamMetadata.d.ts +2 -2
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +10 -0
- package/lib/ogg/vorbis/VorbisDecoder.js +15 -0
- package/lib/ogg/vorbis/VorbisStream.js +3 -9
- package/lib/type.d.ts +4 -2
- package/lib/wav/WaveParser.js +2 -5
- package/package.json +5 -5
|
@@ -4,6 +4,10 @@ import { DataType } from '../ebml/types.js';
|
|
|
4
4
|
* Derived from https://github.com/tungol/EBML/blob/master/doctypes/matroska.dtd
|
|
5
5
|
* Extended with:
|
|
6
6
|
* - https://www.matroska.org/technical/specs/index.html
|
|
7
|
+
* Cardinality and paths audited against:
|
|
8
|
+
* https://github.com/ietf-wg-cellar/matroska-specification/blob/master/ebml_matroska.xml
|
|
9
|
+
* UID integers deliberately decode as bytes to preserve all 64 bits.
|
|
10
|
+
* Recursive ChapterAtom/SimpleTag children are not expanded by this metadata subset.
|
|
7
11
|
*/
|
|
8
12
|
export const matroskaDtd = {
|
|
9
13
|
name: 'dtd',
|
|
@@ -28,6 +32,7 @@ export const matroskaDtd = {
|
|
|
28
32
|
// Meta Seek Information (also known as MetaSeek)
|
|
29
33
|
0x114d9b74: {
|
|
30
34
|
name: 'seekHead',
|
|
35
|
+
multiple: true,
|
|
31
36
|
container: {
|
|
32
37
|
0x4dbb: {
|
|
33
38
|
name: 'seek',
|
|
@@ -51,7 +56,7 @@ export const matroskaDtd = {
|
|
|
51
56
|
0x3e83bb: { name: 'nextFilename', value: DataType.string },
|
|
52
57
|
0x2ad7b1: { name: 'timecodeScale', value: DataType.uint },
|
|
53
58
|
0x4489: { name: 'duration', value: DataType.float },
|
|
54
|
-
0x4461: { name: 'dateUTC', value: DataType.
|
|
59
|
+
0x4461: { name: 'dateUTC', value: DataType.int },
|
|
55
60
|
0x7ba9: { name: 'title', value: DataType.string },
|
|
56
61
|
0x4d80: { name: 'muxingApp', value: DataType.string },
|
|
57
62
|
0x5741: { name: 'writingApp', value: DataType.string }
|
|
@@ -62,12 +67,17 @@ export const matroskaDtd = {
|
|
|
62
67
|
name: 'cluster',
|
|
63
68
|
multiple: true,
|
|
64
69
|
container: {
|
|
65
|
-
0xe7: { name: 'timecode', value: DataType.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
0xe7: { name: 'timecode', value: DataType.uint },
|
|
71
|
+
0x5854: {
|
|
72
|
+
name: 'silentTracks',
|
|
73
|
+
container: {
|
|
74
|
+
0x58d7: { name: 'trackNumber', value: DataType.uint, multiple: true }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
0xa7: { name: 'position', value: DataType.uint },
|
|
78
|
+
0xab: { name: 'prevSize', value: DataType.uint },
|
|
79
|
+
0xa0: { name: 'blockGroup', multiple: true },
|
|
80
|
+
0xa3: { name: 'simpleBlock', value: DataType.binary, multiple: true }
|
|
71
81
|
}
|
|
72
82
|
},
|
|
73
83
|
// Track
|
|
@@ -86,9 +96,9 @@ export const matroskaDtd = {
|
|
|
86
96
|
0x55aa: { name: 'flagForced', value: DataType.bool }, // extended
|
|
87
97
|
0x9c: { name: 'flagLacing', value: DataType.bool },
|
|
88
98
|
0x6de7: { name: 'minCache', value: DataType.uint },
|
|
89
|
-
|
|
99
|
+
0x6df8: { name: 'maxCache', value: DataType.uint },
|
|
90
100
|
0x23e383: { name: 'defaultDuration', value: DataType.uint },
|
|
91
|
-
0x23314f: { name: '
|
|
101
|
+
0x23314f: { name: 'trackTimecodeScale', value: DataType.float },
|
|
92
102
|
0x536e: { name: 'name', value: DataType.string },
|
|
93
103
|
0x22b59c: { name: 'language', value: DataType.string },
|
|
94
104
|
0x22b59d: { name: 'languageIETF', value: DataType.string },
|
|
@@ -96,22 +106,23 @@ export const matroskaDtd = {
|
|
|
96
106
|
0x63a2: { name: 'codecPrivate', value: DataType.binary },
|
|
97
107
|
0x258688: { name: 'codecName', value: DataType.string },
|
|
98
108
|
0x3a9697: { name: 'codecSettings', value: DataType.string },
|
|
99
|
-
0x3b4040: { name: 'codecInfoUrl', value: DataType.string },
|
|
100
|
-
0x26b240: { name: 'codecDownloadUrl', value: DataType.string },
|
|
109
|
+
0x3b4040: { name: 'codecInfoUrl', value: DataType.string, multiple: true },
|
|
110
|
+
0x26b240: { name: 'codecDownloadUrl', value: DataType.string, multiple: true },
|
|
101
111
|
0xaa: { name: 'codecDecodeAll', value: DataType.bool },
|
|
102
|
-
0x6fab: { name: 'trackOverlay', value: DataType.uint },
|
|
112
|
+
0x6fab: { name: 'trackOverlay', value: DataType.uint, multiple: true },
|
|
103
113
|
// Video
|
|
104
114
|
0xe0: {
|
|
105
115
|
name: 'video',
|
|
106
116
|
container: {
|
|
107
|
-
0x9a: { name: 'flagInterlaced', value: DataType.
|
|
117
|
+
0x9a: { name: 'flagInterlaced', value: DataType.uint },
|
|
108
118
|
0x53b8: { name: 'stereoMode', value: DataType.uint },
|
|
109
119
|
0xb0: { name: 'pixelWidth', value: DataType.uint },
|
|
110
120
|
0xba: { name: 'pixelHeight', value: DataType.uint },
|
|
111
121
|
0x54b0: { name: 'displayWidth', value: DataType.uint },
|
|
112
122
|
0x54ba: { name: 'displayHeight', value: DataType.uint },
|
|
123
|
+
0x54b2: { name: 'displayUnit', value: DataType.uint },
|
|
113
124
|
0x54b3: { name: 'aspectRatioType', value: DataType.uint },
|
|
114
|
-
0x2eb524: { name: 'colourSpace', value: DataType.
|
|
125
|
+
0x2eb524: { name: 'colourSpace', value: DataType.binary },
|
|
115
126
|
0x2fb523: { name: 'gammaValue', value: DataType.float }
|
|
116
127
|
}
|
|
117
128
|
},
|
|
@@ -122,7 +133,6 @@ export const matroskaDtd = {
|
|
|
122
133
|
0xb5: { name: 'samplingFrequency', value: DataType.float },
|
|
123
134
|
0x78b5: { name: 'outputSamplingFrequency', value: DataType.float },
|
|
124
135
|
0x9f: { name: 'channels', value: DataType.uint }, // https://www.matroska.org/technical/specs/index.html
|
|
125
|
-
0x94: { name: 'channels', value: DataType.uint },
|
|
126
136
|
0x7d7b: { name: 'channelPositions', value: DataType.binary },
|
|
127
137
|
0x6264: { name: 'bitDepth', value: DataType.uint }
|
|
128
138
|
}
|
|
@@ -133,29 +143,29 @@ export const matroskaDtd = {
|
|
|
133
143
|
container: {
|
|
134
144
|
0x6240: {
|
|
135
145
|
name: 'contentEncoding',
|
|
146
|
+
multiple: true,
|
|
136
147
|
container: {
|
|
137
148
|
0x5031: { name: 'order', value: DataType.uint },
|
|
138
|
-
0x5032: { name: 'scope', value: DataType.
|
|
149
|
+
0x5032: { name: 'scope', value: DataType.uint },
|
|
139
150
|
0x5033: { name: 'type', value: DataType.uint },
|
|
140
151
|
0x5034: {
|
|
141
|
-
name: '
|
|
152
|
+
name: 'contentCompression',
|
|
142
153
|
container: {
|
|
143
154
|
0x4254: { name: 'contentCompAlgo', value: DataType.uint },
|
|
144
155
|
0x4255: { name: 'contentCompSettings', value: DataType.binary }
|
|
145
156
|
}
|
|
146
157
|
},
|
|
147
158
|
0x5035: {
|
|
148
|
-
name: '
|
|
159
|
+
name: 'contentEncryption',
|
|
149
160
|
container: {
|
|
150
161
|
0x47e1: { name: 'contentEncAlgo', value: DataType.uint },
|
|
151
162
|
0x47e2: { name: 'contentEncKeyID', value: DataType.binary },
|
|
152
|
-
0x47e3: { name: 'contentSignature
|
|
153
|
-
0x47e4: { name: '
|
|
154
|
-
0x47e5: { name: 'contentSigAlgo
|
|
155
|
-
0x47e6: { name: 'contentSigHashAlgo
|
|
163
|
+
0x47e3: { name: 'contentSignature', value: DataType.binary },
|
|
164
|
+
0x47e4: { name: 'contentSigKeyID', value: DataType.binary },
|
|
165
|
+
0x47e5: { name: 'contentSigAlgo', value: DataType.uint },
|
|
166
|
+
0x47e6: { name: 'contentSigHashAlgo', value: DataType.uint }
|
|
156
167
|
}
|
|
157
|
-
}
|
|
158
|
-
0x6264: { name: 'bitDepth', value: DataType.uint }
|
|
168
|
+
}
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
171
|
}
|
|
@@ -170,10 +180,12 @@ export const matroskaDtd = {
|
|
|
170
180
|
container: {
|
|
171
181
|
0xbb: {
|
|
172
182
|
name: 'cuePoint',
|
|
183
|
+
multiple: true,
|
|
173
184
|
container: {
|
|
174
|
-
0xb3: { name: 'cueTime', value: DataType.
|
|
185
|
+
0xb3: { name: 'cueTime', value: DataType.uint },
|
|
175
186
|
0xb7: {
|
|
176
187
|
name: 'positions',
|
|
188
|
+
multiple: true,
|
|
177
189
|
container: {
|
|
178
190
|
0xf7: { name: 'track', value: DataType.uint },
|
|
179
191
|
0xf1: { name: 'clusterPosition', value: DataType.uint },
|
|
@@ -181,6 +193,7 @@ export const matroskaDtd = {
|
|
|
181
193
|
0xea: { name: 'codecState', value: DataType.uint },
|
|
182
194
|
0xdb: {
|
|
183
195
|
name: 'reference',
|
|
196
|
+
multiple: true,
|
|
184
197
|
container: {
|
|
185
198
|
0x96: { name: 'time', value: DataType.uint },
|
|
186
199
|
0x97: { name: 'cluster', value: DataType.uint },
|
|
@@ -218,27 +231,30 @@ export const matroskaDtd = {
|
|
|
218
231
|
container: {
|
|
219
232
|
0x45b9: {
|
|
220
233
|
name: 'editionEntry',
|
|
234
|
+
multiple: true,
|
|
221
235
|
container: {
|
|
222
236
|
0xb6: {
|
|
223
237
|
name: 'chapterAtom',
|
|
238
|
+
multiple: true,
|
|
224
239
|
container: {
|
|
225
240
|
0x73c4: { name: 'uid', value: DataType.uid },
|
|
226
241
|
0x91: { name: 'timeStart', value: DataType.uint },
|
|
227
|
-
0x92: { name: 'timeEnd', value: DataType.
|
|
242
|
+
0x92: { name: 'timeEnd', value: DataType.uint },
|
|
228
243
|
0x98: { name: 'hidden', value: DataType.bool },
|
|
229
|
-
0x4598: { name: 'enabled', value: DataType.
|
|
244
|
+
0x4598: { name: 'enabled', value: DataType.bool },
|
|
230
245
|
0x8f: {
|
|
231
246
|
name: 'track',
|
|
232
247
|
container: {
|
|
233
|
-
0x89: { name: '
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
248
|
+
0x89: { name: 'trackUID', value: DataType.uid, multiple: true }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
0x80: {
|
|
252
|
+
name: 'display',
|
|
253
|
+
multiple: true,
|
|
254
|
+
container: {
|
|
255
|
+
0x85: { name: 'string', value: DataType.string },
|
|
256
|
+
0x437c: { name: 'language', value: DataType.string, multiple: true },
|
|
257
|
+
0x437e: { name: 'country', value: DataType.string, multiple: true }
|
|
242
258
|
}
|
|
243
259
|
}
|
|
244
260
|
}
|
|
@@ -250,6 +266,7 @@ export const matroskaDtd = {
|
|
|
250
266
|
// Tagging
|
|
251
267
|
0x1254c367: {
|
|
252
268
|
name: 'tags',
|
|
269
|
+
multiple: true,
|
|
253
270
|
container: {
|
|
254
271
|
0x7373: {
|
|
255
272
|
name: 'tag',
|
|
@@ -258,12 +275,12 @@ export const matroskaDtd = {
|
|
|
258
275
|
0x63c0: {
|
|
259
276
|
name: 'target',
|
|
260
277
|
container: {
|
|
261
|
-
0x63c5: { name: 'tagTrackUID', value: DataType.uid },
|
|
262
|
-
0x63c4: { name: 'tagChapterUID', value: DataType.
|
|
263
|
-
0x63c6: { name: 'tagAttachmentUID', value: DataType.uid },
|
|
278
|
+
0x63c5: { name: 'tagTrackUID', value: DataType.uid, multiple: true },
|
|
279
|
+
0x63c4: { name: 'tagChapterUID', value: DataType.uid, multiple: true },
|
|
280
|
+
0x63c6: { name: 'tagAttachmentUID', value: DataType.uid, multiple: true },
|
|
264
281
|
0x63ca: { name: 'targetType', value: DataType.string }, // extended
|
|
265
282
|
0x68ca: { name: 'targetTypeValue', value: DataType.uint }, // extended
|
|
266
|
-
0x63c9: { name: 'tagEditionUID', value: DataType.uid } // extended
|
|
283
|
+
0x63c9: { name: 'tagEditionUID', value: DataType.uid, multiple: true } // extended
|
|
267
284
|
}
|
|
268
285
|
},
|
|
269
286
|
0x67c8: {
|
|
@@ -4,6 +4,30 @@ import { EbmlIterator, ParseAction } from '../ebml/EbmlIterator.js';
|
|
|
4
4
|
import { matroskaDtd } from './MatroskaDtd.js';
|
|
5
5
|
import { TargetType, TrackType } from './types.js';
|
|
6
6
|
const debug = initDebug('music-metadata:parser:matroska');
|
|
7
|
+
const targetTypes = TargetType;
|
|
8
|
+
function isCompleteTrack(entry) {
|
|
9
|
+
return (typeof entry.trackNumber === 'number' &&
|
|
10
|
+
Number.isSafeInteger(entry.trackNumber) &&
|
|
11
|
+
entry.trackNumber > 0 &&
|
|
12
|
+
typeof entry.codecID === 'string' &&
|
|
13
|
+
entry.codecID.length > 0 &&
|
|
14
|
+
Object.values(TrackType).some(type => type === entry.trackType));
|
|
15
|
+
}
|
|
16
|
+
function isCompleteFile(file) {
|
|
17
|
+
return (typeof file.name === 'string' &&
|
|
18
|
+
typeof file.mimeType === 'string' &&
|
|
19
|
+
file.data instanceof Uint8Array &&
|
|
20
|
+
file.uid instanceof Uint8Array);
|
|
21
|
+
}
|
|
22
|
+
function videoInfo(video) {
|
|
23
|
+
const { flagInterlaced, ...properties } = video;
|
|
24
|
+
const result = properties;
|
|
25
|
+
// Matroska uses 0=undetermined, 1=interlaced, 2=progressive.
|
|
26
|
+
if (flagInterlaced === 1 || flagInterlaced === 2) {
|
|
27
|
+
result.flagInterlaced = flagInterlaced === 1;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* Extensible Binary Meta Language (EBML) parser
|
|
9
33
|
* https://en.wikipedia.org/wiki/Extensible_Binary_Meta_Language
|
|
@@ -23,23 +47,44 @@ export class MatroskaParser extends BasicParser {
|
|
|
23
47
|
flagUseIndexToSkipClusters = this.options.mkvUseIndex ?? false;
|
|
24
48
|
async parse() {
|
|
25
49
|
const containerSize = this.tokenizer.fileInfo.size ?? Number.MAX_SAFE_INTEGER;
|
|
50
|
+
const attachmentMimeTypes = new WeakMap();
|
|
26
51
|
const matroskaIterator = new EbmlIterator(this.tokenizer, this.options);
|
|
27
52
|
debug('Initializing DTD end MatroskaIterator');
|
|
28
53
|
await matroskaIterator.iterate(matroskaDtd, containerSize, {
|
|
29
|
-
|
|
54
|
+
// Process tags and attachments one at a time. Never assemble a Segment/root tree.
|
|
55
|
+
collect: element => [0x114d9b74, 0x1549a966, 0x1654ae6b, 0x7373, 0x61a7].includes(element.id),
|
|
56
|
+
startNext: async (element) => {
|
|
30
57
|
switch (element.id) {
|
|
31
|
-
|
|
58
|
+
case 0x18538067: // SeekPosition is relative to the Segment payload, not the SeekHead.
|
|
59
|
+
this.seekHeadOffset = this.tokenizer.position;
|
|
60
|
+
return ParseAction.ReadNext;
|
|
61
|
+
case 0x1941a469: // Attachments are only used for cover pictures.
|
|
62
|
+
return this.options.skipCovers ? ParseAction.IgnoreElement : ParseAction.ReadNext;
|
|
63
|
+
case 0x465c: {
|
|
64
|
+
// FileData
|
|
65
|
+
const mimeType = element.parent && attachmentMimeTypes.get(element.parent);
|
|
66
|
+
// If MIME follows FileData, retain one payload under maxMemoryAllocation
|
|
67
|
+
// until AttachedFile ends, then classify it. This also works on streams.
|
|
68
|
+
return mimeType !== undefined && !mimeType.startsWith('image/')
|
|
69
|
+
? ParseAction.IgnoreElement
|
|
70
|
+
: ParseAction.ReadNext;
|
|
71
|
+
}
|
|
72
|
+
case 0x114d9b74:
|
|
73
|
+
return this.flagUseIndexToSkipClusters ? ParseAction.ReadNext : ParseAction.IgnoreElement;
|
|
74
|
+
case 0x63a2: // CodecPrivate is not exposed as metadata.
|
|
75
|
+
case 0x6d80: // ContentEncodings
|
|
76
|
+
case 0x1043a770: // Chapters are not currently exposed by this parser.
|
|
32
77
|
case 0x1c53bb6b: // Cueing Data
|
|
33
78
|
debug(`Skip element: name=${element.name}, id=0x${element.id.toString(16)}`);
|
|
34
79
|
return ParseAction.IgnoreElement;
|
|
35
80
|
case 0x1f43b675: // cluster
|
|
36
81
|
if (this.flagUseIndexToSkipClusters && this.seekHead) {
|
|
37
|
-
const index = this.seekHead.seek.find(index => index.position + this.seekHeadOffset > this.tokenizer.position);
|
|
38
|
-
if (index) {
|
|
82
|
+
const index = (this.seekHead.seek ?? []).find(index => index.position !== undefined && index.position + this.seekHeadOffset > this.tokenizer.position);
|
|
83
|
+
if (index?.position !== undefined) {
|
|
39
84
|
// Go to next index position
|
|
40
85
|
const ignoreSize = index.position + this.seekHeadOffset - this.tokenizer.position;
|
|
41
86
|
debug(`Use index to go to next position, ignoring ${ignoreSize} bytes`);
|
|
42
|
-
this.tokenizer.ignore(ignoreSize);
|
|
87
|
+
await this.tokenizer.ignore(ignoreSize);
|
|
43
88
|
return ParseAction.SkipElement;
|
|
44
89
|
}
|
|
45
90
|
}
|
|
@@ -48,24 +93,28 @@ export class MatroskaParser extends BasicParser {
|
|
|
48
93
|
return ParseAction.ReadNext;
|
|
49
94
|
}
|
|
50
95
|
},
|
|
51
|
-
|
|
52
|
-
debug
|
|
53
|
-
|
|
96
|
+
onElement: async (event) => {
|
|
97
|
+
if (debug.enabled) {
|
|
98
|
+
const value = event.value instanceof Uint8Array ? `${event.value.byteLength} bytes` : event.value;
|
|
99
|
+
debug(`Received: name=${event.element.name}, value=${value}`);
|
|
100
|
+
}
|
|
101
|
+
switch (event.id) {
|
|
54
102
|
case 0x4282: // docType
|
|
55
|
-
this.metadata.setFormat('container', `EBML/${value}`);
|
|
103
|
+
this.metadata.setFormat('container', `EBML/${event.value}`);
|
|
56
104
|
break;
|
|
57
105
|
case 0x114d9b74:
|
|
58
|
-
this.seekHead = value;
|
|
59
|
-
this.seekHeadOffset = offset;
|
|
106
|
+
this.seekHead = event.value;
|
|
60
107
|
break;
|
|
61
108
|
case 0x1549a966:
|
|
62
109
|
{
|
|
63
110
|
// Info (Segment Information)
|
|
64
|
-
const info = value;
|
|
111
|
+
const info = event.value;
|
|
112
|
+
if (info.title !== undefined) {
|
|
113
|
+
await this.addTag('segment:title', info.title);
|
|
114
|
+
}
|
|
65
115
|
const timecodeScale = info.timecodeScale ? info.timecodeScale : 1000000;
|
|
66
116
|
if (typeof info.duration === 'number') {
|
|
67
117
|
const duration = (info.duration * timecodeScale) / 1000000000;
|
|
68
|
-
await this.addTag('segment:title', info.title);
|
|
69
118
|
if (Number.isFinite(duration) && duration > 0) {
|
|
70
119
|
this.metadata.setFormat('duration', duration);
|
|
71
120
|
this.metadata.setFormat('containerDuration', duration);
|
|
@@ -76,9 +125,18 @@ export class MatroskaParser extends BasicParser {
|
|
|
76
125
|
case 0x1654ae6b:
|
|
77
126
|
{
|
|
78
127
|
// tracks
|
|
79
|
-
const audioTracks = value;
|
|
128
|
+
const audioTracks = event.value;
|
|
80
129
|
if (audioTracks?.entries) {
|
|
81
|
-
audioTracks.entries
|
|
130
|
+
const entries = audioTracks.entries
|
|
131
|
+
.filter(entry => {
|
|
132
|
+
if (isCompleteTrack(entry)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
this.metadata.addWarning('Ignoring Matroska track without valid TrackNumber, TrackType or CodecID');
|
|
136
|
+
return false;
|
|
137
|
+
})
|
|
138
|
+
.filter(isCompleteTrack);
|
|
139
|
+
entries.forEach(entry => {
|
|
82
140
|
const stream = {
|
|
83
141
|
id: entry.trackNumber,
|
|
84
142
|
codecId: entry.codecID,
|
|
@@ -98,7 +156,7 @@ export class MatroskaParser extends BasicParser {
|
|
|
98
156
|
channels: entry.audio?.channels ?? 1
|
|
99
157
|
}
|
|
100
158
|
: undefined,
|
|
101
|
-
video: entry.trackType === TrackType.video ? entry.video : undefined
|
|
159
|
+
video: entry.trackType === TrackType.video && entry.video ? videoInfo(entry.video) : undefined
|
|
102
160
|
};
|
|
103
161
|
if (stream.video && entry.defaultDuration && entry.defaultDuration > 0) {
|
|
104
162
|
stream.video = { ...stream.video, frameRate: 1e9 / entry.defaultDuration };
|
|
@@ -108,7 +166,7 @@ export class MatroskaParser extends BasicParser {
|
|
|
108
166
|
}
|
|
109
167
|
this.metadata.addStreamInfo(stream);
|
|
110
168
|
});
|
|
111
|
-
const audioTrack =
|
|
169
|
+
const audioTrack = entries
|
|
112
170
|
.filter(entry => entry.trackType === TrackType.audio && entry.flagEnabled !== false)
|
|
113
171
|
.reduce((acc, cur) => {
|
|
114
172
|
if (!acc) {
|
|
@@ -136,50 +194,56 @@ export class MatroskaParser extends BasicParser {
|
|
|
136
194
|
}
|
|
137
195
|
}
|
|
138
196
|
break;
|
|
139
|
-
case
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (simpleTag.name && typeof simpleTag.string === 'string') {
|
|
151
|
-
statistics.set(simpleTag.name.toUpperCase(), simpleTag.string);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
this.trackStatistics.set(key, statistics);
|
|
197
|
+
case 0x7373: {
|
|
198
|
+
// One Tag, released before the next sibling.
|
|
199
|
+
const tag = event.value;
|
|
200
|
+
const target = tag.target;
|
|
201
|
+
for (const uid of target?.tagTrackUID ?? []) {
|
|
202
|
+
const key = this.uidKey(uid);
|
|
203
|
+
if (key !== '0') {
|
|
204
|
+
const statistics = this.trackStatistics.get(key) ?? new Map();
|
|
205
|
+
for (const simpleTag of tag.simpleTags ?? []) {
|
|
206
|
+
if (simpleTag.name && typeof simpleTag.string === 'string') {
|
|
207
|
+
statistics.set(simpleTag.name.toUpperCase(), simpleTag.string);
|
|
155
208
|
}
|
|
156
209
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
210
|
+
this.trackStatistics.set(key, statistics);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const targetType = target?.targetTypeValue !== undefined
|
|
214
|
+
? (targetTypes[target.targetTypeValue] ?? 'track')
|
|
215
|
+
: (target?.targetType ?? 'track');
|
|
216
|
+
for (const simpleTag of tag.simpleTags ?? []) {
|
|
217
|
+
const value = simpleTag.string ?? simpleTag.binary;
|
|
218
|
+
if (simpleTag.name && value !== undefined) {
|
|
219
|
+
await this.addTag(`${targetType}:${simpleTag.name}`, value);
|
|
220
|
+
}
|
|
167
221
|
}
|
|
168
222
|
break;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
await Promise.all(attachments.attachedFiles
|
|
174
|
-
.filter(file => file.mimeType.startsWith('image/'))
|
|
175
|
-
.map(file => this.addTag('picture', {
|
|
176
|
-
data: file.data,
|
|
177
|
-
format: file.mimeType,
|
|
178
|
-
description: file.description,
|
|
179
|
-
name: file.name
|
|
180
|
-
})));
|
|
223
|
+
}
|
|
224
|
+
case 0x4660: // FileMediaType
|
|
225
|
+
if (event.element.parent) {
|
|
226
|
+
attachmentMimeTypes.set(event.element.parent, event.value);
|
|
181
227
|
}
|
|
182
228
|
break;
|
|
229
|
+
case 0x61a7: {
|
|
230
|
+
// One AttachedFile, released before the next sibling.
|
|
231
|
+
const file = event.value;
|
|
232
|
+
if (file.mimeType?.startsWith('image/')) {
|
|
233
|
+
if (isCompleteFile(file)) {
|
|
234
|
+
await this.addTag('picture', {
|
|
235
|
+
data: file.data,
|
|
236
|
+
format: file.mimeType,
|
|
237
|
+
description: file.description,
|
|
238
|
+
name: file.name
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
this.metadata.addWarning('Ignoring incomplete Matroska image attachment');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
183
247
|
}
|
|
184
248
|
}
|
|
185
249
|
});
|
package/lib/matroska/types.d.ts
CHANGED
|
@@ -1,73 +1,38 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { DecodedElement } from '../ebml/types.js';
|
|
2
|
+
import type { TrackType } from '../type.js';
|
|
3
|
+
import type { matroskaDtd } from './MatroskaDtd.js';
|
|
3
4
|
export type { TrackTypeKey } from '../type.js';
|
|
4
5
|
export { TrackType, TrackTypeValueToKeyMap } from '../type.js';
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
timecodeScale?: number;
|
|
15
|
-
duration?: number;
|
|
16
|
-
dateUTC?: number;
|
|
17
|
-
title?: string;
|
|
18
|
-
muxingApp?: string;
|
|
19
|
-
writingApp?: string;
|
|
20
|
-
}
|
|
21
|
-
export interface ITrackEntry {
|
|
22
|
-
uid?: Uint8Array;
|
|
6
|
+
export type IMatroskaDoc = DecodedElement<typeof matroskaDtd>;
|
|
7
|
+
export type IMatroskaSegment = NonNullable<IMatroskaDoc['segment']>;
|
|
8
|
+
export type ISeekHead = NonNullable<IMatroskaSegment['seekHead']>[number];
|
|
9
|
+
export type ISeek = NonNullable<ISeekHead['seek']>[number];
|
|
10
|
+
export type ISegmentInformation = NonNullable<IMatroskaSegment['info']>;
|
|
11
|
+
export type ITrackElement = NonNullable<IMatroskaSegment['tracks']>;
|
|
12
|
+
export type IDecodedTrackEntry = NonNullable<ITrackElement['entries']>[number];
|
|
13
|
+
/** A decoded entry is promoted only after validating these mandatory fields. */
|
|
14
|
+
export type ITrackEntry = IDecodedTrackEntry & {
|
|
23
15
|
trackNumber: number;
|
|
24
|
-
trackType?: TrackType;
|
|
25
|
-
audio?: ITrackAudio;
|
|
26
|
-
video?: ITrackVideo;
|
|
27
|
-
flagEnabled?: boolean;
|
|
28
|
-
flagDefault?: boolean;
|
|
29
|
-
flagForced?: boolean;
|
|
30
|
-
flagLacing?: boolean;
|
|
31
|
-
defaultDuration?: number;
|
|
32
|
-
trackTimecodeScale?: number;
|
|
33
|
-
name?: string;
|
|
34
|
-
language?: string;
|
|
35
|
-
languageIETF?: string;
|
|
36
16
|
codecID: string;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
export type
|
|
46
|
-
export type
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
cueReference?: ICueReference;
|
|
57
|
-
}
|
|
58
|
-
export interface ICueReference {
|
|
59
|
-
cueRefTime?: number;
|
|
60
|
-
cueRefCluster?: number;
|
|
61
|
-
cueRefNumber?: number;
|
|
62
|
-
cueRefCodecState?: number;
|
|
63
|
-
}
|
|
64
|
-
export interface ISimpleTag {
|
|
65
|
-
name?: string;
|
|
66
|
-
string?: string;
|
|
67
|
-
binary?: Uint8Array;
|
|
68
|
-
language?: string;
|
|
69
|
-
default?: boolean;
|
|
70
|
-
}
|
|
17
|
+
trackType: TrackType;
|
|
18
|
+
};
|
|
19
|
+
export type ITrackVideo = NonNullable<IDecodedTrackEntry['video']>;
|
|
20
|
+
export type ITrackAudio = NonNullable<IDecodedTrackEntry['audio']>;
|
|
21
|
+
export type ICuePoint = NonNullable<NonNullable<IMatroskaSegment['cues']>['cuePoint']>[number];
|
|
22
|
+
export type ICueTrackPosition = NonNullable<ICuePoint['positions']>[number];
|
|
23
|
+
export type ICueReference = NonNullable<ICueTrackPosition['reference']>[number];
|
|
24
|
+
export type ITags = NonNullable<IMatroskaSegment['tags']>[number];
|
|
25
|
+
export type ITag = NonNullable<ITags['tag']>[number];
|
|
26
|
+
export type ITarget = NonNullable<ITag['target']>;
|
|
27
|
+
export type ISimpleTag = NonNullable<ITag['simpleTags']>[number];
|
|
28
|
+
export type IAttachments = NonNullable<IMatroskaSegment['attachments']>;
|
|
29
|
+
export type IDecodedAttachedFile = NonNullable<IAttachments['attachedFiles']>[number];
|
|
30
|
+
export type IAttachedFile = IDecodedAttachedFile & {
|
|
31
|
+
name: string;
|
|
32
|
+
mimeType: string;
|
|
33
|
+
data: Uint8Array;
|
|
34
|
+
uid: Uint8Array;
|
|
35
|
+
};
|
|
71
36
|
export declare const TargetType: {
|
|
72
37
|
10: string;
|
|
73
38
|
20: string;
|
|
@@ -77,43 +42,3 @@ export declare const TargetType: {
|
|
|
77
42
|
60: string;
|
|
78
43
|
70: string;
|
|
79
44
|
};
|
|
80
|
-
export interface ITarget {
|
|
81
|
-
tagTrackUID?: Uint8Array;
|
|
82
|
-
trackUID?: Uint8Array;
|
|
83
|
-
chapterUID?: Uint8Array;
|
|
84
|
-
attachmentUID?: Uint8Array;
|
|
85
|
-
targetTypeValue?: keyof typeof TargetType;
|
|
86
|
-
targetType?: string;
|
|
87
|
-
}
|
|
88
|
-
export interface ITag {
|
|
89
|
-
target: ITarget;
|
|
90
|
-
simpleTags: ISimpleTag[];
|
|
91
|
-
}
|
|
92
|
-
export interface ITags {
|
|
93
|
-
tag: ITag[];
|
|
94
|
-
}
|
|
95
|
-
export interface ITrackElement {
|
|
96
|
-
entries?: ITrackEntry[];
|
|
97
|
-
}
|
|
98
|
-
export interface IAttachmedFile {
|
|
99
|
-
description?: string;
|
|
100
|
-
name: string;
|
|
101
|
-
mimeType: string;
|
|
102
|
-
data: Uint8Array;
|
|
103
|
-
uid: string;
|
|
104
|
-
}
|
|
105
|
-
export interface IAttachments {
|
|
106
|
-
attachedFiles: IAttachmedFile[];
|
|
107
|
-
}
|
|
108
|
-
export interface IMatroskaSegment {
|
|
109
|
-
metaSeekInfo?: ISeekHead;
|
|
110
|
-
seekHeads?: ISeek[];
|
|
111
|
-
info?: ISegmentInformation;
|
|
112
|
-
tracks?: ITrackElement;
|
|
113
|
-
tags?: ITags;
|
|
114
|
-
cues?: ICuePoint[];
|
|
115
|
-
attachments?: IAttachments;
|
|
116
|
-
}
|
|
117
|
-
export interface IMatroskaDoc extends IEbmlDoc {
|
|
118
|
-
segment: IMatroskaSegment;
|
|
119
|
-
}
|