music-metadata 12.1.0 → 12.2.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/README.md +44 -9
- package/lib/ParserFactory.js +8 -3
- package/lib/aiff/AiffParser.js +8 -7
- package/lib/apev2/APEv2Parser.js +4 -26
- package/lib/asf/AsfParser.d.ts +1 -0
- package/lib/asf/AsfParser.js +39 -19
- package/lib/caf/CafLoader.d.ts +2 -0
- package/lib/caf/CafLoader.js +8 -0
- package/lib/caf/CafParser.d.ts +37 -0
- package/lib/caf/CafParser.js +253 -0
- package/lib/caf/CafTagMap.d.ts +8 -0
- package/lib/caf/CafTagMap.js +38 -0
- package/lib/caf/CafToken.d.ts +75 -0
- package/lib/caf/CafToken.js +135 -0
- package/lib/common/Allocation.d.ts +14 -0
- package/lib/common/Allocation.js +70 -0
- package/lib/common/BasicParser.d.ts +2 -0
- package/lib/common/BasicParser.js +5 -0
- package/lib/common/CombinedTagMapper.js +3 -1
- package/lib/common/GenericTagTypes.d.ts +1 -1
- package/lib/common/MetadataCollector.js +9 -1
- package/lib/common/TrackInfo.d.ts +3 -0
- package/lib/common/TrackInfo.js +29 -0
- package/lib/core.js +4 -7
- package/lib/dsdiff/DsdiffParser.d.ts +3 -0
- package/lib/dsdiff/DsdiffParser.js +70 -30
- package/lib/ebml/EbmlIterator.d.ts +4 -1
- package/lib/ebml/EbmlIterator.js +19 -27
- package/lib/flac/FlacParser.js +10 -7
- package/lib/id3v2/ID3v2Parser.js +2 -1
- package/lib/index.js +3 -4
- package/lib/matroska/MatroskaParser.d.ts +4 -0
- package/lib/matroska/MatroskaParser.js +65 -2
- package/lib/matroska/types.d.ts +1 -0
- package/lib/mp4/Atom.js +1 -0
- package/lib/mp4/AtomToken.d.ts +2 -0
- package/lib/mp4/AtomToken.js +25 -8
- package/lib/mp4/MP4Parser.d.ts +6 -1
- package/lib/mp4/MP4Parser.js +192 -81
- package/lib/ogg/OggParser.js +5 -1
- package/lib/ogg/theora/Theora.d.ts +1 -0
- package/lib/ogg/theora/Theora.js +2 -1
- package/lib/ogg/theora/TheoraStream.d.ts +4 -1
- package/lib/ogg/theora/TheoraStream.js +30 -4
- package/lib/ogg/vorbis/VorbisDecoder.d.ts +1 -0
- package/lib/ogg/vorbis/VorbisDecoder.js +12 -0
- package/lib/ogg/vorbis/VorbisStream.d.ts +2 -1
- package/lib/ogg/vorbis/VorbisStream.js +14 -6
- package/lib/type.d.ts +12 -2
- package/lib/wav/WaveParser.js +13 -12
- package/lib/wavpack/WavPackParser.js +5 -2
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ If you find this project useful and would like to support its development, consi
|
|
|
51
51
|
| [APE](https://wikipedia.org/wiki/Monkey's_Audio) | Monkey's Audio | <img src="./image/logo-APE-AI.svg" width="40" alt="Monkey's Audio logo"> |
|
|
52
52
|
| [ASF](https://wikipedia.org/wiki/Advanced_Systems_Format) | Advanced Systems Format | |
|
|
53
53
|
| [BWF](https://en.wikipedia.org/wiki/Broadcast_Wave_Format) | Extended WAV format for broadcast and archiving | |
|
|
54
|
+
| [CAF](https://en.wikipedia.org/wiki/Core_Audio_File_Format) | Core Audio File Format | <img src="./image/logo-Apple_Computer_Logo_rainbow.svg" width="40" alt="Apple rainbow logo"> |
|
|
54
55
|
| [DSDIFF](https://wikipedia.org/wiki/Direct_Stream_Digital) | Philips DSDIFF | <img src="./image/logo-DSD.svg" width="80" alt="DSD logo"> |
|
|
55
56
|
| [DSF](https://wikipedia.org/wiki/Direct_Stream_Digital) | Sony's DSD Stream File | <img src="./image/logo-DSD.svg" width="80" alt="DSD logo"> |
|
|
56
57
|
| [FLAC](https://wikipedia.org/wiki/FLAC) | Free Lossless Audio Codec | <img src="./image/logo-FLAC.svg" width="80" alt="FLAC logo"> |
|
|
@@ -80,6 +81,7 @@ Following tag header formats are supported:
|
|
|
80
81
|
- [RIFF](https://wikipedia.org/wiki/Resource_Interchange_File_Format)/INFO
|
|
81
82
|
- [Vorbis comment](https://wikipedia.org/wiki/Vorbis_comment)
|
|
82
83
|
- [AIFF](https://wikipedia.org/wiki/Audio_Interchange_File_Format)
|
|
84
|
+
- [CAF](https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html)
|
|
83
85
|
|
|
84
86
|
Following lyric formats are supported:
|
|
85
87
|
- [LRC](https://en.wikipedia.org/wiki/LRC_(file_format))
|
|
@@ -611,6 +613,17 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
611
613
|
Files, buffers, and Blobs support chapters whether `moov` appears before or after `mdat`.
|
|
612
614
|
For forward-only streams, `moov` must appear before the chapter data.
|
|
613
615
|
|
|
616
|
+
- `maxMemoryAllocation`: `number` (default: `67108864`, 64 MiB)
|
|
617
|
+
|
|
618
|
+
Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
619
|
+
Must be a positive safe integer. Parsing rejects payloads above this limit before allocating them.
|
|
620
|
+
The parser configures this limit once on its `strtok3` tokenizers, including tokenizers for embedded tags.
|
|
621
|
+
Oversized tokenizer allocations or stream lookahead reject with `strtok3.BufferSizeError`.
|
|
622
|
+
When using `parseFromTokenizer()`, also configure `maxBufferSize` on the supplied tokenizer at creation.
|
|
623
|
+
Increase it for files with unusually large metadata or cover art, or lower it for constrained environments.
|
|
624
|
+
Audio/video data that is skipped does not count toward this limit. This does not cap total process memory:
|
|
625
|
+
decoded metadata, multiple retained tags, and temporary copies can require additional memory.
|
|
626
|
+
|
|
614
627
|
- `mkvUseIndex`: `boolean` (default: `false`)
|
|
615
628
|
|
|
616
629
|
When `true`, the parser uses the SeekHead index in Matroska (MKV) files to skip segment and cluster elements.
|
|
@@ -655,8 +668,10 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
|
|
|
655
668
|
- `format.codec?` Name of the codec (algorithm used for the audio compression)
|
|
656
669
|
- `format.codecProfile?: string` Codec profile / settings
|
|
657
670
|
- `format.tagTypes?: TagType[]` List of tagging formats found in parsed audio file
|
|
658
|
-
- `format.duration?: number` Duration in seconds
|
|
659
|
-
- `format.bitrate?: number`
|
|
671
|
+
- `format.duration?: number` Duration in seconds (existing audio/container summary; complete presentation duration for video-only files)
|
|
672
|
+
- `format.bitrate?: number` Existing audio/container summary bitrate in bits per second
|
|
673
|
+
- `format.containerDuration?: number` Complete media presentation duration in seconds, including video
|
|
674
|
+
- `format.overallBitrate?: number` Average bitrate of the entire file in bits per second, including container overhead; requires a known file size and presentation duration
|
|
660
675
|
- `format.sampleRate?: number` Sampling rate in Samples per second (S/s)
|
|
661
676
|
- `format.bitsPerSample?: number` Audio bit depth
|
|
662
677
|
- `format.lossless?: boolean` True if lossless, false for lossy encoding
|
|
@@ -675,9 +690,14 @@ metadata. Import `TrackType` from `music-metadata` to select tracks without know
|
|
|
675
690
|
```js
|
|
676
691
|
import { parseFile, TrackType } from 'music-metadata';
|
|
677
692
|
|
|
678
|
-
const { format } = await parseFile('movie.mp4');
|
|
693
|
+
const { format } = await parseFile('movie.mp4', { duration: true });
|
|
679
694
|
const audioTracks = format.trackInfo.filter(track => track.type === TrackType.audio);
|
|
680
695
|
const videoTracks = format.trackInfo.filter(track => track.type === TrackType.video);
|
|
696
|
+
for (const track of videoTracks) {
|
|
697
|
+
console.log(track.duration, track.bitrate, track.video?.pixelWidth, track.video?.pixelHeight);
|
|
698
|
+
console.log(track.video?.frameRate);
|
|
699
|
+
}
|
|
700
|
+
console.log(format.containerDuration, format.overallBitrate);
|
|
681
701
|
```
|
|
682
702
|
|
|
683
703
|
Each entry describes one discovered track. The order follows container discovery and does not identify
|
|
@@ -687,12 +707,27 @@ logical media streams; Skeleton headers do not create media tracks. Single-strea
|
|
|
687
707
|
their format properties to one audio track after parsing.
|
|
688
708
|
|
|
689
709
|
Properties are optional when unavailable and are not copied between tracks. In particular, a container
|
|
690
|
-
or summary bitrate is not a per-track bitrate.
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
710
|
+
or summary bitrate is not a per-track bitrate. The existing `format.duration` and `format.bitrate`
|
|
711
|
+
fields retain their summary semantics. Use `containerDuration` for the complete presentation and
|
|
712
|
+
`overallBitrate` for the actual file size multiplied by eight and divided by that duration. These
|
|
713
|
+
statistics remain unset when timing is unavailable; `overallBitrate` also requires a known input size.
|
|
714
|
+
|
|
715
|
+
MPEG-4 derives audio and video timing from media headers, sample tables, and fragments, including
|
|
716
|
+
fragment defaults and decode timestamps. Track bitrates use sample sizes and duration; video properties
|
|
717
|
+
include encoded dimensions, display dimensions, and average frame rate. ASF supplies stream dimensions,
|
|
718
|
+
stream bitrates, and extended stream timing and frame rate when present. Standalone and nested Stream
|
|
719
|
+
Properties Objects describe actual streams; Codec List entries supply names without creating unused tracks.
|
|
720
|
+
|
|
721
|
+
Matroska applies specified defaults for flags, language, channels, and sample rate, and prefers IETF
|
|
722
|
+
language tags when present. Track statistics tags provide duration, bitrate, and frame count; without
|
|
723
|
+
these tags, per-track timing may be unavailable even when segment duration is known. Media clusters
|
|
724
|
+
are not scanned to calculate missing track statistics.
|
|
725
|
+
|
|
726
|
+
Ogg/Theora duration comes from granule positions. Set `duration: true` to allow scanning beyond the
|
|
727
|
+
initial metadata pages. Once a video stream has been fully read, its average bitrate uses that logical
|
|
728
|
+
stream's payload bytes, including codec headers and excluding Ogg framing and other streams. Resolution
|
|
729
|
+
and frame rate are available from the identification header. Audio streams retain their own codec
|
|
730
|
+
bitrate semantics.
|
|
696
731
|
|
|
697
732
|
For Ogg/Opus, the per-track average bitrate uses the logical stream's payload bytes, including codec
|
|
698
733
|
headers and excluding Ogg framing and other streams. It requires the logical stream's end-of-stream
|
package/lib/ParserFactory.js
CHANGED
|
@@ -5,8 +5,10 @@ import { parse as mimeTypeParse } from 'media-typer';
|
|
|
5
5
|
import { aiffParserLoader } from './aiff/AiffLoader.js';
|
|
6
6
|
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
7
7
|
import { asfParserLoader } from './asf/AsfLoader.js';
|
|
8
|
+
import { cafParserLoader } from './caf/CafLoader.js';
|
|
9
|
+
import { getMaxMemoryAllocation } from './common/Allocation.js';
|
|
8
10
|
import { MetadataCollector } from './common/MetadataCollector.js';
|
|
9
|
-
import { createAudioTrackInfo } from './common/TrackInfo.js';
|
|
11
|
+
import { createAudioTrackInfo, finalizeContainerInfo } from './common/TrackInfo.js';
|
|
10
12
|
import { scanAppendingHeaders } from './core.js';
|
|
11
13
|
import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
|
|
12
14
|
import { dsfParserLoader } from './dsf/DsfLoader.js';
|
|
@@ -47,7 +49,8 @@ export class ParserFactory {
|
|
|
47
49
|
wavpackParserLoader,
|
|
48
50
|
musepackParserLoader,
|
|
49
51
|
dsfParserLoader,
|
|
50
|
-
dsdiffParserLoader
|
|
52
|
+
dsdiffParserLoader,
|
|
53
|
+
cafParserLoader
|
|
51
54
|
].forEach(parser => {
|
|
52
55
|
this.registerParser(parser);
|
|
53
56
|
});
|
|
@@ -56,6 +59,7 @@ export class ParserFactory {
|
|
|
56
59
|
this.parsers.push(parser);
|
|
57
60
|
}
|
|
58
61
|
async parse(tokenizer, parserLoader, opts = {}) {
|
|
62
|
+
getMaxMemoryAllocation(opts);
|
|
59
63
|
if (tokenizer.supportsRandomAccess()) {
|
|
60
64
|
debug('tokenizer supports random-access, scanning for appending headers');
|
|
61
65
|
await scanAppendingHeaders(tokenizer, opts);
|
|
@@ -90,7 +94,7 @@ export class ParserFactory {
|
|
|
90
94
|
debug(`Loading ${parserLoader.parserType} parser...`);
|
|
91
95
|
const metadata = new MetadataCollector(opts);
|
|
92
96
|
const ParserImpl = await parserLoader.load();
|
|
93
|
-
const parser = new ParserImpl(metadata, tokenizer, opts
|
|
97
|
+
const parser = new ParserImpl(metadata, tokenizer, opts);
|
|
94
98
|
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
95
99
|
await parser.parse();
|
|
96
100
|
// Audio-only parsers describe their single stream through the format fields.
|
|
@@ -106,6 +110,7 @@ export class ParserFactory {
|
|
|
106
110
|
metadata.setFormat('hasVideo', !!metadata.format.trackInfo.find(track => track.type === TrackType.video));
|
|
107
111
|
}
|
|
108
112
|
}
|
|
113
|
+
finalizeContainerInfo(metadata, tokenizer.fileInfo.size);
|
|
109
114
|
return metadata.toCommonMetadata();
|
|
110
115
|
}
|
|
111
116
|
/**
|
package/lib/aiff/AiffParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import * as strtok3 from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { getTokenizerOptions } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { FourCcToken } from '../common/FourCC.js';
|
|
6
7
|
import { ID3v2Parser } from '../id3v2/ID3v2Parser.js';
|
|
@@ -18,11 +19,11 @@ const debug = initDebug('music-metadata:parser:aiff');
|
|
|
18
19
|
export class AIFFParser extends BasicParser {
|
|
19
20
|
isCompressed = null;
|
|
20
21
|
async parse() {
|
|
21
|
-
const header = await this.
|
|
22
|
+
const header = await this.readToken(iff.Header);
|
|
22
23
|
if (header.chunkID !== 'FORM') {
|
|
23
24
|
throw new AiffContentError("Invalid Chunk-ID, expected 'FORM'"); // Not AIFF format
|
|
24
25
|
}
|
|
25
|
-
const type = await this.
|
|
26
|
+
const type = await this.readToken(FourCcToken);
|
|
26
27
|
switch (type) {
|
|
27
28
|
case 'AIFF':
|
|
28
29
|
this.metadata.setFormat('container', type);
|
|
@@ -41,7 +42,7 @@ export class AIFFParser extends BasicParser {
|
|
|
41
42
|
while (!this.tokenizer.fileInfo.size ||
|
|
42
43
|
this.tokenizer.fileInfo.size - this.tokenizer.position >= iff.Header.len) {
|
|
43
44
|
debug(`Reading AIFF chunk at offset=${this.tokenizer.position}`);
|
|
44
|
-
const chunkHeader = await this.
|
|
45
|
+
const chunkHeader = await this.readToken(iff.Header);
|
|
45
46
|
const nextChunk = 2 * Math.round(chunkHeader.chunkSize / 2);
|
|
46
47
|
const bytesRead = await this.readData(chunkHeader);
|
|
47
48
|
await this.tokenizer.ignore(nextChunk - bytesRead);
|
|
@@ -63,7 +64,7 @@ export class AIFFParser extends BasicParser {
|
|
|
63
64
|
if (this.isCompressed === null) {
|
|
64
65
|
throw new AiffContentError('Failed to parse AIFF.COMM chunk when compression type is unknown');
|
|
65
66
|
}
|
|
66
|
-
const common = await this.
|
|
67
|
+
const common = await this.readToken(new AiffToken.Common(header, this.isCompressed));
|
|
67
68
|
this.metadata.setFormat('bitsPerSample', common.sampleSize);
|
|
68
69
|
this.metadata.setFormat('sampleRate', common.sampleRate);
|
|
69
70
|
this.metadata.setFormat('numberOfChannels', common.numChannels);
|
|
@@ -76,8 +77,8 @@ export class AIFFParser extends BasicParser {
|
|
|
76
77
|
}
|
|
77
78
|
case 'ID3 ': {
|
|
78
79
|
// ID3-meta-data
|
|
79
|
-
const id3_data = await this.
|
|
80
|
-
const rst = strtok3.fromBuffer(id3_data);
|
|
80
|
+
const id3_data = await this.readToken(new Token.Uint8ArrayType(header.chunkSize));
|
|
81
|
+
const rst = strtok3.fromBuffer(id3_data, getTokenizerOptions(this.options));
|
|
81
82
|
await new ID3v2Parser().parse(this.metadata, rst, this.options);
|
|
82
83
|
return header.chunkSize;
|
|
83
84
|
}
|
|
@@ -97,7 +98,7 @@ export class AIFFParser extends BasicParser {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
async readTextChunk(header) {
|
|
100
|
-
const value = await this.
|
|
101
|
+
const value = await this.readToken(new Token.StringType(header.chunkSize, 'ascii'));
|
|
101
102
|
const values = value
|
|
102
103
|
.split('\0')
|
|
103
104
|
.map(v => v.trim())
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { textDecode } from '@borewit/text-codec';
|
|
|
2
2
|
import initDebug from 'debug';
|
|
3
3
|
import * as strtok3 from 'strtok3';
|
|
4
4
|
import { StringType } from 'token-types';
|
|
5
|
+
import { getTokenizerOptions, readBuffer } from '../common/Allocation.js';
|
|
5
6
|
import { BasicParser } from '../common/BasicParser.js';
|
|
6
7
|
import * as util from '../common/Util.js';
|
|
7
8
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
@@ -60,8 +61,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
60
61
|
if (footer.ID !== preamble) {
|
|
61
62
|
throw new ApeContentError('Unexpected APEv2 Footer ID preamble value');
|
|
62
63
|
}
|
|
63
|
-
strtok3.fromBuffer(buffer);
|
|
64
|
-
const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer), options);
|
|
64
|
+
const apeParser = new APEv2Parser(metadata, strtok3.fromBuffer(buffer, getTokenizerOptions(options)), options);
|
|
65
65
|
return apeParser.parseTags(footer);
|
|
66
66
|
}
|
|
67
67
|
ape = {};
|
|
@@ -82,8 +82,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
82
82
|
if (this.tokenizer.fileInfo.size) {
|
|
83
83
|
// Try to read the APEv2 header using just the footer-header
|
|
84
84
|
const remaining = this.tokenizer.fileInfo.size - this.tokenizer.position; // ToDo: take ID3v1 into account
|
|
85
|
-
const buffer =
|
|
86
|
-
await this.tokenizer.readBuffer(buffer);
|
|
85
|
+
const buffer = await this.readTagValue(remaining);
|
|
87
86
|
return APEv2Parser.parseTagFooter(this.metadata, buffer, this.options);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
@@ -167,28 +166,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
168
|
async readTagValue(size) {
|
|
170
|
-
|
|
171
|
-
if ((!this.tokenizer.supportsRandomAccess() || this.tokenizer.fileInfo.size === undefined) && size > chunkSize) {
|
|
172
|
-
// Stream sizes may be unknown or untrustworthy. Only allocate the full
|
|
173
|
-
// value after all of its bytes have actually been read.
|
|
174
|
-
const chunks = [];
|
|
175
|
-
for (let remaining = size; remaining > 0;) {
|
|
176
|
-
const chunk = new Uint8Array(Math.min(remaining, chunkSize));
|
|
177
|
-
await this.tokenizer.readBuffer(chunk);
|
|
178
|
-
chunks.push(chunk);
|
|
179
|
-
remaining -= chunk.length;
|
|
180
|
-
}
|
|
181
|
-
const value = new Uint8Array(size);
|
|
182
|
-
let offset = 0;
|
|
183
|
-
for (const chunk of chunks) {
|
|
184
|
-
value.set(chunk, offset);
|
|
185
|
-
offset += chunk.length;
|
|
186
|
-
}
|
|
187
|
-
return value;
|
|
188
|
-
}
|
|
189
|
-
const value = new Uint8Array(size);
|
|
190
|
-
await this.tokenizer.readBuffer(value);
|
|
191
|
-
return value;
|
|
169
|
+
return readBuffer(this.tokenizer, size, this.options, undefined, message => new ApeContentError(message));
|
|
192
170
|
}
|
|
193
171
|
async parseDescriptorExpansion(lenExp) {
|
|
194
172
|
await this.tokenizer.ignore(lenExp);
|
package/lib/asf/AsfParser.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { BasicParser } from '../common/BasicParser.js';
|
|
|
12
12
|
export declare class AsfParser extends BasicParser {
|
|
13
13
|
private streams;
|
|
14
14
|
private codecs;
|
|
15
|
+
private extendedStreams;
|
|
15
16
|
private streamBitrates;
|
|
16
17
|
parse(): Promise<void>;
|
|
17
18
|
private parseObjectHeaders;
|
package/lib/asf/AsfParser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
3
|
import * as Token from 'token-types';
|
|
4
|
+
import { validateAllocation } from '../common/Allocation.js';
|
|
4
5
|
import { BasicParser } from '../common/BasicParser.js';
|
|
5
6
|
import { TrackType } from '../type.js';
|
|
6
7
|
import AsfGuid from './AsfGuid.js';
|
|
@@ -8,7 +9,6 @@ import * as AsfObject from './AsfObject.js';
|
|
|
8
9
|
import { AsfContentParseError } from './AsfObject.js';
|
|
9
10
|
const debug = initDebug('music-metadata:parser:ASF');
|
|
10
11
|
const headerType = 'asf';
|
|
11
|
-
const maxAsfMetadataObjectSize = 16 * 1024 * 1024;
|
|
12
12
|
/**
|
|
13
13
|
* Windows Media Metadata Usage Guidelines
|
|
14
14
|
* - Ref: https://msdn.microsoft.com/en-us/library/ms867702.aspx
|
|
@@ -22,9 +22,10 @@ const maxAsfMetadataObjectSize = 16 * 1024 * 1024;
|
|
|
22
22
|
export class AsfParser extends BasicParser {
|
|
23
23
|
streams = new Map();
|
|
24
24
|
codecs = [];
|
|
25
|
+
extendedStreams = new Map();
|
|
25
26
|
streamBitrates = new Map();
|
|
26
27
|
async parse() {
|
|
27
|
-
const header = await this.
|
|
28
|
+
const header = await this.readToken(AsfObject.TopLevelHeaderObjectToken);
|
|
28
29
|
if (!header.objectId.equals(AsfGuid.HeaderObject)) {
|
|
29
30
|
throw new AsfContentParseError(`expected asf header; but was not found; got: ${header.objectId.str}`);
|
|
30
31
|
}
|
|
@@ -52,6 +53,7 @@ export class AsfParser extends BasicParser {
|
|
|
52
53
|
stream.codecId === new Token.StringType(4, 'ascii').get(codec.information, 0));
|
|
53
54
|
});
|
|
54
55
|
const track = { id: stream.streamNumber, type };
|
|
56
|
+
const extended = this.extendedStreams.get(stream.streamNumber);
|
|
55
57
|
if (stream.codecId !== undefined) {
|
|
56
58
|
track.codecId = stream.codecId;
|
|
57
59
|
}
|
|
@@ -64,7 +66,20 @@ export class AsfParser extends BasicParser {
|
|
|
64
66
|
if (stream.video) {
|
|
65
67
|
track.video = stream.video;
|
|
66
68
|
}
|
|
67
|
-
|
|
69
|
+
if (extended) {
|
|
70
|
+
if (extended.endTime > extended.startTime) {
|
|
71
|
+
track.duration = Number(extended.endTime - extended.startTime) / 1000;
|
|
72
|
+
}
|
|
73
|
+
const name = extended.streamNames[0]?.streamName;
|
|
74
|
+
if (name) {
|
|
75
|
+
track.name = name;
|
|
76
|
+
}
|
|
77
|
+
if (track.video && extended.averageTimePerFrame > 0) {
|
|
78
|
+
track.video.frameRate = 1e7 / extended.averageTimePerFrame;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const bitrate = this.streamBitrates.get(stream.streamNumber) ??
|
|
82
|
+
(extended && extended.dataBitrate > 0 ? extended.dataBitrate : stream.bitrate);
|
|
68
83
|
if (bitrate !== undefined) {
|
|
69
84
|
track.bitrate = bitrate;
|
|
70
85
|
}
|
|
@@ -89,15 +104,21 @@ export class AsfParser extends BasicParser {
|
|
|
89
104
|
case AsfObject.FilePropertiesObject.guid.str: {
|
|
90
105
|
// 3.2
|
|
91
106
|
this.validateAllocationSize(payloadSize, 'File Properties Object');
|
|
92
|
-
const fpo = await this.
|
|
93
|
-
|
|
107
|
+
const fpo = await this.readToken(new AsfObject.FilePropertiesObject(header));
|
|
108
|
+
if (!fpo.flags.broadcast) {
|
|
109
|
+
const duration = Number(fpo.playDuration) / 1e7 - Number(fpo.preroll) / 1000;
|
|
110
|
+
if (Number.isFinite(duration) && duration > 0) {
|
|
111
|
+
this.metadata.setFormat('duration', duration);
|
|
112
|
+
this.metadata.setFormat('containerDuration', duration);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
94
115
|
this.metadata.setFormat('bitrate', fpo.maximumBitrate);
|
|
95
116
|
break;
|
|
96
117
|
}
|
|
97
118
|
case AsfObject.StreamPropertiesObject.guid.str: {
|
|
98
119
|
// 3.3
|
|
99
120
|
this.validateAllocationSize(payloadSize, 'Stream Properties Object');
|
|
100
|
-
const spo = await this.
|
|
121
|
+
const spo = await this.readToken(new AsfObject.StreamPropertiesObject(header));
|
|
101
122
|
this.metadata.setFormat('container', `ASF/${spo.streamType}`);
|
|
102
123
|
if (spo.streamType === 'audio') {
|
|
103
124
|
this.metadata.setFormat('hasAudio', true);
|
|
@@ -114,7 +135,7 @@ export class AsfParser extends BasicParser {
|
|
|
114
135
|
if (payloadSize < extensionHeaderToken.len) {
|
|
115
136
|
throw new AsfContentParseError(`ASF Header Extension Object payload is too small: ${payloadSize} bytes`);
|
|
116
137
|
}
|
|
117
|
-
const extHeader = await this.
|
|
138
|
+
const extHeader = await this.readToken(extensionHeaderToken);
|
|
118
139
|
const expectedExtensionSize = payloadSize - extensionHeaderToken.len;
|
|
119
140
|
if (extHeader.extensionDataSize !== expectedExtensionSize) {
|
|
120
141
|
throw new AsfContentParseError(`ASF extension data size ${extHeader.extensionDataSize} does not match enclosing payload size ${expectedExtensionSize}`);
|
|
@@ -124,12 +145,12 @@ export class AsfParser extends BasicParser {
|
|
|
124
145
|
}
|
|
125
146
|
case AsfObject.ContentDescriptionObjectState.guid.str: // 3.10
|
|
126
147
|
this.validateAllocationSize(payloadSize, 'Content Description Object');
|
|
127
|
-
tags = await this.
|
|
148
|
+
tags = await this.readToken(new AsfObject.ContentDescriptionObjectState(header));
|
|
128
149
|
await this.addTags(tags);
|
|
129
150
|
break;
|
|
130
151
|
case AsfObject.ExtendedContentDescriptionObjectState.guid.str: // 3.11
|
|
131
152
|
this.validateAllocationSize(payloadSize, 'Extended Content Description Object');
|
|
132
|
-
tags = await this.
|
|
153
|
+
tags = await this.readToken(new AsfObject.ExtendedContentDescriptionObjectState(header));
|
|
133
154
|
await this.addTags(tags);
|
|
134
155
|
break;
|
|
135
156
|
case AsfGuid.CodecListObject.str: {
|
|
@@ -154,13 +175,13 @@ export class AsfParser extends BasicParser {
|
|
|
154
175
|
if (payloadSize < 2) {
|
|
155
176
|
throw new AsfContentParseError('Truncated Stream Bitrate Properties Object');
|
|
156
177
|
}
|
|
157
|
-
const count = await this.
|
|
178
|
+
const count = await this.readToken(Token.UINT16_LE);
|
|
158
179
|
if (count * 6 > payloadSize - 2) {
|
|
159
180
|
throw new AsfContentParseError('ASF stream bitrate records exceed object payload');
|
|
160
181
|
}
|
|
161
182
|
for (let index = 0; index < count; ++index) {
|
|
162
|
-
const streamNumber = (await this.
|
|
163
|
-
this.streamBitrates.set(streamNumber, await this.
|
|
183
|
+
const streamNumber = (await this.readToken(Token.UINT16_LE)) & 0x7f;
|
|
184
|
+
this.streamBitrates.set(streamNumber, await this.readToken(Token.UINT32_LE));
|
|
164
185
|
}
|
|
165
186
|
await this.ignorePayload(payloadSize - 2 - count * 6, 'Stream Bitrate Properties Object');
|
|
166
187
|
break;
|
|
@@ -188,7 +209,7 @@ export class AsfParser extends BasicParser {
|
|
|
188
209
|
if (remainingSize < AsfObject.HeaderObjectToken.len) {
|
|
189
210
|
throw new AsfContentParseError(`Insufficient ASF ${containerType} data for an object header: ${remainingSize} bytes`);
|
|
190
211
|
}
|
|
191
|
-
const header = await this.
|
|
212
|
+
const header = await this.readToken(AsfObject.HeaderObjectToken);
|
|
192
213
|
if (header.objectSize > remainingSize) {
|
|
193
214
|
throw new AsfContentParseError(`ASF ${containerType} object size ${header.objectSize} exceeds remaining ${containerType} payload size ${remainingSize}`);
|
|
194
215
|
}
|
|
@@ -209,9 +230,7 @@ export class AsfParser extends BasicParser {
|
|
|
209
230
|
return payloadSize;
|
|
210
231
|
}
|
|
211
232
|
validateAllocationSize(payloadSize, objectType) {
|
|
212
|
-
|
|
213
|
-
throw new AsfContentParseError(`ASF ${objectType} payload size ${payloadSize} exceeds allocation limit ${maxAsfMetadataObjectSize}`);
|
|
214
|
-
}
|
|
233
|
+
validateAllocation(payloadSize, this.options, message => new AsfContentParseError(`ASF ${objectType}: ${message}`));
|
|
215
234
|
}
|
|
216
235
|
async ignorePayload(payloadSize, objectType) {
|
|
217
236
|
let ignored;
|
|
@@ -236,7 +255,8 @@ export class AsfParser extends BasicParser {
|
|
|
236
255
|
case AsfObject.ExtendedStreamPropertiesObjectState.guid.str: {
|
|
237
256
|
// 4.1
|
|
238
257
|
this.validateAllocationSize(payloadSize, 'Extended Stream Properties Object');
|
|
239
|
-
const extended = await this.
|
|
258
|
+
const extended = await this.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
|
|
259
|
+
this.extendedStreams.set(extended.streamNumber, extended);
|
|
240
260
|
if (extended.streamPropertiesObject) {
|
|
241
261
|
const stream = extended.streamPropertiesObject;
|
|
242
262
|
this.streams.set(stream.streamNumber, stream);
|
|
@@ -247,14 +267,14 @@ export class AsfParser extends BasicParser {
|
|
|
247
267
|
case AsfObject.MetadataObjectState.guid.str: {
|
|
248
268
|
// 4.7
|
|
249
269
|
this.validateAllocationSize(payloadSize, 'Metadata Object');
|
|
250
|
-
const moTags = await this.
|
|
270
|
+
const moTags = await this.readToken(new AsfObject.MetadataObjectState(header));
|
|
251
271
|
await this.addTags(moTags);
|
|
252
272
|
break;
|
|
253
273
|
}
|
|
254
274
|
case AsfObject.MetadataLibraryObjectState.guid.str: {
|
|
255
275
|
// 4.8
|
|
256
276
|
this.validateAllocationSize(payloadSize, 'Metadata Library Object');
|
|
257
|
-
const mlTags = await this.
|
|
277
|
+
const mlTags = await this.readToken(new AsfObject.MetadataLibraryObjectState(header));
|
|
258
278
|
await this.addTags(mlTags);
|
|
259
279
|
break;
|
|
260
280
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BasicParser } from '../common/BasicParser.js';
|
|
2
|
+
/**
|
|
3
|
+
* CAF - Core Audio File Format
|
|
4
|
+
*
|
|
5
|
+
* Ref: https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html
|
|
6
|
+
*/
|
|
7
|
+
export declare class CafParser extends BasicParser {
|
|
8
|
+
private desc;
|
|
9
|
+
private packetTable;
|
|
10
|
+
private dataBytes;
|
|
11
|
+
private expectedFirstChunk;
|
|
12
|
+
private reachedEndOfFile;
|
|
13
|
+
parse(): Promise<void>;
|
|
14
|
+
private endOfFileReached;
|
|
15
|
+
private readChunk;
|
|
16
|
+
private readAudioDescription;
|
|
17
|
+
private applyAudioDescription;
|
|
18
|
+
private readPacketTable;
|
|
19
|
+
private readAudioData;
|
|
20
|
+
private readInfo;
|
|
21
|
+
private readChannelLayout;
|
|
22
|
+
private addChannelLayout;
|
|
23
|
+
/**
|
|
24
|
+
* Rejects a whole chunk whose declared size exceeds the remaining input.
|
|
25
|
+
* Only applicable when the total input size is known.
|
|
26
|
+
*/
|
|
27
|
+
private validateChunkAvailability;
|
|
28
|
+
/**
|
|
29
|
+
* Reads a chunk body without trusting its declared size.
|
|
30
|
+
* @param chunkSize Declared body size
|
|
31
|
+
* @param chunkType Chunk name for error messages
|
|
32
|
+
*/
|
|
33
|
+
private readChunkBody;
|
|
34
|
+
private skipUnknownChunk;
|
|
35
|
+
private applyDerivedFormat;
|
|
36
|
+
private deriveFrameCount;
|
|
37
|
+
}
|