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.
Files changed (52) hide show
  1. package/README.md +44 -9
  2. package/lib/ParserFactory.js +8 -3
  3. package/lib/aiff/AiffParser.js +8 -7
  4. package/lib/apev2/APEv2Parser.js +4 -26
  5. package/lib/asf/AsfParser.d.ts +1 -0
  6. package/lib/asf/AsfParser.js +39 -19
  7. package/lib/caf/CafLoader.d.ts +2 -0
  8. package/lib/caf/CafLoader.js +8 -0
  9. package/lib/caf/CafParser.d.ts +37 -0
  10. package/lib/caf/CafParser.js +253 -0
  11. package/lib/caf/CafTagMap.d.ts +8 -0
  12. package/lib/caf/CafTagMap.js +38 -0
  13. package/lib/caf/CafToken.d.ts +75 -0
  14. package/lib/caf/CafToken.js +135 -0
  15. package/lib/common/Allocation.d.ts +14 -0
  16. package/lib/common/Allocation.js +70 -0
  17. package/lib/common/BasicParser.d.ts +2 -0
  18. package/lib/common/BasicParser.js +5 -0
  19. package/lib/common/CombinedTagMapper.js +3 -1
  20. package/lib/common/GenericTagTypes.d.ts +1 -1
  21. package/lib/common/MetadataCollector.js +9 -1
  22. package/lib/common/TrackInfo.d.ts +3 -0
  23. package/lib/common/TrackInfo.js +29 -0
  24. package/lib/core.js +4 -7
  25. package/lib/dsdiff/DsdiffParser.d.ts +3 -0
  26. package/lib/dsdiff/DsdiffParser.js +70 -30
  27. package/lib/ebml/EbmlIterator.d.ts +4 -1
  28. package/lib/ebml/EbmlIterator.js +19 -27
  29. package/lib/flac/FlacParser.js +10 -7
  30. package/lib/id3v2/ID3v2Parser.js +2 -1
  31. package/lib/index.js +3 -4
  32. package/lib/matroska/MatroskaParser.d.ts +4 -0
  33. package/lib/matroska/MatroskaParser.js +65 -2
  34. package/lib/matroska/types.d.ts +1 -0
  35. package/lib/mp4/Atom.js +1 -0
  36. package/lib/mp4/AtomToken.d.ts +2 -0
  37. package/lib/mp4/AtomToken.js +25 -8
  38. package/lib/mp4/MP4Parser.d.ts +6 -1
  39. package/lib/mp4/MP4Parser.js +192 -81
  40. package/lib/ogg/OggParser.js +5 -1
  41. package/lib/ogg/theora/Theora.d.ts +1 -0
  42. package/lib/ogg/theora/Theora.js +2 -1
  43. package/lib/ogg/theora/TheoraStream.d.ts +4 -1
  44. package/lib/ogg/theora/TheoraStream.js +30 -4
  45. package/lib/ogg/vorbis/VorbisDecoder.d.ts +1 -0
  46. package/lib/ogg/vorbis/VorbisDecoder.js +12 -0
  47. package/lib/ogg/vorbis/VorbisStream.d.ts +2 -1
  48. package/lib/ogg/vorbis/VorbisStream.js +14 -6
  49. package/lib/type.d.ts +12 -2
  50. package/lib/wav/WaveParser.js +13 -12
  51. package/lib/wavpack/WavPackParser.js +5 -2
  52. 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` Number bits per second of encoded audio file
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. MPEG-4 track timing comes from media headers (or parsed
691
- audio fragments), and track bitrates from sample sizes and duration. ASF supplies audio/video stream
692
- properties and stream bitrates from both standalone and nested Stream Properties Objects. Codec List
693
- entries supply names for matching streams; unused codecs do not create tracks. Matroska applies
694
- specified defaults for flags, language, channels, and sample rate, and prefers IETF language tags when present. Video duration/bitrate coverage and overall
695
- video statistics remain incomplete. The existing `format` fields retain their summary semantics.
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
@@ -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
  /**
@@ -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);
@@ -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;
@@ -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.tokenizer.readToken(AsfObject.TopLevelHeaderObjectToken);
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
- const bitrate = this.streamBitrates.get(stream.streamNumber) ?? stream.bitrate;
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.tokenizer.readToken(new AsfObject.FilePropertiesObject(header));
93
- this.metadata.setFormat('duration', Number(fpo.playDuration / BigInt(1000)) / 10000 - Number(fpo.preroll) / 1000);
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.tokenizer.readToken(new AsfObject.StreamPropertiesObject(header));
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.tokenizer.readToken(extensionHeaderToken);
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.tokenizer.readToken(new AsfObject.ContentDescriptionObjectState(header));
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.tokenizer.readToken(new AsfObject.ExtendedContentDescriptionObjectState(header));
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.tokenizer.readToken(Token.UINT16_LE);
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.tokenizer.readToken(Token.UINT16_LE)) & 0x7f;
163
- this.streamBitrates.set(streamNumber, await this.tokenizer.readToken(Token.UINT32_LE));
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.tokenizer.readToken(AsfObject.HeaderObjectToken);
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
- if (payloadSize > maxAsfMetadataObjectSize) {
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.tokenizer.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
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.tokenizer.readToken(new AsfObject.MetadataObjectState(header));
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.tokenizer.readToken(new AsfObject.MetadataLibraryObjectState(header));
277
+ const mlTags = await this.readToken(new AsfObject.MetadataLibraryObjectState(header));
258
278
  await this.addTags(mlTags);
259
279
  break;
260
280
  }
@@ -0,0 +1,2 @@
1
+ import type { IParserLoader } from '../ParserFactory.js';
2
+ export declare const cafParserLoader: IParserLoader;
@@ -0,0 +1,8 @@
1
+ export const cafParserLoader = {
2
+ parserType: 'caf',
3
+ extensions: ['.caf', '.caff'],
4
+ mimeTypes: ['audio/caf', 'application/caf'],
5
+ async load() {
6
+ return (await import('./CafParser.js')).CafParser;
7
+ }
8
+ };
@@ -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
+ }