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.
Files changed (64) hide show
  1. package/README.md +83 -30
  2. package/lib/ParserFactory.js +9 -1
  3. package/lib/aiff/AiffParser.js +8 -7
  4. package/lib/apev2/APEv2Parser.js +5 -26
  5. package/lib/asf/AsfObject.d.ts +7 -2
  6. package/lib/asf/AsfObject.js +103 -26
  7. package/lib/asf/AsfParser.d.ts +3 -0
  8. package/lib/asf/AsfParser.js +92 -25
  9. package/lib/common/Allocation.d.ts +14 -0
  10. package/lib/common/Allocation.js +70 -0
  11. package/lib/common/BasicParser.d.ts +2 -0
  12. package/lib/common/BasicParser.js +5 -0
  13. package/lib/common/MetadataCollector.js +4 -0
  14. package/lib/common/TrackInfo.d.ts +5 -0
  15. package/lib/common/TrackInfo.js +39 -0
  16. package/lib/common/Util.d.ts +1 -1
  17. package/lib/common/Util.js +6 -12
  18. package/lib/core.d.ts +1 -1
  19. package/lib/core.js +5 -8
  20. package/lib/dsdiff/DsdiffParser.d.ts +3 -0
  21. package/lib/dsdiff/DsdiffParser.js +70 -30
  22. package/lib/ebml/EbmlIterator.d.ts +4 -1
  23. package/lib/ebml/EbmlIterator.js +19 -27
  24. package/lib/flac/FlacParser.js +10 -7
  25. package/lib/id3v2/ID3v2Parser.js +2 -1
  26. package/lib/index.js +3 -4
  27. package/lib/matroska/MatroskaDtd.js +1 -0
  28. package/lib/matroska/MatroskaParser.js +32 -14
  29. package/lib/matroska/types.d.ts +8 -32
  30. package/lib/matroska/types.js +1 -18
  31. package/lib/mp4/AtomToken.d.ts +11 -0
  32. package/lib/mp4/AtomToken.js +85 -12
  33. package/lib/mp4/MP4Parser.d.ts +8 -1
  34. package/lib/mp4/MP4Parser.js +283 -79
  35. package/lib/mpeg/AdtsFrameHeader.d.ts +17 -0
  36. package/lib/mpeg/AdtsFrameHeader.js +44 -0
  37. package/lib/mpeg/MpegFrameHeader.d.ts +41 -0
  38. package/lib/mpeg/MpegFrameHeader.js +84 -0
  39. package/lib/mpeg/MpegParser.d.ts +11 -25
  40. package/lib/mpeg/MpegParser.js +60 -333
  41. package/lib/ogg/OggLoader.js +1 -1
  42. package/lib/ogg/OggParser.js +44 -3
  43. package/lib/ogg/OggStreamMetadata.d.ts +14 -0
  44. package/lib/ogg/OggStreamMetadata.js +27 -0
  45. package/lib/ogg/OggToken.d.ts +1 -0
  46. package/lib/ogg/OggToken.js +2 -1
  47. package/lib/ogg/flac/FlacStream.d.ts +3 -1
  48. package/lib/ogg/flac/FlacStream.js +17 -3
  49. package/lib/ogg/opus/OpusStream.js +1 -0
  50. package/lib/ogg/speex/SpeexStream.js +1 -0
  51. package/lib/ogg/theora/Theora.d.ts +4 -0
  52. package/lib/ogg/theora/Theora.js +5 -1
  53. package/lib/ogg/theora/TheoraStream.d.ts +1 -1
  54. package/lib/ogg/theora/TheoraStream.js +13 -0
  55. package/lib/ogg/vorbis/Vorbis.js +3 -3
  56. package/lib/ogg/vorbis/VorbisDecoder.d.ts +1 -0
  57. package/lib/ogg/vorbis/VorbisDecoder.js +12 -0
  58. package/lib/ogg/vorbis/VorbisStream.d.ts +2 -1
  59. package/lib/ogg/vorbis/VorbisStream.js +15 -6
  60. package/lib/type.d.ts +40 -3
  61. package/lib/type.js +22 -1
  62. package/lib/wav/WaveParser.js +13 -12
  63. package/lib/wavpack/WavPackParser.js +5 -2
  64. 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 scans the `mdat` atom for chapters.
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
- Containers such as Matroska and MPEG-4 can contain multiple audio and video tracks.
669
- The experimental `metadata.format.trackInfo` property describes these individual tracks.
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.type?: TrackType` Track type
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` Set if the track is usable, default: `true`
680
- - `trackInfo.flagDefault?: boolean` Set if that track (audio, video or subs) SHOULD be active if no language found matches the user preference.
681
- - `trackInfo.flagLacing?: boolean` Set if the track **may** contain blocks using lacing
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.audioTrack`](#trackinfoaudiotrack)
685
- - `trackInfo.video?: IVideoTrack`, see [`trackInfo.videoTrack`](#trackinfovideotrack)
686
-
687
- ##### `trackInfo.audioTrack`
688
-
689
- - `audioTrack.samplingFrequency?: number`
690
- - `audioTrack.outputSamplingFrequency?: number`
691
- - `audioTrack.channels?: number`
692
- - `audioTrack.channelPositions?: Uint8Array`
693
- - `audioTrack.bitDepth?: number`
694
-
695
- ##### `trackInfo.videoTrack`
696
-
697
- - `videoTrack.flagInterlaced?: boolean`
698
- - `videoTrack.stereoMode?: number`
699
- - `videoTrack.pixelWidth?: number`
700
- - `videoTrack.pixelHeight?: number`
701
- - `videoTrack.displayWidth?: number`
702
- - `videoTrack.displayHeight?: number`
703
- - `videoTrack.displayUnit?: number`
704
- - `videoTrack.aspectRatioType?: number`
705
- - `videoTrack.colourSpace?: Uint8Array`
706
- - `videoTrack.gammaValue?: number`
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
 
@@ -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));
@@ -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.tokenizer.readToken(iff.Header);
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.tokenizer.readToken(FourCcToken);
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.tokenizer.readToken(iff.Header);
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.tokenizer.readToken(new AiffToken.Common(header, this.isCompressed));
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.tokenizer.readToken(new Token.Uint8ArrayType(header.chunkSize));
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.tokenizer.readToken(new Token.StringType(header.chunkSize, 'ascii'));
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())
@@ -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 = new Uint8Array(remaining);
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
- const chunkSize = 64 * 1024;
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);
@@ -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: number | null;
291
+ streamPropertiesObject: IStreamPropertiesObject | null;
287
292
  }
288
293
  /**
289
294
  * 4.1 Extended Stream Properties Object (optional, 1 per media stream)
@@ -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: util.getBit(buf, off + 64, 24),
110
- seekable: util.getBit(buf, off + 64, 25)
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
- return {
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 + 8)
129
- // ToDo
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 view = new DataView(buf.buffer, off);
297
- return {
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: view.getInt32(12, true),
301
- bufferSize: view.getInt32(16, true),
302
- initialBufferFullness: view.getInt32(20, true),
303
- alternateDataBitrate: view.getInt32(24, true),
304
- alternateBufferSize: view.getInt32(28, true),
305
- alternateInitialBufferFullness: view.getInt32(32, true),
306
- maximumObjectSize: view.getInt32(36, true),
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
- // ToDo, check flag positions
309
- reliableFlag: util.getBit(buf, off + 40, 0),
310
- seekableFlag: util.getBit(buf, off + 40, 1),
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
- // flagsNumeric: Token.UINT32_LE.get(buf, off + 64),
314
- streamNumber: view.getInt16(42, true),
315
- streamLanguageId: view.getInt16(44, true),
316
- averageTimePerFrame: view.getInt32(52, true),
317
- streamNameCount: view.getInt32(54, true),
318
- payloadExtensionSystems: view.getInt32(56, true),
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
  /**
@@ -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;