1
0
mirror of synced 2026-05-22 22:53:20 +00:00

Add format.StreamInfo

This commit is contained in:
Borewit
2020-01-05 20:16:43 +01:00
parent 128251a045
commit 1b33e6371e
14 changed files with 451 additions and 34 deletions
+60 -16
View File
@@ -239,26 +239,70 @@ To enforce parsing the entire file if needed you should set `duration` to `true`
### Metadata result
If the returned promise resolves, the metadata (TypeScript `IAudioMetadata` interface) contains:
* [`format: IFormat`](#format) Audio format information
* `native: INativeTags` List of native (original) tags found in the parsed audio file.
* [`common: ICommonTagsResult`](doc/common_metadata.md) Is a generic (abstract) way of reading metadata information.
* [`metadata.format`](#metadataformat) Audio format information
* [`metadata.common`](#metadatacommon) Is a generic (abstract) way of reading metadata information.
* [`metadata.trackInfo`](#metadatatrackInfo) Is a generic (abstract) way of reading metadata information.
* `metadata.native` List of native (original) tags found in the parsed audio file.
#### Format
#### `metadata.format`
The questionmark `?` indicates the property is optional.
Audio format information. Defined in the TypeScript `IFormat` interface:
* `container?: string` Audio encoding format. e.g.: 'flac'
* `codec?` Name of the codec (algorithm used for the audio compression)
* `codecProfile?: string` Codec profile / settings
* `tagTypes?: TagType[]` List of tagging formats found in parsed audio file
* `duration?: number` Duration in seconds
* `bitrate?: number` Number bits per second of encoded audio file
* `sampleRate?: number` Sampling rate in Samples per second (S/s)
* `bitsPerSample?: number` Audio bit depth
* `lossless?: boolean` True if lossless, false for lossy encoding
* `numberOfChannels?: number` Number of audio channels
* `numberOfSamples?: number` Number of samples frames, one sample contains all channels. The duration is: numberOfSamples / sampleRate
* `format.container?: string` Audio encoding format. e.g.: 'flac'
* `format.codec?` Name of the codec (algorithm used for the audio compression)
* `format.codecProfile?: string` Codec profile / settings
* `format.tagTypes?: TagType[]` List of tagging formats found in parsed audio file
* `format.duration?: number` Duration in seconds
* `format.bitrate?: number` Number bits per second of encoded audio file
* `format.sampleRate?: number` Sampling rate in Samples per second (S/s)
* `format.bitsPerSample?: number` Audio bit depth
* `format.lossless?: boolean` True if lossless, false for lossy encoding
* `format.numberOfChannels?: number` Number of audio channels
* `format.numberOfSamples?: number` Number of samples frames, one sample contains all channels. The duration is: numberOfSamples / sampleRate
#### `metadata.trackInfo`
To support advanced containers like [Matroska](https://wikipedia.org/wiki/Matroska) or [MPEG-4](https://en.wikipedia.org/wiki/MPEG-4), which may contain multiple audio and video tracks, the **experimental** `metadata.trackInfo` has been added,
`metadata.trackInfo` is either `undefined` or has an **array** of [trackInfo](#trackinfo)
#### Common
##### trackInfo
Audio format information. Defined in the TypeScript `IFormat` interface:
* `trackInfo.type?: TrackType` Track type
* `trackInfo.codecName?: string` Codec name
* `trackInfo.codecSettings?: string` Codec settings
* `trackInfo.flagEnabled?: boolean` Set if the track is usable, default: `true`
* `trackInfo.flagDefault?: boolean` Set if that track (audio, video or subs) SHOULD be active if no language found matches the user preference.
* `trackInfo.flagLacing?: boolean` Set if the track **may** contain blocks using lacing
* `trackInfo.name?: string` A human-readable track name.
* `trackInfo.language?: string` Specifies the language of the track
* `trackInfo.audio?: IAudioTrack`, see [`trackInfo.audioTrack`](#trackinfoaudiotrack)
* `trackInfo.video?: IVideoTrack`, see [`trackInfo.videoTrack`](#trackinfovideotrack)
##### `trackInfo.audioTrack`
* `audioTrack.samplingFrequency?: number`
* `audioTrack.outputSamplingFrequency?: number`
* `audioTrack.channels?: number`
* `audioTrack.channelPositions?: Buffer`
* `audioTrack.bitDepth?: number`
##### `trackInfo.videoTrack`
* `videoTrack.flagInterlaced?: boolean`
* `videoTrack.stereoMode?: number`
* `videoTrack.pixelWidth?: number`
* `videoTrack.pixelHeight?: number`
* `videoTrack.displayWidth?: number`
* `videoTrack.displayHeight?: number`
* `videoTrack.displayUnit?: number`
* `videoTrack.aspectRatioType?: number`
* `videoTrack.colourSpace?: Buffer`
* `videoTrack.gammaValue?: number`
#### `metadata.common`
[Common tag documentation](doc/common_metadata.md) is automatically generated.
+7 -1
View File
@@ -1,4 +1,4 @@
import { ITag } from '../type';
import { ITag, TrackType } from '../type';
import GUID from './GUID';
import * as AsfObject from './AsfObject';
import * as _debug from 'debug';
@@ -70,6 +70,12 @@ export class AsfParser extends BasicParser {
case GUID.CodecListObject.str:
const codecs = await AsfObject.readCodecEntries(this.tokenizer);
codecs.forEach(codec => {
this.metadata.addStreamInfo({
type: codec.type.videoCodec ? TrackType.video : TrackType.audio,
codecName: codec.codecName
});
});
const audioCodecs = codecs.filter(codec => codec.type.audioCodec).map(codec => codec.codecName).join('/');
this.metadata.setFormat('codec', audioCodecs);
break;
+10 -2
View File
@@ -2,7 +2,7 @@ import {
FormatId,
IAudioMetadata, ICommonTagsResult,
IFormat,
INativeTags, IOptions, IPicture, IQualityInformation
INativeTags, IOptions, IQualityInformation, IPicture, ITrackInfo, TrackType
} from '../type';
import * as _debug from 'debug';
@@ -48,6 +48,8 @@ export interface INativeMetadataCollector extends IWarningCollector {
setFormat(key: FormatId, value: any);
addTag(tagType: TagType, tagId: string, value: any);
addStreamInfo(streamInfo: ITrackInfo);
}
/**
@@ -57,7 +59,8 @@ export interface INativeMetadataCollector extends IWarningCollector {
export class MetadataCollector implements INativeMetadataCollector {
public readonly format: IFormat = {
tagTypes: []
tagTypes: [],
trackInfo: []
};
public readonly native: INativeTags = {};
@@ -103,6 +106,11 @@ export class MetadataCollector implements INativeMetadataCollector {
return Object.keys(this.native).length > 0;
}
public addStreamInfo(streamInfo: ITrackInfo) {
debug(`streamInfo: type=${TrackType[streamInfo.type]}, codec=${streamInfo.codecName}`);
this.format.trackInfo.push(streamInfo);
}
public setFormat(key: FormatId, value: any) {
debug(`format: ${key} = ${value}`);
(this.format as any)[key] = value; // as any to override readonly
+18 -7
View File
@@ -2,15 +2,10 @@ import * as Token from 'token-types';
import * as _debug from 'debug';
import { INativeMetadataCollector } from '../common/MetadataCollector';
import { ITokenizer } from 'strtok3/lib/core';
import { IOptions } from '../type';
import { IOptions, ITrackInfo } from '../type';
import { ITokenParser } from '../ParserFactory';
import { BasicParser } from '../common/BasicParser';
import {
DataType,
IContainerType,
IHeader, IMatroskaDoc,
ITree, TargetType, TrackType
} from './types';
import { DataType, IContainerType, IHeader, IMatroskaDoc, ITree, TargetType, TrackType } from './types';
import * as matroskaDtd from './MatroskaDtd';
const debug = _debug('music-metadata:parser:matroska');
@@ -66,6 +61,22 @@ export class MatroskaParser extends BasicParser {
const audioTracks = matroska.segment.tracks;
if (audioTracks && audioTracks.entries) {
audioTracks.entries.forEach(entry => {
const stream: ITrackInfo = {
codecName: entry.codecID.replace('A_', '').replace('V_', ''),
codecSettings: entry.codecSettings,
flagDefault: entry.flagDefault,
flagLacing: entry.flagLacing,
flagEnabled: entry.flagEnabled,
language: entry. language,
name: entry.name,
type: entry.trackType,
audio: entry.audio,
video: entry.video
};
this.metadata.addStreamInfo(stream);
});
const audioTrack = audioTracks.entries
.filter(entry => {
return entry.trackType === TrackType.audio.valueOf();
+1 -1
View File
@@ -38,7 +38,7 @@ export interface ISegmentInformation {
export interface ITrackEntry {
uid?: Buffer;
trackNumber?: number;
trackType?: number;
trackType?: TrackType;
audio?: ITrackAudio;
video?: ITrackVideo;
flagEnabled?: boolean;
+19 -1
View File
@@ -6,7 +6,7 @@ import { BasicParser } from '../common/BasicParser';
import { Atom } from './Atom';
import * as AtomToken from './AtomToken';
import { Genres } from '../id3v1/ID3v1Parser';
import { IChapter } from '../type';
import { IChapter, ITrackInfo, TrackType } from '../type';
const debug = initDebug('music-metadata:parser:MP4');
const tagFormat = 'iTunes';
@@ -163,12 +163,30 @@ export class MP4Parser extends BasicParser {
const formatList: string[] = [];
this.tracks.forEach(track => {
const trackFormats: string[] = [];
track.soundSampleDescription.forEach(ssd => {
const streamInfo: ITrackInfo = {};
const encoderInfo = encoderDict[ssd.dataFormat];
if (encoderInfo) {
trackFormats.push(encoderInfo.format);
streamInfo.codecName = encoderInfo.format;
} else {
streamInfo.codecName = `<${ssd.dataFormat}>`;
}
if (ssd.description) {
const {description} = ssd;
if (description.sampleRate > 0) {
streamInfo.type = TrackType.audio;
streamInfo.audio = {
samplingFrequency: description.sampleRate,
bitDepth: description.sampleSize,
channels: description.numAudioChannels
};
}
}
this.metadata.addStreamInfo(streamInfo);
});
if (trackFormats.length >= 1) {
formatList.push(trackFormats.join('/'));
}
+46
View File
@@ -326,8 +326,54 @@ export type FormatId =
| 'audioMD5'
| 'chapters';
export interface IAudioTrack {
samplingFrequency?: number;
outputSamplingFrequency?: number;
channels?: number;
channelPositions?: Buffer;
bitDepth?: number
}
export interface IVideoTrack {
flagInterlaced?: boolean;
stereoMode?: number;
pixelWidth?: number;
pixelHeight?: number;
displayWidth?: number;
displayHeight?: number;
displayUnit?: number;
aspectRatioType?: number;
colourSpace?: Buffer;
gammaValue?: number;
}
export enum TrackType {
video = 0x01,
audio = 0x02,
complex = 0x03,
logo = 0x04,
subtitle= 0x11,
button = 0x12,
control = 0x20
}
export interface ITrackInfo {
type?: TrackType;
codecName?: string;
codecSettings?: string;
flagEnabled?: boolean;
flagDefault?: boolean;
flagLacing?: boolean;
name?: string;
language?: string;
audio?: IAudioTrack;
video?: IVideoTrack;
}
export interface IFormat {
readonly trackInfo: ITrackInfo[]
/**
* E.g.: 'flac'
*/
Binary file not shown.
+4 -4
View File
@@ -68,7 +68,7 @@ describe("Parse ASF", () => {
describe("parse", () => {
const asfFilePath = path.join(__dirname, 'samples', 'asf.wma');
const asfFilePath = path.join(__dirname, 'samples', 'asf');
function checkFormat(format) {
assert.strictEqual(format.container, 'ASF/audio', 'format.container');
@@ -99,7 +99,7 @@ describe("Parse ASF", () => {
Parsers.forEach(parser => {
it(parser.description, async () => {
const metadata = await parser.initParser(asfFilePath, 'audio/x-ms-wma');
const metadata = await parser.initParser(path.join(asfFilePath, 'asf.wma'), 'audio/x-ms-wma');
assert.isDefined(metadata, 'metadata');
checkFormat(metadata.format);
checkCommon(metadata.common);
@@ -115,7 +115,7 @@ describe("Parse ASF", () => {
Parsers.forEach(parser => {
it(parser.description, async () => {
const filePath = path.join(__dirname, 'samples', 'issue_57.wma');
const filePath = path.join(asfFilePath, 'issue_57.wma');
const metadata = await parser.initParser(filePath, 'audio/x-ms-wma');
const asf = mm.orderTags(metadata.native.asf);
assert.exists(asf['WM/Picture'][0], 'ASF WM/Picture should be set');
@@ -131,7 +131,7 @@ describe("Parse ASF", () => {
*/
it("should be able to parse truncated .wma file", async () => {
const filePath = path.join(__dirname, 'samples', '13 Thirty Dirty Birds.wma');
const filePath = path.join(asfFilePath, '13 Thirty Dirty Birds.wma');
const {format, common, native} = await mm.parseFile(filePath);
+2 -2
View File
@@ -141,7 +141,7 @@ describe('MIME & extension mapping', () => {
it('should recognize WMA', () => {
// file-type returns 'video/x-ms-wmv'
return testFileType('asf.wma', 'ASF/audio');
return testFileType(path.join('asf', 'asf.wma'), 'ASF/audio');
});
it('should recognize MPEG-4 / m4a', () => {
@@ -173,7 +173,7 @@ describe('MIME & extension mapping', () => {
});
it('should recognize WMA', () => {
return testFileType('issue_57.wma', 'ASF/audio');
return testFileType(path.join('asf', 'issue_57.wma'), 'ASF/audio');
});
it('should recognize WavPack', () => {
+284
View File
@@ -0,0 +1,284 @@
import { assert } from 'chai';
import * as mm from '../lib';
import * as path from 'path';
import { TrackType } from '../lib/type';
const path_samples = path.join(__dirname, 'samples');
describe('format.trackInfo', () => {
describe('Containers', () => {
describe('ASF', () => {
const path_asf = path.join(path_samples, 'asf');
it('wma', async () => {
const filePath = path.join(path_asf, 'issue_57.wma');
const {format} = await mm.parseFile(filePath);
assert.includeDeepOrderedMembers(format.trackInfo, [
{
codecName: 'Windows Media Audio 9.2',
type: TrackType.audio
}
], 'format.trackInfo');
});
it('elephant.asf', async () => {
const filePath = path.join(path_asf, 'elephant.asf');
const {format} = await mm.parseFile(filePath);
assert.includeDeepOrderedMembers(format.trackInfo, [
{
codecName: 'Windows Media Audio V2',
type: TrackType.audio
},
{
codecName: 'Microsoft MPEG-4 Video Codec V3',
type: TrackType.video
}
], 'format.trackInfo');
});
});
describe('Matroska', () => {
it('WebM', async () => {
const filePath = path.join(path_samples, 'matroska', 'big-buck-bunny_trailer-short.vp8.webm');
const {format} = await mm.parseFile(filePath);
assert.includeDeepOrderedMembers(format.trackInfo, [
{
audio: undefined,
codecName: 'VP8',
codecSettings: undefined,
flagDefault: undefined,
flagEnabled: undefined,
flagLacing: undefined,
language: undefined,
name: undefined,
type: TrackType.video,
video: {
displayHeight: 360,
displayWidth: 640,
pixelHeight: 360,
pixelWidth: 640
}
},
{
audio: {
samplingFrequency: 44100
},
codecName: 'VORBIS',
codecSettings: undefined,
flagDefault: undefined,
flagEnabled: undefined,
flagLacing: undefined,
language: undefined,
name: undefined,
type: TrackType.audio,
video: undefined
}
], 'format.trackInfo');
});
it('matroska-test-w1-test5-short.mkv', async () => {
const filePath = path.join(path_samples, 'matroska', 'matroska-test-w1-test5-short.mkv');
const {format} = await mm.parseFile(filePath);
assert.includeDeepOrderedMembers(format.trackInfo, [
{
audio: undefined,
codecName: 'MPEG4/ISO/AVC',
codecSettings: undefined,
flagDefault: undefined,
flagEnabled: undefined,
flagLacing: false,
language: 'und',
name: undefined,
type: TrackType.video,
video: {
displayHeight: 576,
displayWidth: 1024,
pixelHeight: 576,
pixelWidth: 1024
}
},
{
audio: {
channels: 2,
samplingFrequency: 48000
},
codecName: 'AAC',
codecSettings: undefined,
flagDefault: undefined,
flagEnabled: undefined,
flagLacing: undefined,
language: 'und',
name: undefined,
type: TrackType.audio,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: undefined,
flagEnabled: undefined,
flagLacing: false,
language: undefined,
name: undefined,
type: TrackType.subtitle,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'hun',
name: undefined,
type: TrackType.subtitle,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'ger',
name: undefined,
type: 17,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'fre',
name: undefined,
type: TrackType.subtitle,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'spa',
name: undefined,
type: TrackType.subtitle,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
name: undefined,
language: 'ita',
type: TrackType.subtitle,
video: undefined
},
{
audio: {
outputSamplingFrequency: 44100,
samplingFrequency: 22050
},
codecName: 'AAC',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: undefined,
language: undefined,
name: 'Commentary',
type: TrackType.audio,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'jpn',
name: undefined,
type: TrackType.subtitle,
video: undefined
},
{
audio: undefined,
codecName: 'S_TEXT/UTF8',
codecSettings: undefined,
flagDefault: false,
flagEnabled: undefined,
flagLacing: false,
language: 'und',
name: undefined,
type: TrackType.subtitle,
video: undefined
}
], 'format.trackInfo');
});
});
describe('MPEG-4', () => {
it('.mp4: "Mr. Pickles S02E07 My Dear Boy.mp4"', async () => {
const filePath = path.join(path_samples, 'mp4', 'Mr. Pickles S02E07 My Dear Boy.mp4');
const {format} = await mm.parseFile(filePath);
assert.includeDeepOrderedMembers(format.trackInfo, [
{
audio: {
bitDepth: 16,
channels: 2,
samplingFrequency: 48000
},
codecName: 'MPEG-4/AAC',
type: TrackType.audio
},
{
audio: {
bitDepth: 0,
channels: 0,
samplingFrequency: 1916.1076
},
codecName: '<avc1>',
type: TrackType.audio
},
{
audio: {
bitDepth: 16,
channels: 2,
samplingFrequency: 48000
},
codecName: 'AC-3',
type: TrackType.audio
},
{
codecName: 'CEA-608'
}
], 'format.trackInfo');
});
});
});
});