music-metadata 12.0.0 → 12.1.1
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 +83 -30
- package/lib/ParserFactory.js +9 -1
- package/lib/aiff/AiffParser.js +8 -7
- package/lib/apev2/APEv2Parser.js +5 -26
- package/lib/asf/AsfObject.d.ts +7 -2
- package/lib/asf/AsfObject.js +103 -26
- package/lib/asf/AsfParser.d.ts +3 -0
- package/lib/asf/AsfParser.js +92 -25
- 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/MetadataCollector.js +4 -0
- package/lib/common/TrackInfo.d.ts +5 -0
- package/lib/common/TrackInfo.js +39 -0
- package/lib/common/Util.d.ts +1 -1
- package/lib/common/Util.js +6 -12
- package/lib/core.d.ts +1 -1
- package/lib/core.js +5 -8
- 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/MatroskaDtd.js +1 -0
- package/lib/matroska/MatroskaParser.js +32 -14
- package/lib/matroska/types.d.ts +8 -32
- package/lib/matroska/types.js +1 -18
- package/lib/mp4/AtomToken.d.ts +11 -0
- package/lib/mp4/AtomToken.js +85 -12
- package/lib/mp4/MP4Parser.d.ts +8 -1
- package/lib/mp4/MP4Parser.js +283 -79
- package/lib/mpeg/AdtsFrameHeader.d.ts +17 -0
- package/lib/mpeg/AdtsFrameHeader.js +44 -0
- package/lib/mpeg/MpegFrameHeader.d.ts +41 -0
- package/lib/mpeg/MpegFrameHeader.js +84 -0
- package/lib/mpeg/MpegParser.d.ts +11 -25
- package/lib/mpeg/MpegParser.js +60 -333
- package/lib/ogg/OggLoader.js +1 -1
- package/lib/ogg/OggParser.js +44 -3
- package/lib/ogg/OggStreamMetadata.d.ts +14 -0
- package/lib/ogg/OggStreamMetadata.js +27 -0
- package/lib/ogg/OggToken.d.ts +1 -0
- package/lib/ogg/OggToken.js +2 -1
- package/lib/ogg/flac/FlacStream.d.ts +3 -1
- package/lib/ogg/flac/FlacStream.js +17 -3
- package/lib/ogg/opus/OpusStream.js +1 -0
- package/lib/ogg/speex/SpeexStream.js +1 -0
- package/lib/ogg/theora/Theora.d.ts +4 -0
- package/lib/ogg/theora/Theora.js +5 -1
- package/lib/ogg/theora/TheoraStream.d.ts +1 -1
- package/lib/ogg/theora/TheoraStream.js +13 -0
- package/lib/ogg/vorbis/Vorbis.js +3 -3
- 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 +15 -6
- package/lib/type.d.ts +40 -3
- package/lib/type.js +22 -1
- package/lib/wav/WaveParser.js +13 -12
- package/lib/wavpack/WavPackParser.js +5 -2
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -606,7 +606,21 @@ Returns a list of supported MIME-types. This may include some MIME-types which a
|
|
|
606
606
|
only that the parser will attempt to calculate it when possible, even if it requires reading the full file.
|
|
607
607
|
|
|
608
608
|
- `includeChapters`: `boolean` (default: `false`)
|
|
609
|
-
When `true`, the MP4 parser
|
|
609
|
+
When `true`, the MP4 parser reads QuickTime chapter tracks and Nero `chpl` chapter lists.
|
|
610
|
+
A usable QuickTime chapter track takes precedence over a Nero list.
|
|
611
|
+
Files, buffers, and Blobs support chapters whether `moov` appears before or after `mdat`.
|
|
612
|
+
For forward-only streams, `moov` must appear before the chapter data.
|
|
613
|
+
|
|
614
|
+
- `maxMemoryAllocation`: `number` (default: `67108864`, 64 MiB)
|
|
615
|
+
|
|
616
|
+
Maximum bytes buffered for a single metadata payload, accumulated Ogg packet, or tokenizer stream lookahead.
|
|
617
|
+
Must be a positive safe integer. Parsing rejects payloads above this limit before allocating them.
|
|
618
|
+
The parser configures this limit once on its `strtok3` tokenizers, including tokenizers for embedded tags.
|
|
619
|
+
Oversized tokenizer allocations or stream lookahead reject with `strtok3.BufferSizeError`.
|
|
620
|
+
When using `parseFromTokenizer()`, also configure `maxBufferSize` on the supplied tokenizer at creation.
|
|
621
|
+
Increase it for files with unusually large metadata or cover art, or lower it for constrained environments.
|
|
622
|
+
Audio/video data that is skipped does not count toward this limit. This does not cap total process memory:
|
|
623
|
+
decoded metadata, multiple retained tags, and temporary copies can require additional memory.
|
|
610
624
|
|
|
611
625
|
- `mkvUseIndex`: `boolean` (default: `false`)
|
|
612
626
|
|
|
@@ -665,45 +679,84 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
|
|
|
665
679
|
|
|
666
680
|
#### `metadata.format.trackInfo`
|
|
667
681
|
|
|
668
|
-
|
|
669
|
-
|
|
682
|
+
`metadata.format.trackInfo` provides the same track abstraction for single-stream audio files and
|
|
683
|
+
multi-track containers (MPEG-4, Matroska, ASF, and Ogg). Tracks can carry audio, video, subtitles, or
|
|
684
|
+
metadata. Import `TrackType` from `music-metadata` to select tracks without knowing the container:
|
|
685
|
+
|
|
686
|
+
```js
|
|
687
|
+
import { parseFile, TrackType } from 'music-metadata';
|
|
688
|
+
|
|
689
|
+
const { format } = await parseFile('movie.mp4');
|
|
690
|
+
const audioTracks = format.trackInfo.filter(track => track.type === TrackType.audio);
|
|
691
|
+
const videoTracks = format.trackInfo.filter(track => track.type === TrackType.video);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Each entry describes one discovered track. The order follows container discovery and does not identify
|
|
695
|
+
a preferred track; use `type`, container flags, and language to choose one. In MPEG-4, the codec and
|
|
696
|
+
media properties describe the first sample description, even when the track has multiple sample descriptions. In Ogg, entries describe
|
|
697
|
+
logical media streams; Skeleton headers do not create media tracks. Single-stream audio parsers map
|
|
698
|
+
their format properties to one audio track after parsing.
|
|
699
|
+
|
|
700
|
+
Properties are optional when unavailable and are not copied between tracks. In particular, a container
|
|
701
|
+
or summary bitrate is not a per-track bitrate. MPEG-4 track timing comes from media headers (or parsed
|
|
702
|
+
audio fragments), and track bitrates from sample sizes and duration. ASF supplies audio/video stream
|
|
703
|
+
properties and stream bitrates from both standalone and nested Stream Properties Objects. Codec List
|
|
704
|
+
entries supply names for matching streams; unused codecs do not create tracks. Matroska applies
|
|
705
|
+
specified defaults for flags, language, channels, and sample rate, and prefers IETF language tags when present. Video duration/bitrate coverage and overall
|
|
706
|
+
video statistics remain incomplete. The existing `format` fields retain their summary semantics.
|
|
707
|
+
|
|
708
|
+
For Ogg/Opus, the per-track average bitrate uses the logical stream's payload bytes, including codec
|
|
709
|
+
headers and excluding Ogg framing and other streams. It requires the logical stream's end-of-stream
|
|
710
|
+
(EOS) page and a known duration; reaching input EOF alone is insufficient. Set `duration: true` to
|
|
711
|
+
allow scanning when needed. The summary `format.bitrate` retains
|
|
712
|
+
its existing file-size estimate and can differ from the per-track bitrate. Vorbis and Speex use nominal
|
|
713
|
+
bitrates when their headers provide them.
|
|
670
714
|
|
|
671
715
|
`metadata.format.trackInfo` is an array of [trackInfo](#trackinfo) objects, empty when no track information is available.
|
|
672
716
|
|
|
673
717
|
##### trackInfo
|
|
674
718
|
|
|
675
719
|
Individual track information. Defined in the TypeScript `ITrackInfo` interface:
|
|
676
|
-
- `trackInfo.
|
|
720
|
+
- `trackInfo.id?: number` Container track ID/number, or Ogg stream serial number; only meaningful within the file
|
|
721
|
+
- `trackInfo.type?: TrackType` Track type (`audio`, `video`, `subtitle`, `metadata`, or other known types)
|
|
722
|
+
- `trackInfo.codecId?: string` Container-specific codec identifier, such as `mp4a`, `A_AAC`, or ASF audio format tag `0x0161`
|
|
677
723
|
- `trackInfo.codecName?: string` Codec name
|
|
724
|
+
- `trackInfo.codecProfile?: string` Codec profile
|
|
725
|
+
- `trackInfo.duration?: number` Track duration in seconds
|
|
726
|
+
- `trackInfo.bitrate?: number` Encoded track bitrate in bits per second
|
|
727
|
+
- `trackInfo.lossless?: boolean` Whether the track uses lossless compression
|
|
678
728
|
- `trackInfo.codecSettings?: string` Codec settings
|
|
679
|
-
- `trackInfo.flagEnabled?: boolean`
|
|
680
|
-
- `trackInfo.flagDefault?: boolean`
|
|
681
|
-
- `trackInfo.
|
|
729
|
+
- `trackInfo.flagEnabled?: boolean` Whether the track is enabled; Matroska defaults to `true`, MPEG-4 uses the track header flag
|
|
730
|
+
- `trackInfo.flagDefault?: boolean` Whether the container marks the track as a default selection; Matroska defaults to `true`
|
|
731
|
+
- `trackInfo.flagForced?: boolean` Whether a subtitle track should be displayed without explicit selection
|
|
732
|
+
- `trackInfo.flagLacing?: boolean` Whether a Matroska track may contain blocks using lacing; defaults to `true`
|
|
682
733
|
- `trackInfo.name?: string` A human-readable track name.
|
|
683
734
|
- `trackInfo.language?: string` Specifies the language of the track
|
|
684
|
-
- `trackInfo.audio?: IAudioTrack`, see [`trackInfo.
|
|
685
|
-
- `trackInfo.video?: IVideoTrack`, see [`trackInfo.
|
|
686
|
-
|
|
687
|
-
##### `trackInfo.
|
|
688
|
-
|
|
689
|
-
- `
|
|
690
|
-
- `
|
|
691
|
-
- `
|
|
692
|
-
- `
|
|
693
|
-
- `
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
- `
|
|
699
|
-
- `
|
|
700
|
-
- `
|
|
701
|
-
- `
|
|
702
|
-
- `
|
|
703
|
-
- `
|
|
704
|
-
- `
|
|
705
|
-
- `
|
|
706
|
-
- `
|
|
735
|
+
- `trackInfo.audio?: IAudioTrack`, see [`trackInfo.audio`](#trackinfoaudio)
|
|
736
|
+
- `trackInfo.video?: IVideoTrack`, see [`trackInfo.video`](#trackinfovideo)
|
|
737
|
+
|
|
738
|
+
##### `trackInfo.audio`
|
|
739
|
+
|
|
740
|
+
- `trackInfo.audio.numberOfSamples?: number` Number of decoded sample frames (one sample per channel), when known
|
|
741
|
+
- `trackInfo.audio.samplingFrequency?: number` Sample rate in hertz; for Opus, the informational original input rate
|
|
742
|
+
- `trackInfo.audio.outputSamplingFrequency?: number` Output sample rate in hertz; Opus granules and sample counts use 48000 Hz
|
|
743
|
+
- `trackInfo.audio.channels?: number`
|
|
744
|
+
- `trackInfo.audio.channelPositions?: Uint8Array`
|
|
745
|
+
- `trackInfo.audio.bitDepth?: number`
|
|
746
|
+
|
|
747
|
+
##### `trackInfo.video`
|
|
748
|
+
|
|
749
|
+
- `trackInfo.video.frameRate?: number` Frames per second, when known
|
|
750
|
+
- `trackInfo.video.flagInterlaced?: boolean`
|
|
751
|
+
- `trackInfo.video.stereoMode?: number`
|
|
752
|
+
- `trackInfo.video.pixelWidth?: number`
|
|
753
|
+
- `trackInfo.video.pixelHeight?: number`
|
|
754
|
+
- `trackInfo.video.displayWidth?: number`
|
|
755
|
+
- `trackInfo.video.displayHeight?: number`
|
|
756
|
+
- `trackInfo.video.displayUnit?: number`
|
|
757
|
+
- `trackInfo.video.aspectRatioType?: number`
|
|
758
|
+
- `trackInfo.video.colourSpace?: Uint8Array`
|
|
759
|
+
- `trackInfo.video.gammaValue?: number`
|
|
707
760
|
|
|
708
761
|
#### `metadata.common`
|
|
709
762
|
|
package/lib/ParserFactory.js
CHANGED
|
@@ -5,7 +5,9 @@ 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 { getMaxMemoryAllocation } from './common/Allocation.js';
|
|
8
9
|
import { MetadataCollector } from './common/MetadataCollector.js';
|
|
10
|
+
import { createAudioTrackInfo } from './common/TrackInfo.js';
|
|
9
11
|
import { scanAppendingHeaders } from './core.js';
|
|
10
12
|
import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
|
|
11
13
|
import { dsfParserLoader } from './dsf/DsfLoader.js';
|
|
@@ -55,6 +57,7 @@ export class ParserFactory {
|
|
|
55
57
|
this.parsers.push(parser);
|
|
56
58
|
}
|
|
57
59
|
async parse(tokenizer, parserLoader, opts = {}) {
|
|
60
|
+
getMaxMemoryAllocation(opts);
|
|
58
61
|
if (tokenizer.supportsRandomAccess()) {
|
|
59
62
|
debug('tokenizer supports random-access, scanning for appending headers');
|
|
60
63
|
await scanAppendingHeaders(tokenizer, opts);
|
|
@@ -89,9 +92,14 @@ export class ParserFactory {
|
|
|
89
92
|
debug(`Loading ${parserLoader.parserType} parser...`);
|
|
90
93
|
const metadata = new MetadataCollector(opts);
|
|
91
94
|
const ParserImpl = await parserLoader.load();
|
|
92
|
-
const parser = new ParserImpl(metadata, tokenizer, opts
|
|
95
|
+
const parser = new ParserImpl(metadata, tokenizer, opts);
|
|
93
96
|
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
94
97
|
await parser.parse();
|
|
98
|
+
// Audio-only parsers describe their single stream through the format fields.
|
|
99
|
+
// Container parsers supply their own tracks; never copy aggregate fields to them.
|
|
100
|
+
if (metadata.format.trackInfo.length === 0 && metadata.format.hasAudio === true) {
|
|
101
|
+
metadata.addStreamInfo(createAudioTrackInfo(metadata.format));
|
|
102
|
+
}
|
|
95
103
|
if (metadata.format.trackInfo) {
|
|
96
104
|
if (metadata.format.hasAudio === undefined) {
|
|
97
105
|
metadata.setFormat('hasAudio', !!metadata.format.trackInfo.find(track => track.type === TrackType.audio));
|
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);
|
|
@@ -199,6 +177,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
199
177
|
// ToDo before
|
|
200
178
|
this.metadata.setFormat('lossless', true);
|
|
201
179
|
this.metadata.setFormat('container', "Monkey's Audio");
|
|
180
|
+
this.metadata.setFormat('codec', "Monkey's Audio");
|
|
202
181
|
this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
|
|
203
182
|
this.metadata.setFormat('sampleRate', header.sampleRate);
|
|
204
183
|
this.metadata.setFormat('numberOfChannels', header.channel);
|
package/lib/asf/AsfObject.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IGetToken, ITokenizer } from 'strtok3';
|
|
2
|
-
import type { AnyTagValue, IPicture, ITag } from '../type.js';
|
|
2
|
+
import type { AnyTagValue, IAudioTrack, IPicture, ITag, IVideoTrack } from '../type.js';
|
|
3
3
|
import AsfGuid from './AsfGuid.js';
|
|
4
4
|
declare const AsfContentParseError_base: {
|
|
5
5
|
new (message: string): {
|
|
@@ -199,6 +199,11 @@ export interface IStreamPropertiesObject {
|
|
|
199
199
|
* Error Correction Type
|
|
200
200
|
*/
|
|
201
201
|
errorCorrectionType: AsfGuid;
|
|
202
|
+
streamNumber: number;
|
|
203
|
+
codecId?: string;
|
|
204
|
+
audio?: IAudioTrack;
|
|
205
|
+
video?: IVideoTrack;
|
|
206
|
+
bitrate?: number;
|
|
202
207
|
}
|
|
203
208
|
/**
|
|
204
209
|
* Token for: 3.3 Stream Properties Object (mandatory, one per stream)
|
|
@@ -283,7 +288,7 @@ export interface IExtendedStreamPropertiesObject {
|
|
|
283
288
|
streamNameCount: number;
|
|
284
289
|
payloadExtensionSystems: number;
|
|
285
290
|
streamNames: IStreamName[];
|
|
286
|
-
streamPropertiesObject:
|
|
291
|
+
streamPropertiesObject: IStreamPropertiesObject | null;
|
|
287
292
|
}
|
|
288
293
|
/**
|
|
289
294
|
* 4.1 Extended Stream Properties Object (optional, 1 per media stream)
|
package/lib/asf/AsfObject.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// ASF Objects
|
|
2
2
|
import * as Token from 'token-types';
|
|
3
|
-
import * as util from '../common/Util.js';
|
|
4
3
|
import { AttachedPictureType } from '../id3v2/ID3v2Token.js';
|
|
5
4
|
import { makeUnexpectedFileContentError } from '../ParseError.js';
|
|
6
5
|
import AsfGuid from './AsfGuid.js';
|
|
@@ -97,6 +96,7 @@ export class State {
|
|
|
97
96
|
export class FilePropertiesObject extends State {
|
|
98
97
|
static guid = AsfGuid.FilePropertiesObject;
|
|
99
98
|
get(buf, off) {
|
|
99
|
+
const flags = Token.UINT32_LE.get(buf, off + 64);
|
|
100
100
|
return {
|
|
101
101
|
fileId: AsfGuid.fromBin(buf, off),
|
|
102
102
|
fileSize: Token.UINT64_LE.get(buf, off + 16),
|
|
@@ -106,8 +106,8 @@ export class FilePropertiesObject extends State {
|
|
|
106
106
|
sendDuration: Token.UINT64_LE.get(buf, off + 48),
|
|
107
107
|
preroll: Token.UINT64_LE.get(buf, off + 56),
|
|
108
108
|
flags: {
|
|
109
|
-
broadcast:
|
|
110
|
-
seekable:
|
|
109
|
+
broadcast: (flags & 1) !== 0,
|
|
110
|
+
seekable: (flags & 2) !== 0
|
|
111
111
|
},
|
|
112
112
|
// flagsNumeric: Token.UINT32_LE.get(buf, off + 64),
|
|
113
113
|
minimumDataPacketSize: Token.UINT32_LE.get(buf, off + 68),
|
|
@@ -123,11 +123,52 @@ export class FilePropertiesObject extends State {
|
|
|
123
123
|
export class StreamPropertiesObject extends State {
|
|
124
124
|
static guid = AsfGuid.StreamPropertiesObject;
|
|
125
125
|
get(buf, off) {
|
|
126
|
-
|
|
126
|
+
const available = Math.min(this.len, buf.length - off);
|
|
127
|
+
if (available < 54) {
|
|
128
|
+
throw new AsfContentParseError('Truncated Stream Properties Object');
|
|
129
|
+
}
|
|
130
|
+
const typeSpecificSize = Token.UINT32_LE.get(buf, off + 40);
|
|
131
|
+
const correctionSize = Token.UINT32_LE.get(buf, off + 44);
|
|
132
|
+
if (typeSpecificSize + correctionSize > available - 54) {
|
|
133
|
+
throw new AsfContentParseError('ASF stream data exceeds Stream Properties Object payload');
|
|
134
|
+
}
|
|
135
|
+
const result = {
|
|
127
136
|
streamType: AsfGuid.decodeMediaType(AsfGuid.fromBin(buf, off)),
|
|
128
|
-
errorCorrectionType: AsfGuid.fromBin(buf, off +
|
|
129
|
-
|
|
137
|
+
errorCorrectionType: AsfGuid.fromBin(buf, off + 16),
|
|
138
|
+
streamNumber: Token.UINT16_LE.get(buf, off + 48) & 0x7f
|
|
130
139
|
};
|
|
140
|
+
const data = off + 54;
|
|
141
|
+
if (result.streamType === 'audio' && typeSpecificSize >= 16) {
|
|
142
|
+
result.codecId = `0x${Token.UINT16_LE.get(buf, data).toString(16).padStart(4, '0')}`;
|
|
143
|
+
result.audio = {
|
|
144
|
+
channels: Token.UINT16_LE.get(buf, data + 2),
|
|
145
|
+
samplingFrequency: Token.UINT32_LE.get(buf, data + 4)
|
|
146
|
+
};
|
|
147
|
+
const bitDepth = Token.UINT16_LE.get(buf, data + 14);
|
|
148
|
+
if (bitDepth > 0) {
|
|
149
|
+
result.audio.bitDepth = bitDepth;
|
|
150
|
+
}
|
|
151
|
+
const bytesPerSecond = Token.UINT32_LE.get(buf, data + 8);
|
|
152
|
+
if (bytesPerSecond > 0) {
|
|
153
|
+
result.bitrate = bytesPerSecond * 8;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (result.streamType === 'video' && typeSpecificSize >= 11) {
|
|
157
|
+
result.video = {
|
|
158
|
+
pixelWidth: Token.UINT32_LE.get(buf, data),
|
|
159
|
+
pixelHeight: Token.UINT32_LE.get(buf, data + 4)
|
|
160
|
+
};
|
|
161
|
+
// ASF specification 9.2: DWORD width, DWORD height, BYTE flags, WORD format size.
|
|
162
|
+
// The packed BITMAPINFOHEADER follows at +11; its compression DWORD is at +16.
|
|
163
|
+
const formatSize = Token.UINT16_LE.get(buf, data + 9);
|
|
164
|
+
if (formatSize > typeSpecificSize - 11) {
|
|
165
|
+
throw new AsfContentParseError('ASF video format data exceeds stream data');
|
|
166
|
+
}
|
|
167
|
+
if (formatSize >= 20) {
|
|
168
|
+
result.codecId = new Token.StringType(4, 'ascii').get(buf, data + 27);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
131
172
|
}
|
|
132
173
|
}
|
|
133
174
|
/**
|
|
@@ -293,32 +334,68 @@ export class ExtendedContentDescriptionObjectState extends State {
|
|
|
293
334
|
export class ExtendedStreamPropertiesObjectState extends State {
|
|
294
335
|
static guid = AsfGuid.ExtendedStreamPropertiesObject;
|
|
295
336
|
get(buf, off) {
|
|
296
|
-
const
|
|
297
|
-
|
|
337
|
+
const end = off + Math.min(this.len, buf.length - off);
|
|
338
|
+
if (end - off < 64) {
|
|
339
|
+
throw new AsfContentParseError('Truncated Extended Stream Properties Object');
|
|
340
|
+
}
|
|
341
|
+
const flags = Token.UINT32_LE.get(buf, off + 44);
|
|
342
|
+
const result = {
|
|
298
343
|
startTime: Token.UINT64_LE.get(buf, off),
|
|
299
344
|
endTime: Token.UINT64_LE.get(buf, off + 8),
|
|
300
|
-
dataBitrate:
|
|
301
|
-
bufferSize:
|
|
302
|
-
initialBufferFullness:
|
|
303
|
-
alternateDataBitrate:
|
|
304
|
-
alternateBufferSize:
|
|
305
|
-
alternateInitialBufferFullness:
|
|
306
|
-
maximumObjectSize:
|
|
345
|
+
dataBitrate: Token.UINT32_LE.get(buf, off + 16),
|
|
346
|
+
bufferSize: Token.UINT32_LE.get(buf, off + 20),
|
|
347
|
+
initialBufferFullness: Token.UINT32_LE.get(buf, off + 24),
|
|
348
|
+
alternateDataBitrate: Token.UINT32_LE.get(buf, off + 28),
|
|
349
|
+
alternateBufferSize: Token.UINT32_LE.get(buf, off + 32),
|
|
350
|
+
alternateInitialBufferFullness: Token.UINT32_LE.get(buf, off + 36),
|
|
351
|
+
maximumObjectSize: Token.UINT32_LE.get(buf, off + 40),
|
|
307
352
|
flags: {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
resendLiveCleanpointsFlag: util.getBit(buf, off + 40, 2)
|
|
353
|
+
reliableFlag: (flags & 1) !== 0,
|
|
354
|
+
seekableFlag: (flags & 2) !== 0,
|
|
355
|
+
resendLiveCleanpointsFlag: (flags & 8) !== 0
|
|
312
356
|
},
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
streamNames: [], // ToDo
|
|
357
|
+
streamNumber: Token.UINT16_LE.get(buf, off + 48) & 0x7f,
|
|
358
|
+
streamLanguageId: Token.UINT16_LE.get(buf, off + 50),
|
|
359
|
+
averageTimePerFrame: Number(Token.UINT64_LE.get(buf, off + 52)),
|
|
360
|
+
streamNameCount: Token.UINT16_LE.get(buf, off + 60),
|
|
361
|
+
payloadExtensionSystems: Token.UINT16_LE.get(buf, off + 62),
|
|
362
|
+
streamNames: [],
|
|
320
363
|
streamPropertiesObject: null
|
|
321
364
|
};
|
|
365
|
+
let cursor = off + 64;
|
|
366
|
+
const requireBytes = (length) => {
|
|
367
|
+
if (length > end - cursor) {
|
|
368
|
+
throw new AsfContentParseError('ASF extended stream data exceeds object payload');
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
for (let index = 0; index < result.streamNameCount; ++index) {
|
|
372
|
+
requireBytes(4);
|
|
373
|
+
const language = Token.UINT16_LE.get(buf, cursor);
|
|
374
|
+
const length = Token.UINT16_LE.get(buf, cursor + 2);
|
|
375
|
+
cursor += 4;
|
|
376
|
+
requireBytes(length);
|
|
377
|
+
result.streamNames.push({
|
|
378
|
+
streamLanguageId: language,
|
|
379
|
+
streamName: parseUnicodeAttr(buf.subarray(cursor, cursor + length))
|
|
380
|
+
});
|
|
381
|
+
cursor += length;
|
|
382
|
+
}
|
|
383
|
+
for (let index = 0; index < result.payloadExtensionSystems; ++index) {
|
|
384
|
+
requireBytes(22);
|
|
385
|
+
const length = Token.UINT32_LE.get(buf, cursor + 18);
|
|
386
|
+
cursor += 22;
|
|
387
|
+
requireBytes(length);
|
|
388
|
+
cursor += length;
|
|
389
|
+
}
|
|
390
|
+
if (cursor < end) {
|
|
391
|
+
requireBytes(HeaderObjectToken.len);
|
|
392
|
+
const header = HeaderObjectToken.get(buf, cursor);
|
|
393
|
+
if (!header.objectId.equals(AsfGuid.StreamPropertiesObject) || header.objectSize !== end - cursor) {
|
|
394
|
+
throw new AsfContentParseError('Invalid embedded ASF Stream Properties Object');
|
|
395
|
+
}
|
|
396
|
+
result.streamPropertiesObject = new StreamPropertiesObject(header).get(buf, cursor + HeaderObjectToken.len);
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
322
399
|
}
|
|
323
400
|
}
|
|
324
401
|
/**
|
package/lib/asf/AsfParser.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { BasicParser } from '../common/BasicParser.js';
|
|
|
10
10
|
* - https://msdn.microsoft.com/en-us/library/windows/desktop/ee663575(v=vs.85).aspx
|
|
11
11
|
*/
|
|
12
12
|
export declare class AsfParser extends BasicParser {
|
|
13
|
+
private streams;
|
|
14
|
+
private codecs;
|
|
15
|
+
private streamBitrates;
|
|
13
16
|
parse(): Promise<void>;
|
|
14
17
|
private parseObjectHeaders;
|
|
15
18
|
private addTags;
|