music-metadata 12.0.0 → 12.1.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 +72 -30
- package/lib/ParserFactory.js +6 -0
- package/lib/apev2/APEv2Parser.js +1 -0
- 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 +81 -12
- 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 +1 -1
- package/lib/matroska/MatroskaDtd.js +1 -0
- package/lib/matroska/MatroskaParser.js +31 -13
- 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 +6 -0
- package/lib/mp4/MP4Parser.js +281 -75
- 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 +42 -2
- 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/VorbisStream.js +1 -0
- package/lib/type.d.ts +34 -3
- package/lib/type.js +22 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -606,7 +606,10 @@ 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.
|
|
610
613
|
|
|
611
614
|
- `mkvUseIndex`: `boolean` (default: `false`)
|
|
612
615
|
|
|
@@ -665,45 +668,84 @@ Audio format information. Defined in the TypeScript `IFormat` interface:
|
|
|
665
668
|
|
|
666
669
|
#### `metadata.format.trackInfo`
|
|
667
670
|
|
|
668
|
-
|
|
669
|
-
|
|
671
|
+
`metadata.format.trackInfo` provides the same track abstraction for single-stream audio files and
|
|
672
|
+
multi-track containers (MPEG-4, Matroska, ASF, and Ogg). Tracks can carry audio, video, subtitles, or
|
|
673
|
+
metadata. Import `TrackType` from `music-metadata` to select tracks without knowing the container:
|
|
674
|
+
|
|
675
|
+
```js
|
|
676
|
+
import { parseFile, TrackType } from 'music-metadata';
|
|
677
|
+
|
|
678
|
+
const { format } = await parseFile('movie.mp4');
|
|
679
|
+
const audioTracks = format.trackInfo.filter(track => track.type === TrackType.audio);
|
|
680
|
+
const videoTracks = format.trackInfo.filter(track => track.type === TrackType.video);
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
Each entry describes one discovered track. The order follows container discovery and does not identify
|
|
684
|
+
a preferred track; use `type`, container flags, and language to choose one. In MPEG-4, the codec and
|
|
685
|
+
media properties describe the first sample description, even when the track has multiple sample descriptions. In Ogg, entries describe
|
|
686
|
+
logical media streams; Skeleton headers do not create media tracks. Single-stream audio parsers map
|
|
687
|
+
their format properties to one audio track after parsing.
|
|
688
|
+
|
|
689
|
+
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.
|
|
696
|
+
|
|
697
|
+
For Ogg/Opus, the per-track average bitrate uses the logical stream's payload bytes, including codec
|
|
698
|
+
headers and excluding Ogg framing and other streams. It requires the logical stream's end-of-stream
|
|
699
|
+
(EOS) page and a known duration; reaching input EOF alone is insufficient. Set `duration: true` to
|
|
700
|
+
allow scanning when needed. The summary `format.bitrate` retains
|
|
701
|
+
its existing file-size estimate and can differ from the per-track bitrate. Vorbis and Speex use nominal
|
|
702
|
+
bitrates when their headers provide them.
|
|
670
703
|
|
|
671
704
|
`metadata.format.trackInfo` is an array of [trackInfo](#trackinfo) objects, empty when no track information is available.
|
|
672
705
|
|
|
673
706
|
##### trackInfo
|
|
674
707
|
|
|
675
708
|
Individual track information. Defined in the TypeScript `ITrackInfo` interface:
|
|
676
|
-
- `trackInfo.
|
|
709
|
+
- `trackInfo.id?: number` Container track ID/number, or Ogg stream serial number; only meaningful within the file
|
|
710
|
+
- `trackInfo.type?: TrackType` Track type (`audio`, `video`, `subtitle`, `metadata`, or other known types)
|
|
711
|
+
- `trackInfo.codecId?: string` Container-specific codec identifier, such as `mp4a`, `A_AAC`, or ASF audio format tag `0x0161`
|
|
677
712
|
- `trackInfo.codecName?: string` Codec name
|
|
713
|
+
- `trackInfo.codecProfile?: string` Codec profile
|
|
714
|
+
- `trackInfo.duration?: number` Track duration in seconds
|
|
715
|
+
- `trackInfo.bitrate?: number` Encoded track bitrate in bits per second
|
|
716
|
+
- `trackInfo.lossless?: boolean` Whether the track uses lossless compression
|
|
678
717
|
- `trackInfo.codecSettings?: string` Codec settings
|
|
679
|
-
- `trackInfo.flagEnabled?: boolean`
|
|
680
|
-
- `trackInfo.flagDefault?: boolean`
|
|
681
|
-
- `trackInfo.
|
|
718
|
+
- `trackInfo.flagEnabled?: boolean` Whether the track is enabled; Matroska defaults to `true`, MPEG-4 uses the track header flag
|
|
719
|
+
- `trackInfo.flagDefault?: boolean` Whether the container marks the track as a default selection; Matroska defaults to `true`
|
|
720
|
+
- `trackInfo.flagForced?: boolean` Whether a subtitle track should be displayed without explicit selection
|
|
721
|
+
- `trackInfo.flagLacing?: boolean` Whether a Matroska track may contain blocks using lacing; defaults to `true`
|
|
682
722
|
- `trackInfo.name?: string` A human-readable track name.
|
|
683
723
|
- `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
|
-
- `
|
|
724
|
+
- `trackInfo.audio?: IAudioTrack`, see [`trackInfo.audio`](#trackinfoaudio)
|
|
725
|
+
- `trackInfo.video?: IVideoTrack`, see [`trackInfo.video`](#trackinfovideo)
|
|
726
|
+
|
|
727
|
+
##### `trackInfo.audio`
|
|
728
|
+
|
|
729
|
+
- `trackInfo.audio.numberOfSamples?: number` Number of decoded sample frames (one sample per channel), when known
|
|
730
|
+
- `trackInfo.audio.samplingFrequency?: number` Sample rate in hertz; for Opus, the informational original input rate
|
|
731
|
+
- `trackInfo.audio.outputSamplingFrequency?: number` Output sample rate in hertz; Opus granules and sample counts use 48000 Hz
|
|
732
|
+
- `trackInfo.audio.channels?: number`
|
|
733
|
+
- `trackInfo.audio.channelPositions?: Uint8Array`
|
|
734
|
+
- `trackInfo.audio.bitDepth?: number`
|
|
735
|
+
|
|
736
|
+
##### `trackInfo.video`
|
|
737
|
+
|
|
738
|
+
- `trackInfo.video.frameRate?: number` Frames per second, when known
|
|
739
|
+
- `trackInfo.video.flagInterlaced?: boolean`
|
|
740
|
+
- `trackInfo.video.stereoMode?: number`
|
|
741
|
+
- `trackInfo.video.pixelWidth?: number`
|
|
742
|
+
- `trackInfo.video.pixelHeight?: number`
|
|
743
|
+
- `trackInfo.video.displayWidth?: number`
|
|
744
|
+
- `trackInfo.video.displayHeight?: number`
|
|
745
|
+
- `trackInfo.video.displayUnit?: number`
|
|
746
|
+
- `trackInfo.video.aspectRatioType?: number`
|
|
747
|
+
- `trackInfo.video.colourSpace?: Uint8Array`
|
|
748
|
+
- `trackInfo.video.gammaValue?: number`
|
|
707
749
|
|
|
708
750
|
#### `metadata.common`
|
|
709
751
|
|
package/lib/ParserFactory.js
CHANGED
|
@@ -6,6 +6,7 @@ import { aiffParserLoader } from './aiff/AiffLoader.js';
|
|
|
6
6
|
import { apeParserLoader } from './apev2/Apev2Loader.js';
|
|
7
7
|
import { asfParserLoader } from './asf/AsfLoader.js';
|
|
8
8
|
import { MetadataCollector } from './common/MetadataCollector.js';
|
|
9
|
+
import { createAudioTrackInfo } from './common/TrackInfo.js';
|
|
9
10
|
import { scanAppendingHeaders } from './core.js';
|
|
10
11
|
import { dsdiffParserLoader } from './dsdiff/DsdiffLoader.js';
|
|
11
12
|
import { dsfParserLoader } from './dsf/DsfLoader.js';
|
|
@@ -92,6 +93,11 @@ export class ParserFactory {
|
|
|
92
93
|
const parser = new ParserImpl(metadata, tokenizer, opts ?? {});
|
|
93
94
|
debug(`Parser ${parserLoader.parserType} loaded`);
|
|
94
95
|
await parser.parse();
|
|
96
|
+
// Audio-only parsers describe their single stream through the format fields.
|
|
97
|
+
// Container parsers supply their own tracks; never copy aggregate fields to them.
|
|
98
|
+
if (metadata.format.trackInfo.length === 0 && metadata.format.hasAudio === true) {
|
|
99
|
+
metadata.addStreamInfo(createAudioTrackInfo(metadata.format));
|
|
100
|
+
}
|
|
95
101
|
if (metadata.format.trackInfo) {
|
|
96
102
|
if (metadata.format.hasAudio === undefined) {
|
|
97
103
|
metadata.setFormat('hasAudio', !!metadata.format.trackInfo.find(track => track.type === TrackType.audio));
|
package/lib/apev2/APEv2Parser.js
CHANGED
|
@@ -199,6 +199,7 @@ export class APEv2Parser extends BasicParser {
|
|
|
199
199
|
// ToDo before
|
|
200
200
|
this.metadata.setFormat('lossless', true);
|
|
201
201
|
this.metadata.setFormat('container', "Monkey's Audio");
|
|
202
|
+
this.metadata.setFormat('codec', "Monkey's Audio");
|
|
202
203
|
this.metadata.setFormat('bitsPerSample', header.bitsPerSample);
|
|
203
204
|
this.metadata.setFormat('sampleRate', header.sampleRate);
|
|
204
205
|
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;
|
package/lib/asf/AsfParser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import initDebug from 'debug';
|
|
2
2
|
import { EndOfStreamError } from 'strtok3';
|
|
3
|
+
import * as Token from 'token-types';
|
|
3
4
|
import { BasicParser } from '../common/BasicParser.js';
|
|
4
5
|
import { TrackType } from '../type.js';
|
|
5
6
|
import AsfGuid from './AsfGuid.js';
|
|
@@ -19,6 +20,9 @@ const maxAsfMetadataObjectSize = 16 * 1024 * 1024;
|
|
|
19
20
|
* - https://msdn.microsoft.com/en-us/library/windows/desktop/ee663575(v=vs.85).aspx
|
|
20
21
|
*/
|
|
21
22
|
export class AsfParser extends BasicParser {
|
|
23
|
+
streams = new Map();
|
|
24
|
+
codecs = [];
|
|
25
|
+
streamBitrates = new Map();
|
|
22
26
|
async parse() {
|
|
23
27
|
const header = await this.tokenizer.readToken(AsfObject.TopLevelHeaderObjectToken);
|
|
24
28
|
if (!header.objectId.equals(AsfGuid.HeaderObject)) {
|
|
@@ -28,6 +32,52 @@ export class AsfParser extends BasicParser {
|
|
|
28
32
|
throw new AsfContentParseError(`Unrealistic number of ASF header objects: ${header.numberOfHeaderObjects}`);
|
|
29
33
|
}
|
|
30
34
|
await this.parseObjectHeaders(header.numberOfHeaderObjects, header.objectSize - AsfObject.TopLevelHeaderObjectToken.len);
|
|
35
|
+
for (const stream of this.streams.values()) {
|
|
36
|
+
const type = stream.streamType === 'audio'
|
|
37
|
+
? TrackType.audio
|
|
38
|
+
: stream.streamType === 'video'
|
|
39
|
+
? TrackType.video
|
|
40
|
+
: stream.streamType === 'command'
|
|
41
|
+
? TrackType.control
|
|
42
|
+
: stream.streamType === 'binary'
|
|
43
|
+
? TrackType.metadata
|
|
44
|
+
: undefined;
|
|
45
|
+
const codec = this.codecs.find(codec => {
|
|
46
|
+
if (type === TrackType.audio && codec.type.audioCodec && codec.information.length >= 2) {
|
|
47
|
+
return stream.codecId === `0x${Token.UINT16_LE.get(codec.information, 0).toString(16).padStart(4, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
return (type === TrackType.video &&
|
|
50
|
+
codec.type.videoCodec &&
|
|
51
|
+
codec.information.length >= 4 &&
|
|
52
|
+
stream.codecId === new Token.StringType(4, 'ascii').get(codec.information, 0));
|
|
53
|
+
});
|
|
54
|
+
const track = { id: stream.streamNumber, type };
|
|
55
|
+
if (stream.codecId !== undefined) {
|
|
56
|
+
track.codecId = stream.codecId;
|
|
57
|
+
}
|
|
58
|
+
if (codec) {
|
|
59
|
+
track.codecName = codec.codecName;
|
|
60
|
+
}
|
|
61
|
+
if (stream.audio) {
|
|
62
|
+
track.audio = stream.audio;
|
|
63
|
+
}
|
|
64
|
+
if (stream.video) {
|
|
65
|
+
track.video = stream.video;
|
|
66
|
+
}
|
|
67
|
+
const bitrate = this.streamBitrates.get(stream.streamNumber) ?? stream.bitrate;
|
|
68
|
+
if (bitrate !== undefined) {
|
|
69
|
+
track.bitrate = bitrate;
|
|
70
|
+
}
|
|
71
|
+
this.metadata.addStreamInfo(track);
|
|
72
|
+
}
|
|
73
|
+
const audio = [...this.streams.values()].find(stream => stream.audio)?.audio;
|
|
74
|
+
if (audio) {
|
|
75
|
+
this.metadata.setFormat('sampleRate', audio.samplingFrequency);
|
|
76
|
+
this.metadata.setFormat('numberOfChannels', audio.channels);
|
|
77
|
+
if (audio.bitDepth !== undefined) {
|
|
78
|
+
this.metadata.setFormat('bitsPerSample', audio.bitDepth);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
31
81
|
}
|
|
32
82
|
async parseObjectHeaders(numberOfObjectHeaders, remainingHeaderSize) {
|
|
33
83
|
let tags;
|
|
@@ -49,6 +99,13 @@ export class AsfParser extends BasicParser {
|
|
|
49
99
|
this.validateAllocationSize(payloadSize, 'Stream Properties Object');
|
|
50
100
|
const spo = await this.tokenizer.readToken(new AsfObject.StreamPropertiesObject(header));
|
|
51
101
|
this.metadata.setFormat('container', `ASF/${spo.streamType}`);
|
|
102
|
+
if (spo.streamType === 'audio') {
|
|
103
|
+
this.metadata.setFormat('hasAudio', true);
|
|
104
|
+
}
|
|
105
|
+
else if (spo.streamType === 'video') {
|
|
106
|
+
this.metadata.setFormat('hasVideo', true);
|
|
107
|
+
}
|
|
108
|
+
this.streams.set(spo.streamNumber, spo);
|
|
52
109
|
break;
|
|
53
110
|
}
|
|
54
111
|
case AsfObject.HeaderExtensionObject.guid.str: {
|
|
@@ -85,12 +142,7 @@ export class AsfParser extends BasicParser {
|
|
|
85
142
|
const message = error instanceof Error ? error.message : String(error);
|
|
86
143
|
throw new AsfContentParseError(`Invalid ASF Codec List Object: ${message}`);
|
|
87
144
|
}
|
|
88
|
-
codecs.
|
|
89
|
-
this.metadata.addStreamInfo({
|
|
90
|
-
type: codec.type.videoCodec ? TrackType.video : TrackType.audio,
|
|
91
|
-
codecName: codec.codecName
|
|
92
|
-
});
|
|
93
|
-
});
|
|
145
|
+
this.codecs.push(...codecs);
|
|
94
146
|
const audioCodecs = codecs
|
|
95
147
|
.filter(codec => codec.type.audioCodec)
|
|
96
148
|
.map(codec => codec.codecName)
|
|
@@ -98,10 +150,21 @@ export class AsfParser extends BasicParser {
|
|
|
98
150
|
this.metadata.setFormat('codec', audioCodecs);
|
|
99
151
|
break;
|
|
100
152
|
}
|
|
101
|
-
case AsfGuid.StreamBitratePropertiesObject.str:
|
|
102
|
-
|
|
103
|
-
|
|
153
|
+
case AsfGuid.StreamBitratePropertiesObject.str: {
|
|
154
|
+
if (payloadSize < 2) {
|
|
155
|
+
throw new AsfContentParseError('Truncated Stream Bitrate Properties Object');
|
|
156
|
+
}
|
|
157
|
+
const count = await this.tokenizer.readToken(Token.UINT16_LE);
|
|
158
|
+
if (count * 6 > payloadSize - 2) {
|
|
159
|
+
throw new AsfContentParseError('ASF stream bitrate records exceed object payload');
|
|
160
|
+
}
|
|
161
|
+
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));
|
|
164
|
+
}
|
|
165
|
+
await this.ignorePayload(payloadSize - 2 - count * 6, 'Stream Bitrate Properties Object');
|
|
104
166
|
break;
|
|
167
|
+
}
|
|
105
168
|
case AsfGuid.PaddingObject.str:
|
|
106
169
|
// ToDo: register bytes pad
|
|
107
170
|
debug('Padding: %s bytes', payloadSize);
|
|
@@ -170,11 +233,17 @@ export class AsfParser extends BasicParser {
|
|
|
170
233
|
const { header, payloadSize } = await this.readObjectHeader(extensionSize, 'extension');
|
|
171
234
|
// Parse data part of the ASF Object
|
|
172
235
|
switch (header.objectId.str) {
|
|
173
|
-
case AsfObject.ExtendedStreamPropertiesObjectState.guid.str:
|
|
174
|
-
//
|
|
236
|
+
case AsfObject.ExtendedStreamPropertiesObjectState.guid.str: {
|
|
237
|
+
// 4.1
|
|
175
238
|
this.validateAllocationSize(payloadSize, 'Extended Stream Properties Object');
|
|
176
|
-
await this.tokenizer.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
|
|
239
|
+
const extended = await this.tokenizer.readToken(new AsfObject.ExtendedStreamPropertiesObjectState(header));
|
|
240
|
+
if (extended.streamPropertiesObject) {
|
|
241
|
+
const stream = extended.streamPropertiesObject;
|
|
242
|
+
this.streams.set(stream.streamNumber, stream);
|
|
243
|
+
this.metadata.setFormat('container', `ASF/${stream.streamType}`);
|
|
244
|
+
}
|
|
177
245
|
break;
|
|
246
|
+
}
|
|
178
247
|
case AsfObject.MetadataObjectState.guid.str: {
|
|
179
248
|
// 4.7
|
|
180
249
|
this.validateAllocationSize(payloadSize, 'Metadata Object');
|
|
@@ -5,6 +5,7 @@ import { TrackTypeValueToKeyMap } from '../type.js';
|
|
|
5
5
|
import { CombinedTagMapper } from './CombinedTagMapper.js';
|
|
6
6
|
import { CommonTagMapper } from './GenericTagMapper.js';
|
|
7
7
|
import { isSingleton, isUnique } from './GenericTagTypes.js';
|
|
8
|
+
import { normalizeBitrate } from './TrackInfo.js';
|
|
8
9
|
import { toRatio } from './Util.js';
|
|
9
10
|
const debug = initDebug('music-metadata:collector');
|
|
10
11
|
const TagPriority = [
|
|
@@ -65,6 +66,9 @@ export class MetadataCollector {
|
|
|
65
66
|
}
|
|
66
67
|
addStreamInfo(streamInfo) {
|
|
67
68
|
debug(`streamInfo: type=${streamInfo.type ? TrackTypeValueToKeyMap[streamInfo.type] : '?'}, codec=${streamInfo.codecName}`);
|
|
69
|
+
if (normalizeBitrate(streamInfo.bitrate) === undefined) {
|
|
70
|
+
delete streamInfo.bitrate;
|
|
71
|
+
}
|
|
68
72
|
this.format.trackInfo.push(streamInfo);
|
|
69
73
|
}
|
|
70
74
|
setFormat(key, value) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type IFormat, type ITrackInfo } from '../type.js';
|
|
2
|
+
/** Return a bitrate only when it conveys a finite, positive value. */
|
|
3
|
+
export declare function normalizeBitrate(bitrate: number | undefined): number | undefined;
|
|
4
|
+
/** Map the format of a single audio stream to the shared track abstraction. */
|
|
5
|
+
export declare function createAudioTrackInfo(format: IFormat): ITrackInfo;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TrackType } from '../type.js';
|
|
2
|
+
/** Return a bitrate only when it conveys a finite, positive value. */
|
|
3
|
+
export function normalizeBitrate(bitrate) {
|
|
4
|
+
return bitrate !== undefined && Number.isFinite(bitrate) && bitrate > 0 ? bitrate : undefined;
|
|
5
|
+
}
|
|
6
|
+
/** Map the format of a single audio stream to the shared track abstraction. */
|
|
7
|
+
export function createAudioTrackInfo(format) {
|
|
8
|
+
const audio = {};
|
|
9
|
+
const track = { type: TrackType.audio, audio };
|
|
10
|
+
if (format.codec !== undefined) {
|
|
11
|
+
track.codecName = format.codec;
|
|
12
|
+
}
|
|
13
|
+
if (format.codecProfile !== undefined) {
|
|
14
|
+
track.codecProfile = format.codecProfile;
|
|
15
|
+
}
|
|
16
|
+
if (format.duration !== undefined) {
|
|
17
|
+
track.duration = format.duration;
|
|
18
|
+
}
|
|
19
|
+
const bitrate = normalizeBitrate(format.bitrate);
|
|
20
|
+
if (bitrate !== undefined) {
|
|
21
|
+
track.bitrate = bitrate;
|
|
22
|
+
}
|
|
23
|
+
if (format.lossless !== undefined) {
|
|
24
|
+
track.lossless = format.lossless;
|
|
25
|
+
}
|
|
26
|
+
if (format.sampleRate !== undefined) {
|
|
27
|
+
audio.samplingFrequency = format.sampleRate;
|
|
28
|
+
}
|
|
29
|
+
if (format.numberOfChannels !== undefined) {
|
|
30
|
+
audio.channels = format.numberOfChannels;
|
|
31
|
+
}
|
|
32
|
+
if (format.bitsPerSample !== undefined) {
|
|
33
|
+
audio.bitDepth = format.bitsPerSample;
|
|
34
|
+
}
|
|
35
|
+
if (format.numberOfSamples !== undefined) {
|
|
36
|
+
audio.numberOfSamples = format.numberOfSamples;
|
|
37
|
+
}
|
|
38
|
+
return track;
|
|
39
|
+
}
|
package/lib/common/Util.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare function stripNulls(str: string): string;
|
|
|
19
19
|
* Total offset in bits = byteOffset * 8 + bitOffset
|
|
20
20
|
* @param source Byte buffer
|
|
21
21
|
* @param byteOffset Starting offset in bytes
|
|
22
|
-
* @param bitOffset Starting offset in bits: 0 =
|
|
22
|
+
* @param bitOffset Starting offset in bits: 0 = most significant bit
|
|
23
23
|
* @param len Length of number in bits
|
|
24
24
|
* @return Decoded bit aligned number
|
|
25
25
|
*/
|
package/lib/common/Util.js
CHANGED
|
@@ -81,25 +81,19 @@ export function stripNulls(str) {
|
|
|
81
81
|
* Total offset in bits = byteOffset * 8 + bitOffset
|
|
82
82
|
* @param source Byte buffer
|
|
83
83
|
* @param byteOffset Starting offset in bytes
|
|
84
|
-
* @param bitOffset Starting offset in bits: 0 =
|
|
84
|
+
* @param bitOffset Starting offset in bits: 0 = most significant bit
|
|
85
85
|
* @param len Length of number in bits
|
|
86
86
|
* @return Decoded bit aligned number
|
|
87
87
|
*/
|
|
88
88
|
export function getBitAllignedNumber(source, byteOffset, bitOffset, len) {
|
|
89
89
|
const byteOff = byteOffset + ~~(bitOffset / 8);
|
|
90
90
|
const bitOff = bitOffset % 8;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (bitsLeft < 0) {
|
|
96
|
-
value >>= 8 - bitOff - len;
|
|
91
|
+
const value = source[byteOff] & (0xff >> bitOff);
|
|
92
|
+
const bitsLeft = len + bitOff - 8;
|
|
93
|
+
if (bitsLeft <= 0) {
|
|
94
|
+
return value >> -bitsLeft;
|
|
97
95
|
}
|
|
98
|
-
|
|
99
|
-
value <<= bitsLeft;
|
|
100
|
-
value |= getBitAllignedNumber(source, byteOffset, bitOffset + bitsRead, bitsLeft);
|
|
101
|
-
}
|
|
102
|
-
return value;
|
|
96
|
+
return (value << bitsLeft) | getBitAllignedNumber(source, byteOff + 1, 0, bitsLeft);
|
|
103
97
|
}
|
|
104
98
|
/**
|
|
105
99
|
* Read bit-aligned number start from buffer
|
package/lib/core.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { IAudioMetadata, INativeTagDict, IOptions, IPicture, IPrivateOption
|
|
|
7
7
|
export type { IFileInfo } from 'strtok3';
|
|
8
8
|
export * from './ParseError.js';
|
|
9
9
|
export { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
|
|
10
|
-
export { type IAudioMetadata, type IChapter, type ICommonTagsResult, type IFormat, type ILyricsTag, type IMetadataEvent, type IMetadataEventTag, type INativeTagDict, type IOptions, type IPicture, type IRatio, type ITag, LyricsContentType, TimestampFormat } from './type.js';
|
|
10
|
+
export { type IAudioMetadata, type IAudioTrack, type IChapter, type ICommonTagsResult, type IFormat, type ILyricsTag, type IMetadataEvent, type IMetadataEventTag, type INativeTagDict, type IOptions, type IPicture, type IRatio, type ITag, type ITrackInfo, type IVideoTrack, LyricsContentType, TimestampFormat, TrackType } from './type.js';
|
|
11
11
|
/**
|
|
12
12
|
* Parse Web API File
|
|
13
13
|
* Requires Blob to be able to stream using a ReadableStreamBYOBReader, only available since Node.js ≥ 20
|
package/lib/core.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getLyricsHeaderLength } from './lyrics3/Lyrics3.js';
|
|
|
8
8
|
import { ParserFactory } from './ParserFactory.js';
|
|
9
9
|
export * from './ParseError.js';
|
|
10
10
|
export { CouldNotDetermineFileTypeError, UnsupportedFileTypeError } from './ParseError.js';
|
|
11
|
-
export { LyricsContentType, TimestampFormat } from './type.js';
|
|
11
|
+
export { LyricsContentType, TimestampFormat, TrackType } from './type.js';
|
|
12
12
|
/**
|
|
13
13
|
* Parse Web API File
|
|
14
14
|
* Requires Blob to be able to stream using a ReadableStreamBYOBReader, only available since Node.js ≥ 20
|