Initial working version
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
import { ITokenizer } from 'strtok3/lib/core';
|
||||
import * as AtomToken from './AtomToken';
|
||||
export declare type AtomDataHandler = (atom: Atom, remaining: number) => Promise<void>;
|
||||
export declare class Atom {
|
||||
readonly header: AtomToken.IAtomHeader;
|
||||
extended: boolean;
|
||||
readonly parent: Atom;
|
||||
static readAtom(tokenizer: ITokenizer, dataHandler: AtomDataHandler, parent: Atom, remaining: number): Promise<Atom>;
|
||||
readonly children: Atom[];
|
||||
readonly atomPath: string;
|
||||
constructor(header: AtomToken.IAtomHeader, extended: boolean, parent: Atom);
|
||||
getHeaderLength(): number;
|
||||
getPayloadLength(remaining: number): number;
|
||||
readAtoms(tokenizer: ITokenizer, dataHandler: AtomDataHandler, size: number): Promise<void>;
|
||||
private readData;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Atom = void 0;
|
||||
const initDebug = require("debug");
|
||||
const AtomToken = require("./AtomToken");
|
||||
const debug = initDebug('music-metadata:parser:MP4:Atom');
|
||||
class Atom {
|
||||
constructor(header, extended, parent) {
|
||||
this.header = header;
|
||||
this.extended = extended;
|
||||
this.parent = parent;
|
||||
this.children = [];
|
||||
this.atomPath = (this.parent ? this.parent.atomPath + '.' : '') + this.header.name;
|
||||
}
|
||||
static async readAtom(tokenizer, dataHandler, parent, remaining) {
|
||||
// Parse atom header
|
||||
const offset = tokenizer.position;
|
||||
// debug(`Reading next token on offset=${offset}...`); // buf.toString('ascii')
|
||||
const header = await tokenizer.readToken(AtomToken.Header);
|
||||
const extended = header.length === BigInt(1);
|
||||
if (extended) {
|
||||
header.length = await tokenizer.readToken(AtomToken.ExtendedSize);
|
||||
}
|
||||
const atomBean = new Atom(header, header.length === BigInt(1), parent);
|
||||
const payloadLength = atomBean.getPayloadLength(remaining);
|
||||
debug(`parse atom name=${atomBean.atomPath}, extended=${atomBean.extended}, offset=${offset}, len=${atomBean.header.length}`); // buf.toString('ascii')
|
||||
await atomBean.readData(tokenizer, dataHandler, payloadLength);
|
||||
return atomBean;
|
||||
}
|
||||
getHeaderLength() {
|
||||
return this.extended ? 16 : 8;
|
||||
}
|
||||
getPayloadLength(remaining) {
|
||||
return (this.header.length === BigInt(0) ? remaining : Number(this.header.length)) - this.getHeaderLength();
|
||||
}
|
||||
async readAtoms(tokenizer, dataHandler, size) {
|
||||
while (size > 0) {
|
||||
const atomBean = await Atom.readAtom(tokenizer, dataHandler, this, size);
|
||||
this.children.push(atomBean);
|
||||
size -= atomBean.header.length === BigInt(0) ? size : Number(atomBean.header.length);
|
||||
}
|
||||
}
|
||||
async readData(tokenizer, dataHandler, remaining) {
|
||||
switch (this.header.name) {
|
||||
// "Container" atoms, contains nested atoms
|
||||
case 'moov': // The Movie Atom: contains other atoms
|
||||
case 'udta': // User defined atom
|
||||
case 'trak':
|
||||
case 'mdia': // Media atom
|
||||
case 'minf': // Media Information Atom
|
||||
case 'stbl': // The Sample Table Atom
|
||||
case '<id>':
|
||||
case 'ilst':
|
||||
case 'tref':
|
||||
return this.readAtoms(tokenizer, dataHandler, this.getPayloadLength(remaining));
|
||||
case 'meta': // Metadata Atom, ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
||||
// meta has 4 bytes of padding, ignore
|
||||
await tokenizer.ignore(4);
|
||||
return this.readAtoms(tokenizer, dataHandler, this.getPayloadLength(remaining) - 4);
|
||||
case 'mdhd': // Media header atom
|
||||
case 'mvhd': // 'movie' => 'mvhd': movie header atom; child of Movie Atom
|
||||
case 'tkhd':
|
||||
case 'stsz':
|
||||
case 'mdat':
|
||||
default:
|
||||
return dataHandler(this, remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Atom = Atom;
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
/// <reference types="node" />
|
||||
import { IToken, IGetToken } from 'strtok3/lib/core';
|
||||
interface IVersionAndFlags {
|
||||
/**
|
||||
* A 1-byte specification of the version
|
||||
*/
|
||||
version: number;
|
||||
/**
|
||||
* Three bytes of space for (future) flags.
|
||||
*/
|
||||
flags: number;
|
||||
}
|
||||
export interface IAtomHeader {
|
||||
length: bigint;
|
||||
name: string;
|
||||
}
|
||||
export interface IAtomFtyp {
|
||||
type: string;
|
||||
}
|
||||
/**
|
||||
* Common interface for the mvhd (Movie Header) & mdhd (Media) atom
|
||||
*/
|
||||
export interface IAtomMxhd extends IVersionAndFlags {
|
||||
/**
|
||||
* A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the media atom was created.
|
||||
* It is strongly recommended that this value should be specified using coordinated universal time (UTC).
|
||||
*/
|
||||
creationTime: Date;
|
||||
/**
|
||||
* A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the media atom was changed.
|
||||
* It is strongly recommended that this value should be specified using coordinated universal time (UTC).
|
||||
*/
|
||||
modificationTime: Date;
|
||||
/**
|
||||
* A time value that indicates the time scale for this media—that is, the number of time units that pass per second in its time coordinate system.
|
||||
*/
|
||||
timeScale: number;
|
||||
/**
|
||||
* Duration: the duration of this media in units of its time scale.
|
||||
*/
|
||||
duration: number;
|
||||
}
|
||||
/**
|
||||
* Interface for the parsed Movie Header Atom (mvhd)
|
||||
*/
|
||||
export interface IAtomMvhd extends IAtomMxhd {
|
||||
/**
|
||||
* Preferred rate: a 32-bit fixed-point number that specifies the rate at which to play this movie.
|
||||
* A value of 1.0 indicates normal rate.
|
||||
*/
|
||||
preferredRate: number;
|
||||
/**
|
||||
* Preferred volume: A 16-bit fixed-point number that specifies how loud to play this movie’s sound.
|
||||
* A value of 1.0 indicates full volume.
|
||||
*/
|
||||
preferredVolume: number;
|
||||
/**
|
||||
* Reserved: Ten bytes reserved for use by Apple. Set to 0.
|
||||
*/
|
||||
/**
|
||||
* Matrix structure: The matrix structure associated with this movie.
|
||||
* A matrix shows how to map points from one coordinate space into another.
|
||||
* See Matrices for a discussion of how display matrices are used in QuickTime.
|
||||
*/
|
||||
/**
|
||||
* Preview time: The time value in the movie at which the preview begins.
|
||||
*/
|
||||
previewTime: number;
|
||||
/**
|
||||
* Preview duration: The duration of the movie preview in movie time scale units.
|
||||
*/
|
||||
previewDuration: number;
|
||||
/**
|
||||
* Poster time: The time value of the time of the movie poster.
|
||||
*/
|
||||
posterTime: number;
|
||||
/**
|
||||
* selection time: The time value for the start time of the current selection.
|
||||
*/
|
||||
selectionTime: number;
|
||||
/**
|
||||
* Selection duration: The duration of the current selection in movie time scale units.
|
||||
*/
|
||||
selectionDuration: number;
|
||||
/**
|
||||
* Current time: The time value for current time position within the movie.
|
||||
*/
|
||||
currentTime: number;
|
||||
/**
|
||||
* Next track ID: A 32-bit integer that indicates a value to use for the track ID number of the next track added to this movie. Note that 0 is not a valid track ID value.
|
||||
*/
|
||||
nextTrackID: number;
|
||||
}
|
||||
/**
|
||||
* Interface for the metadata header atom: 'mhdr'
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW13
|
||||
*/
|
||||
export interface IMovieHeaderAtom extends IVersionAndFlags {
|
||||
/**
|
||||
* A 32-bit unsigned integer indicating the value to use for the item ID of the next item created or assigned an item ID.
|
||||
* If the value is all ones, it indicates that future additions will require a search for an unused item ID.
|
||||
*/
|
||||
nextItemID: number;
|
||||
}
|
||||
export declare const Header: IToken<IAtomHeader>;
|
||||
/**
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html#//apple_ref/doc/uid/TP40000939-CH203-38190
|
||||
*/
|
||||
export declare const ExtendedSize: IToken<bigint>;
|
||||
export declare const ftyp: IGetToken<IAtomFtyp>;
|
||||
export declare const tkhd: IGetToken<IAtomFtyp>;
|
||||
/**
|
||||
* Token: Movie Header Atom
|
||||
*/
|
||||
export declare const mhdr: IGetToken<IMovieHeaderAtom>;
|
||||
/**
|
||||
* Base class for 'fixed' length atoms.
|
||||
* In some cases these atoms are longer then the sum of the described fields.
|
||||
* Issue: https://github.com/Borewit/music-metadata/issues/120
|
||||
*/
|
||||
export declare abstract class FixedLengthAtom {
|
||||
len: number;
|
||||
/**
|
||||
*
|
||||
* @param {number} len Length as specified in the size field
|
||||
* @param {number} expLen Total length of sum of specified fields in the standard
|
||||
*/
|
||||
protected constructor(len: number, expLen: number, atomId: string);
|
||||
}
|
||||
/**
|
||||
* Interface for the parsed Movie Header Atom (mdhd)
|
||||
*/
|
||||
export interface IAtomMdhd extends IAtomMxhd {
|
||||
/**
|
||||
* A 16-bit integer that specifies the language code for this media.
|
||||
* See Language Code Values for valid language codes.
|
||||
* Also see Extended Language Tag Atom for the preferred code to use here if an extended language tag is also included in the media atom.
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap4/qtff4.html#//apple_ref/doc/uid/TP40000939-CH206-34353
|
||||
*/
|
||||
language: number;
|
||||
quality: number;
|
||||
}
|
||||
/**
|
||||
* Token: Media Header Atom
|
||||
* Ref:
|
||||
* https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-SW34
|
||||
* https://wiki.multimedia.cx/index.php/QuickTime_container#mdhd
|
||||
*/
|
||||
export declare class MdhdAtom extends FixedLengthAtom implements IGetToken<IAtomMdhd> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): IAtomMdhd;
|
||||
}
|
||||
/**
|
||||
* Token: Movie Header Atom
|
||||
*/
|
||||
export declare class MvhdAtom extends FixedLengthAtom implements IGetToken<IAtomMvhd> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): IAtomMvhd;
|
||||
}
|
||||
/**
|
||||
* Data Atom Structure ('data')
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW32
|
||||
*/
|
||||
export interface IDataAtom {
|
||||
/**
|
||||
* Type Indicator
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW28
|
||||
*/
|
||||
type: {
|
||||
/**
|
||||
* The set of types from which the type is drawn
|
||||
* If 0, type is drawn from the well-known set of types.
|
||||
*/
|
||||
set: number;
|
||||
type: number;
|
||||
};
|
||||
/**
|
||||
* Locale Indicator
|
||||
*/
|
||||
locale: number;
|
||||
/**
|
||||
* An array of bytes containing the value of the metadata.
|
||||
*/
|
||||
value: Buffer;
|
||||
}
|
||||
/**
|
||||
* Data Atom Structure
|
||||
*/
|
||||
export declare class DataAtom implements IGetToken<IDataAtom> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Uint8Array, off: number): IDataAtom;
|
||||
}
|
||||
/**
|
||||
* Data Atom Structure ('data')
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW32
|
||||
*/
|
||||
export interface INameAtom extends IVersionAndFlags {
|
||||
/**
|
||||
* An array of bytes containing the value of the metadata.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
/**
|
||||
* Data Atom Structure
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW31
|
||||
*/
|
||||
export declare class NameAtom implements IGetToken<INameAtom> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): INameAtom;
|
||||
}
|
||||
/**
|
||||
* Track Header Atoms interface
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
|
||||
*/
|
||||
export interface ITrackHeaderAtom extends IVersionAndFlags {
|
||||
/**
|
||||
* Creation Time
|
||||
*/
|
||||
creationTime: Date;
|
||||
/**
|
||||
* Modification Time
|
||||
*/
|
||||
modificationTime: Date;
|
||||
/**
|
||||
* TrackID
|
||||
*/
|
||||
trackId: number;
|
||||
/**
|
||||
* A time value that indicates the duration of this track (in the movie’s time coordinate system).
|
||||
* Note that this property is derived from the track’s edits. The value of this field is equal to the sum of the
|
||||
* durations of all of the track’s edits. If there is no edit list, then the duration is the sum of the sample
|
||||
* durations, converted into the movie timescale.
|
||||
*/
|
||||
duration: number;
|
||||
/**
|
||||
* A 16-bit integer that indicates this track’s spatial priority in its movie.
|
||||
* The QuickTime Movie Toolbox uses this value to determine how tracks overlay one another.
|
||||
* Tracks with lower layer values are displayed in front of tracks with higher layer values.
|
||||
*/
|
||||
layer: number;
|
||||
/**
|
||||
* A 16-bit integer that identifies a collection of movie tracks that contain alternate data for one another.
|
||||
* This same identifier appears in each 'tkhd' atom of the other tracks in the group.
|
||||
* QuickTime chooses one track from the group to be used when the movie is played.
|
||||
* The choice may be based on such considerations as playback quality, language, or the capabilities of the computer.
|
||||
* A value of zero indicates that the track is not in an alternate track group.
|
||||
*/
|
||||
alternateGroup: number;
|
||||
/**
|
||||
* A 16-bit fixed-point value that indicates how loudly this track’s sound is to be played.
|
||||
* A value of 1.0 indicates normal volume.
|
||||
*/
|
||||
volume: number;
|
||||
}
|
||||
/**
|
||||
* Track Header Atoms structure
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
|
||||
*/
|
||||
export declare class TrackHeaderAtom implements IGetToken<ITrackHeaderAtom> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): ITrackHeaderAtom;
|
||||
}
|
||||
/**
|
||||
* Atom: Sample Description Atom ('stsd')
|
||||
*/
|
||||
interface IAtomStsdHeader extends IVersionAndFlags {
|
||||
numberOfEntries: number;
|
||||
}
|
||||
/**
|
||||
* Atom: Sample Description Atom ('stsd')
|
||||
*/
|
||||
export interface ISampleDescription {
|
||||
dataFormat: string;
|
||||
dataReferenceIndex: number;
|
||||
description: Uint8Array;
|
||||
}
|
||||
export interface IAtomStsd {
|
||||
header: IAtomStsdHeader;
|
||||
table: ISampleDescription[];
|
||||
}
|
||||
/**
|
||||
* Atom: Sample-description Atom ('stsd')
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
|
||||
*/
|
||||
export declare class StsdAtom implements IGetToken<IAtomStsd> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): IAtomStsd;
|
||||
}
|
||||
export interface ISoundSampleDescriptionVersion {
|
||||
version: number;
|
||||
revision: number;
|
||||
vendor: number;
|
||||
}
|
||||
/**
|
||||
* Common Sound Sample Description (version & revision)
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-57317
|
||||
*/
|
||||
export declare const SoundSampleDescriptionVersion: IGetToken<ISoundSampleDescriptionVersion>;
|
||||
export interface ISoundSampleDescriptionV0 {
|
||||
numAudioChannels: number;
|
||||
/**
|
||||
* number of bits in each uncompressed sound sample
|
||||
*/
|
||||
sampleSize: number;
|
||||
/**
|
||||
* Compression ID
|
||||
*/
|
||||
compressionId: number;
|
||||
packetSize: number;
|
||||
sampleRate: number;
|
||||
}
|
||||
/**
|
||||
* Sound Sample Description (Version 0)
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-130736
|
||||
*/
|
||||
export declare const SoundSampleDescriptionV0: IGetToken<ISoundSampleDescriptionV0>;
|
||||
export interface ITableAtom<T> extends IVersionAndFlags {
|
||||
numberOfEntries: number;
|
||||
entries: T[];
|
||||
}
|
||||
declare class SimpleTableAtom<T> implements IGetToken<ITableAtom<T>> {
|
||||
len: number;
|
||||
private token;
|
||||
constructor(len: number, token: IGetToken<T>);
|
||||
get(buf: Buffer, off: number): ITableAtom<T>;
|
||||
}
|
||||
export interface ITimeToSampleToken {
|
||||
count: number;
|
||||
duration: number;
|
||||
}
|
||||
export declare const TimeToSampleToken: IGetToken<ITimeToSampleToken>;
|
||||
/**
|
||||
* Time-to-sample('stts') atom.
|
||||
* Store duration information for a media’s samples.
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25696
|
||||
*/
|
||||
export declare class SttsAtom extends SimpleTableAtom<ITimeToSampleToken> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
}
|
||||
/**
|
||||
* Sample-to-Chunk ('stsc') atom table entry interface
|
||||
*/
|
||||
export interface ISampleToChunk {
|
||||
firstChunk: number;
|
||||
samplesPerChunk: number;
|
||||
sampleDescriptionId: number;
|
||||
}
|
||||
export declare const SampleToChunkToken: IGetToken<ISampleToChunk>;
|
||||
/**
|
||||
* Sample-to-Chunk ('stsc') atom interface
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25706
|
||||
*/
|
||||
export declare class StscAtom extends SimpleTableAtom<ISampleToChunk> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
}
|
||||
/**
|
||||
* Sample-size ('stsz') atom interface
|
||||
*/
|
||||
export interface IStszAtom extends ITableAtom<number> {
|
||||
sampleSize: number;
|
||||
}
|
||||
/**
|
||||
* Sample-size ('stsz') atom
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25710
|
||||
*/
|
||||
export declare class StszAtom implements IGetToken<IStszAtom> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): IStszAtom;
|
||||
}
|
||||
/**
|
||||
* Chunk offset atom, 'stco'
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25715
|
||||
*/
|
||||
export declare class StcoAtom extends SimpleTableAtom<number> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
}
|
||||
/**
|
||||
* Token used to decode text-track from 'mdat' atom (raw data stream)
|
||||
*/
|
||||
export declare class ChapterText implements IGetToken<string> {
|
||||
len: number;
|
||||
constructor(len: number);
|
||||
get(buf: Buffer, off: number): string;
|
||||
}
|
||||
export {};
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ChapterText = exports.StcoAtom = exports.StszAtom = exports.StscAtom = exports.SampleToChunkToken = exports.SttsAtom = exports.TimeToSampleToken = exports.SoundSampleDescriptionV0 = exports.SoundSampleDescriptionVersion = exports.StsdAtom = exports.TrackHeaderAtom = exports.NameAtom = exports.DataAtom = exports.MvhdAtom = exports.MdhdAtom = exports.FixedLengthAtom = exports.mhdr = exports.tkhd = exports.ftyp = exports.ExtendedSize = exports.Header = void 0;
|
||||
const Token = require("token-types");
|
||||
const FourCC_1 = require("../common/FourCC");
|
||||
const initDebug = require("debug");
|
||||
const debug = initDebug('music-metadata:parser:MP4:atom');
|
||||
exports.Header = {
|
||||
len: 8,
|
||||
get: (buf, off) => {
|
||||
const length = Token.UINT32_BE.get(buf, off);
|
||||
if (length < 0)
|
||||
throw new Error('Invalid atom header length');
|
||||
return {
|
||||
length: BigInt(length),
|
||||
name: new Token.StringType(4, 'binary').get(buf, off + 4)
|
||||
};
|
||||
},
|
||||
put: (buf, off, hdr) => {
|
||||
Token.UINT32_BE.put(buf, off, Number(hdr.length));
|
||||
return FourCC_1.FourCcToken.put(buf, off + 4, hdr.name);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap1/qtff1.html#//apple_ref/doc/uid/TP40000939-CH203-38190
|
||||
*/
|
||||
exports.ExtendedSize = Token.UINT64_BE;
|
||||
exports.ftyp = {
|
||||
len: 4,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
type: new Token.StringType(4, 'ascii').get(buf, off)
|
||||
};
|
||||
}
|
||||
};
|
||||
exports.tkhd = {
|
||||
len: 4,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
type: new Token.StringType(4, 'ascii').get(buf, off)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Token: Movie Header Atom
|
||||
*/
|
||||
exports.mhdr = {
|
||||
len: 8,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
nextItemID: Token.UINT32_BE.get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Base class for 'fixed' length atoms.
|
||||
* In some cases these atoms are longer then the sum of the described fields.
|
||||
* Issue: https://github.com/Borewit/music-metadata/issues/120
|
||||
*/
|
||||
class FixedLengthAtom {
|
||||
/**
|
||||
*
|
||||
* @param {number} len Length as specified in the size field
|
||||
* @param {number} expLen Total length of sum of specified fields in the standard
|
||||
*/
|
||||
constructor(len, expLen, atomId) {
|
||||
this.len = len;
|
||||
if (len < expLen) {
|
||||
throw new Error(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`);
|
||||
}
|
||||
else if (len > expLen) {
|
||||
debug(`Warning: atom ${atomId} expected to be ${expLen}, but was actually ${len} bytes long.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.FixedLengthAtom = FixedLengthAtom;
|
||||
/**
|
||||
* Timestamp stored in seconds since Mac Epoch (1 January 1904)
|
||||
*/
|
||||
const SecondsSinceMacEpoch = {
|
||||
len: 4,
|
||||
get: (buf, off) => {
|
||||
const secondsSinceUnixEpoch = Token.UINT32_BE.get(buf, off) - 2082844800;
|
||||
return new Date(secondsSinceUnixEpoch * 1000);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Token: Media Header Atom
|
||||
* Ref:
|
||||
* https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-SW34
|
||||
* https://wiki.multimedia.cx/index.php/QuickTime_container#mdhd
|
||||
*/
|
||||
class MdhdAtom extends FixedLengthAtom {
|
||||
constructor(len) {
|
||||
super(len, 24, 'mdhd');
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off + 0),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
|
||||
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
|
||||
timeScale: Token.UINT32_BE.get(buf, off + 12),
|
||||
duration: Token.UINT32_BE.get(buf, off + 16),
|
||||
language: Token.UINT16_BE.get(buf, off + 20),
|
||||
quality: Token.UINT16_BE.get(buf, off + 22)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.MdhdAtom = MdhdAtom;
|
||||
/**
|
||||
* Token: Movie Header Atom
|
||||
*/
|
||||
class MvhdAtom extends FixedLengthAtom {
|
||||
constructor(len) {
|
||||
super(len, 100, 'mvhd');
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
|
||||
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
|
||||
timeScale: Token.UINT32_BE.get(buf, off + 12),
|
||||
duration: Token.UINT32_BE.get(buf, off + 16),
|
||||
preferredRate: Token.UINT32_BE.get(buf, off + 20),
|
||||
preferredVolume: Token.UINT16_BE.get(buf, off + 24),
|
||||
// ignore reserver: 10 bytes
|
||||
// ignore matrix structure: 36 bytes
|
||||
previewTime: Token.UINT32_BE.get(buf, off + 72),
|
||||
previewDuration: Token.UINT32_BE.get(buf, off + 76),
|
||||
posterTime: Token.UINT32_BE.get(buf, off + 80),
|
||||
selectionTime: Token.UINT32_BE.get(buf, off + 84),
|
||||
selectionDuration: Token.UINT32_BE.get(buf, off + 88),
|
||||
currentTime: Token.UINT32_BE.get(buf, off + 92),
|
||||
nextTrackID: Token.UINT32_BE.get(buf, off + 96)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.MvhdAtom = MvhdAtom;
|
||||
/**
|
||||
* Data Atom Structure
|
||||
*/
|
||||
class DataAtom {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
type: {
|
||||
set: Token.UINT8.get(buf, off + 0),
|
||||
type: Token.UINT24_BE.get(buf, off + 1)
|
||||
},
|
||||
locale: Token.UINT24_BE.get(buf, off + 4),
|
||||
value: Buffer.from(new Token.Uint8ArrayType(this.len - 8).get(buf, off + 8))
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.DataAtom = DataAtom;
|
||||
/**
|
||||
* Data Atom Structure
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW31
|
||||
*/
|
||||
class NameAtom {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
name: new Token.StringType(this.len - 4, 'utf-8').get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.NameAtom = NameAtom;
|
||||
/**
|
||||
* Track Header Atoms structure
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
|
||||
*/
|
||||
class TrackHeaderAtom {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
creationTime: SecondsSinceMacEpoch.get(buf, off + 4),
|
||||
modificationTime: SecondsSinceMacEpoch.get(buf, off + 8),
|
||||
trackId: Token.UINT32_BE.get(buf, off + 12),
|
||||
// reserved 4 bytes
|
||||
duration: Token.UINT32_BE.get(buf, off + 20),
|
||||
layer: Token.UINT16_BE.get(buf, off + 24),
|
||||
alternateGroup: Token.UINT16_BE.get(buf, off + 26),
|
||||
volume: Token.UINT16_BE.get(buf, off + 28) // ToDo: fixed point
|
||||
// ToDo: add remaining fields
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.TrackHeaderAtom = TrackHeaderAtom;
|
||||
/**
|
||||
* Atom: Sample Description Atom ('stsd')
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
|
||||
*/
|
||||
const stsdHeader = {
|
||||
len: 8,
|
||||
get: (buf, off) => {
|
||||
return {
|
||||
version: Token.UINT8.get(buf, off),
|
||||
flags: Token.UINT24_BE.get(buf, off + 1),
|
||||
numberOfEntries: Token.UINT32_BE.get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Atom: Sample Description Atom ('stsd')
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
|
||||
*/
|
||||
class SampleDescriptionTable {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
return {
|
||||
dataFormat: FourCC_1.FourCcToken.get(buf, off),
|
||||
dataReferenceIndex: Token.UINT16_BE.get(buf, off + 10),
|
||||
description: new Token.Uint8ArrayType(this.len - 12).get(buf, off + 12)
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Atom: Sample-description Atom ('stsd')
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25691
|
||||
*/
|
||||
class StsdAtom {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
const header = stsdHeader.get(buf, off);
|
||||
off += stsdHeader.len;
|
||||
const table = [];
|
||||
for (let n = 0; n < header.numberOfEntries; ++n) {
|
||||
const size = Token.UINT32_BE.get(buf, off); // Sample description size
|
||||
off += Token.UINT32_BE.len;
|
||||
table.push(new SampleDescriptionTable(size).get(buf, off));
|
||||
off += size;
|
||||
}
|
||||
return {
|
||||
header,
|
||||
table
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.StsdAtom = StsdAtom;
|
||||
/**
|
||||
* Common Sound Sample Description (version & revision)
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-57317
|
||||
*/
|
||||
exports.SoundSampleDescriptionVersion = {
|
||||
len: 8,
|
||||
get(buf, off) {
|
||||
return {
|
||||
version: Token.INT16_BE.get(buf, off),
|
||||
revision: Token.INT16_BE.get(buf, off + 2),
|
||||
vendor: Token.INT32_BE.get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Sound Sample Description (Version 0)
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-130736
|
||||
*/
|
||||
exports.SoundSampleDescriptionV0 = {
|
||||
len: 12,
|
||||
get(buf, off) {
|
||||
return {
|
||||
numAudioChannels: Token.INT16_BE.get(buf, off + 0),
|
||||
sampleSize: Token.INT16_BE.get(buf, off + 2),
|
||||
compressionId: Token.INT16_BE.get(buf, off + 4),
|
||||
packetSize: Token.INT16_BE.get(buf, off + 6),
|
||||
sampleRate: Token.UINT16_BE.get(buf, off + 8) + Token.UINT16_BE.get(buf, off + 10) / 10000
|
||||
};
|
||||
}
|
||||
};
|
||||
class SimpleTableAtom {
|
||||
constructor(len, token) {
|
||||
this.len = len;
|
||||
this.token = token;
|
||||
}
|
||||
get(buf, off) {
|
||||
const nrOfEntries = Token.INT32_BE.get(buf, off + 4);
|
||||
return {
|
||||
version: Token.INT8.get(buf, off + 0),
|
||||
flags: Token.INT24_BE.get(buf, off + 1),
|
||||
numberOfEntries: nrOfEntries,
|
||||
entries: readTokenTable(buf, this.token, off + 8, this.len - 8, nrOfEntries)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.TimeToSampleToken = {
|
||||
len: 8,
|
||||
get(buf, off) {
|
||||
return {
|
||||
count: Token.INT32_BE.get(buf, off + 0),
|
||||
duration: Token.INT32_BE.get(buf, off + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Time-to-sample('stts') atom.
|
||||
* Store duration information for a media’s samples.
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25696
|
||||
*/
|
||||
class SttsAtom extends SimpleTableAtom {
|
||||
constructor(len) {
|
||||
super(len, exports.TimeToSampleToken);
|
||||
this.len = len;
|
||||
}
|
||||
}
|
||||
exports.SttsAtom = SttsAtom;
|
||||
exports.SampleToChunkToken = {
|
||||
len: 12,
|
||||
get(buf, off) {
|
||||
return {
|
||||
firstChunk: Token.INT32_BE.get(buf, off),
|
||||
samplesPerChunk: Token.INT32_BE.get(buf, off + 4),
|
||||
sampleDescriptionId: Token.INT32_BE.get(buf, off + 8)
|
||||
};
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Sample-to-Chunk ('stsc') atom interface
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25706
|
||||
*/
|
||||
class StscAtom extends SimpleTableAtom {
|
||||
constructor(len) {
|
||||
super(len, exports.SampleToChunkToken);
|
||||
this.len = len;
|
||||
}
|
||||
}
|
||||
exports.StscAtom = StscAtom;
|
||||
/**
|
||||
* Sample-size ('stsz') atom
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25710
|
||||
*/
|
||||
class StszAtom {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
const nrOfEntries = Token.INT32_BE.get(buf, off + 8);
|
||||
return {
|
||||
version: Token.INT8.get(buf, off),
|
||||
flags: Token.INT24_BE.get(buf, off + 1),
|
||||
sampleSize: Token.INT32_BE.get(buf, off + 4),
|
||||
numberOfEntries: nrOfEntries,
|
||||
entries: readTokenTable(buf, Token.INT32_BE, off + 12, this.len - 12, nrOfEntries)
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.StszAtom = StszAtom;
|
||||
/**
|
||||
* Chunk offset atom, 'stco'
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25715
|
||||
*/
|
||||
class StcoAtom extends SimpleTableAtom {
|
||||
constructor(len) {
|
||||
super(len, Token.INT32_BE);
|
||||
this.len = len;
|
||||
}
|
||||
}
|
||||
exports.StcoAtom = StcoAtom;
|
||||
/**
|
||||
* Token used to decode text-track from 'mdat' atom (raw data stream)
|
||||
*/
|
||||
class ChapterText {
|
||||
constructor(len) {
|
||||
this.len = len;
|
||||
}
|
||||
get(buf, off) {
|
||||
const titleLen = Token.INT16_BE.get(buf, off + 0);
|
||||
const str = new Token.StringType(titleLen, 'utf-8');
|
||||
return str.get(buf, off + 2);
|
||||
}
|
||||
}
|
||||
exports.ChapterText = ChapterText;
|
||||
function readTokenTable(buf, token, off, remainingLen, numberOfEntries) {
|
||||
debug(`remainingLen=${remainingLen}, numberOfEntries=${numberOfEntries} * token-len=${token.len}`);
|
||||
if (remainingLen === 0)
|
||||
return [];
|
||||
if (remainingLen !== numberOfEntries * token.len)
|
||||
throw new Error('mismatch number-of-entries with remaining atom-length');
|
||||
const entries = [];
|
||||
// parse offset-table
|
||||
for (let n = 0; n < numberOfEntries; ++n) {
|
||||
entries.push(token.get(buf, off));
|
||||
off += token.len;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { BasicParser } from '../common/BasicParser';
|
||||
import { Atom } from './Atom';
|
||||
export declare class MP4Parser extends BasicParser {
|
||||
private static read_BE_Integer;
|
||||
private audioLengthInBytes;
|
||||
private tracks;
|
||||
parse(): Promise<void>;
|
||||
handleAtom(atom: Atom, remaining: number): Promise<void>;
|
||||
private getTrackDescription;
|
||||
private calculateBitRate;
|
||||
private addTag;
|
||||
private addWarning;
|
||||
/**
|
||||
* Parse data of Meta-item-list-atom (item of 'ilst' atom)
|
||||
* @param metaAtom
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
||||
*/
|
||||
private parseMetadataItemData;
|
||||
private parseValueAtom;
|
||||
private atomParsers;
|
||||
/**
|
||||
* @param sampleDescription
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
||||
*/
|
||||
private parseSoundSampleDescription;
|
||||
private parseChapterTrack;
|
||||
private findSampleOffset;
|
||||
private getChunkDuration;
|
||||
private getSamplesPerChunk;
|
||||
}
|
||||
+511
@@ -0,0 +1,511 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MP4Parser = void 0;
|
||||
const initDebug = require("debug");
|
||||
const Token = require("token-types");
|
||||
const BasicParser_1 = require("../common/BasicParser");
|
||||
const Atom_1 = require("./Atom");
|
||||
const AtomToken = require("./AtomToken");
|
||||
const ID3v1Parser_1 = require("../id3v1/ID3v1Parser");
|
||||
const type_1 = require("../type");
|
||||
const debug = initDebug('music-metadata:parser:MP4');
|
||||
const tagFormat = 'iTunes';
|
||||
const encoderDict = {
|
||||
raw: {
|
||||
lossy: false,
|
||||
format: 'raw'
|
||||
},
|
||||
MAC3: {
|
||||
lossy: true,
|
||||
format: 'MACE 3:1'
|
||||
},
|
||||
MAC6: {
|
||||
lossy: true,
|
||||
format: 'MACE 6:1'
|
||||
},
|
||||
ima4: {
|
||||
lossy: true,
|
||||
format: 'IMA 4:1'
|
||||
},
|
||||
ulaw: {
|
||||
lossy: true,
|
||||
format: 'uLaw 2:1'
|
||||
},
|
||||
alaw: {
|
||||
lossy: true,
|
||||
format: 'uLaw 2:1'
|
||||
},
|
||||
Qclp: {
|
||||
lossy: true,
|
||||
format: 'QUALCOMM PureVoice'
|
||||
},
|
||||
'.mp3': {
|
||||
lossy: true,
|
||||
format: 'MPEG-1 layer 3'
|
||||
},
|
||||
alac: {
|
||||
lossy: false,
|
||||
format: 'ALAC'
|
||||
},
|
||||
'ac-3': {
|
||||
lossy: true,
|
||||
format: 'AC-3'
|
||||
},
|
||||
mp4a: {
|
||||
lossy: true,
|
||||
format: 'MPEG-4/AAC'
|
||||
},
|
||||
mp4s: {
|
||||
lossy: true,
|
||||
format: 'MP4S'
|
||||
},
|
||||
// Closed Captioning Media, https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-SW87
|
||||
c608: {
|
||||
lossy: true,
|
||||
format: 'CEA-608'
|
||||
},
|
||||
c708: {
|
||||
lossy: true,
|
||||
format: 'CEA-708'
|
||||
}
|
||||
};
|
||||
function distinct(value, index, self) {
|
||||
return self.indexOf(value) === index;
|
||||
}
|
||||
/*
|
||||
* Parser for the MP4 (MPEG-4 Part 14) container format
|
||||
* Standard: ISO/IEC 14496-14
|
||||
* supporting:
|
||||
* - QuickTime container
|
||||
* - MP4 File Format
|
||||
* - 3GPP file format
|
||||
* - 3GPP2 file format
|
||||
*
|
||||
* MPEG-4 Audio / Part 3 (.m4a)& MPEG 4 Video (m4v, mp4) extension.
|
||||
* Support for Apple iTunes tags as found in a M4A/M4V files.
|
||||
* Ref:
|
||||
* https://en.wikipedia.org/wiki/ISO_base_media_file_format
|
||||
* https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html
|
||||
* http://atomicparsley.sourceforge.net/mpeg-4files.html
|
||||
* https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
|
||||
* https://wiki.multimedia.cx/index.php/QuickTime_container
|
||||
*/
|
||||
class MP4Parser extends BasicParser_1.BasicParser {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.atomParsers = {
|
||||
/**
|
||||
* Parse movie header (mvhd) atom
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-56313
|
||||
*/
|
||||
mvhd: async (len) => {
|
||||
const _mvhd = await this.tokenizer.readToken(new AtomToken.MvhdAtom(len));
|
||||
this.metadata.setFormat('creationTime', _mvhd.creationTime);
|
||||
this.metadata.setFormat('modificationTime', _mvhd.modificationTime);
|
||||
},
|
||||
/**
|
||||
* Parse media header (mdhd) atom
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25615
|
||||
*/
|
||||
mdhd: async (len) => {
|
||||
const mdhd_data = await this.tokenizer.readToken(new AtomToken.MdhdAtom(len));
|
||||
// this.parse_mxhd(mdhd_data, this.currentTrack);
|
||||
const td = this.getTrackDescription();
|
||||
td.creationTime = mdhd_data.creationTime;
|
||||
td.modificationTime = mdhd_data.modificationTime;
|
||||
td.timeScale = mdhd_data.timeScale;
|
||||
td.duration = mdhd_data.duration;
|
||||
},
|
||||
chap: async (len) => {
|
||||
const td = this.getTrackDescription();
|
||||
const trackIds = [];
|
||||
while (len >= Token.UINT32_BE.len) {
|
||||
trackIds.push(await this.tokenizer.readNumber(Token.UINT32_BE));
|
||||
len -= Token.UINT32_BE.len;
|
||||
}
|
||||
td.chapterList = trackIds;
|
||||
},
|
||||
tkhd: async (len) => {
|
||||
const track = (await this.tokenizer.readToken(new AtomToken.TrackHeaderAtom(len)));
|
||||
this.tracks.push(track);
|
||||
},
|
||||
/**
|
||||
* Parse mdat atom.
|
||||
* Will scan for chapters
|
||||
*/
|
||||
mdat: async (len) => {
|
||||
this.audioLengthInBytes = len;
|
||||
this.calculateBitRate();
|
||||
if (this.options.includeChapters) {
|
||||
const trackWithChapters = this.tracks.filter(track => track.chapterList);
|
||||
if (trackWithChapters.length === 1) {
|
||||
const chapterTrackIds = trackWithChapters[0].chapterList;
|
||||
const chapterTracks = this.tracks.filter(track => chapterTrackIds.indexOf(track.trackId) !== -1);
|
||||
if (chapterTracks.length === 1) {
|
||||
return this.parseChapterTrack(chapterTracks[0], trackWithChapters[0], len);
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.tokenizer.ignore(len);
|
||||
},
|
||||
ftyp: async (len) => {
|
||||
const types = [];
|
||||
while (len > 0) {
|
||||
const ftype = await this.tokenizer.readToken(AtomToken.ftyp);
|
||||
len -= AtomToken.ftyp.len;
|
||||
const value = ftype.type.replace(/\W/g, '');
|
||||
if (value.length > 0) {
|
||||
types.push(value); // unshift for backward compatibility
|
||||
}
|
||||
}
|
||||
debug(`ftyp: ${types.join('/')}`);
|
||||
const x = types.filter(distinct).join('/');
|
||||
this.metadata.setFormat('container', x);
|
||||
},
|
||||
/**
|
||||
* Parse sample description atom
|
||||
*/
|
||||
stsd: async (len) => {
|
||||
const stsd = await this.tokenizer.readToken(new AtomToken.StsdAtom(len));
|
||||
const trackDescription = this.getTrackDescription();
|
||||
trackDescription.soundSampleDescription = stsd.table.map(dfEntry => this.parseSoundSampleDescription(dfEntry));
|
||||
},
|
||||
/**
|
||||
* sample-to-Chunk Atoms
|
||||
*/
|
||||
stsc: async (len) => {
|
||||
const stsc = await this.tokenizer.readToken(new AtomToken.StscAtom(len));
|
||||
this.getTrackDescription().sampleToChunkTable = stsc.entries;
|
||||
},
|
||||
/**
|
||||
* time to sample
|
||||
*/
|
||||
stts: async (len) => {
|
||||
const stts = await this.tokenizer.readToken(new AtomToken.SttsAtom(len));
|
||||
this.getTrackDescription().timeToSampleTable = stts.entries;
|
||||
},
|
||||
/**
|
||||
* Parse sample-sizes atom ('stsz')
|
||||
*/
|
||||
stsz: async (len) => {
|
||||
const stsz = await this.tokenizer.readToken(new AtomToken.StszAtom(len));
|
||||
const td = this.getTrackDescription();
|
||||
td.sampleSize = stsz.sampleSize;
|
||||
td.sampleSizeTable = stsz.entries;
|
||||
},
|
||||
/**
|
||||
* Parse chunk-offset atom ('stco')
|
||||
*/
|
||||
stco: async (len) => {
|
||||
const stco = await this.tokenizer.readToken(new AtomToken.StcoAtom(len));
|
||||
this.getTrackDescription().chunkOffsetTable = stco.entries; // remember chunk offsets
|
||||
},
|
||||
date: async (len) => {
|
||||
const date = await this.tokenizer.readToken(new Token.StringType(len, 'utf-8'));
|
||||
this.addTag('date', date);
|
||||
}
|
||||
};
|
||||
}
|
||||
static read_BE_Integer(array, signed) {
|
||||
const integerType = (signed ? 'INT' : 'UINT') + array.length * 8 + (array.length > 1 ? '_BE' : '');
|
||||
const token = Token[integerType];
|
||||
if (!token) {
|
||||
throw new Error('Token for integer type not found: "' + integerType + '"');
|
||||
}
|
||||
return Number(token.get(array, 0));
|
||||
}
|
||||
async parse() {
|
||||
this.tracks = [];
|
||||
let remainingFileSize = this.tokenizer.fileInfo.size;
|
||||
while (!this.tokenizer.fileInfo.size || remainingFileSize > 0) {
|
||||
try {
|
||||
const token = await this.tokenizer.peekToken(AtomToken.Header);
|
||||
if (token.name === '\0\0\0\0') {
|
||||
const errMsg = `Error at offset=${this.tokenizer.position}: box.id=0`;
|
||||
debug(errMsg);
|
||||
this.addWarning(errMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const errMsg = `Error at offset=${this.tokenizer.position}: ${error.message}`;
|
||||
debug(errMsg);
|
||||
this.addWarning(errMsg);
|
||||
break;
|
||||
}
|
||||
const rootAtom = await Atom_1.Atom.readAtom(this.tokenizer, (atom, remaining) => this.handleAtom(atom, remaining), null, remainingFileSize);
|
||||
remainingFileSize -= rootAtom.header.length === BigInt(0) ? remainingFileSize : Number(rootAtom.header.length);
|
||||
}
|
||||
// Post process metadata
|
||||
const formatList = [];
|
||||
this.tracks.forEach(track => {
|
||||
const trackFormats = [];
|
||||
track.soundSampleDescription.forEach(ssd => {
|
||||
const streamInfo = {};
|
||||
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 = type_1.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('/'));
|
||||
}
|
||||
});
|
||||
if (formatList.length > 0) {
|
||||
this.metadata.setFormat('codec', formatList.filter(distinct).join('+'));
|
||||
}
|
||||
const audioTracks = this.tracks.filter(track => {
|
||||
return track.soundSampleDescription.length >= 1 && track.soundSampleDescription[0].description && track.soundSampleDescription[0].description.numAudioChannels > 0;
|
||||
});
|
||||
if (audioTracks.length >= 1) {
|
||||
const audioTrack = audioTracks[0];
|
||||
const duration = audioTrack.duration / audioTrack.timeScale;
|
||||
this.metadata.setFormat('duration', duration); // calculate duration in seconds
|
||||
const ssd = audioTrack.soundSampleDescription[0];
|
||||
if (ssd.description) {
|
||||
this.metadata.setFormat('sampleRate', ssd.description.sampleRate);
|
||||
this.metadata.setFormat('bitsPerSample', ssd.description.sampleSize);
|
||||
this.metadata.setFormat('numberOfChannels', ssd.description.numAudioChannels);
|
||||
}
|
||||
const encoderInfo = encoderDict[ssd.dataFormat];
|
||||
if (encoderInfo) {
|
||||
this.metadata.setFormat('lossless', !encoderInfo.lossy);
|
||||
}
|
||||
this.calculateBitRate();
|
||||
}
|
||||
}
|
||||
async handleAtom(atom, remaining) {
|
||||
if (atom.parent) {
|
||||
switch (atom.parent.header.name) {
|
||||
case 'ilst':
|
||||
case '<id>':
|
||||
return this.parseMetadataItemData(atom);
|
||||
}
|
||||
}
|
||||
// const payloadLength = atom.getPayloadLength(remaining);
|
||||
if (this.atomParsers[atom.header.name]) {
|
||||
return this.atomParsers[atom.header.name](remaining);
|
||||
}
|
||||
else {
|
||||
debug(`No parser for atom path=${atom.atomPath}, payload-len=${remaining}, ignoring atom`);
|
||||
await this.tokenizer.ignore(remaining);
|
||||
}
|
||||
}
|
||||
getTrackDescription() {
|
||||
return this.tracks[this.tracks.length - 1];
|
||||
}
|
||||
calculateBitRate() {
|
||||
if (this.audioLengthInBytes && this.metadata.format.duration) {
|
||||
this.metadata.setFormat('bitrate', 8 * this.audioLengthInBytes / this.metadata.format.duration);
|
||||
}
|
||||
}
|
||||
addTag(id, value) {
|
||||
this.metadata.addTag(tagFormat, id, value);
|
||||
}
|
||||
addWarning(message) {
|
||||
debug('Warning: ' + message);
|
||||
this.metadata.addWarning(message);
|
||||
}
|
||||
/**
|
||||
* Parse data of Meta-item-list-atom (item of 'ilst' atom)
|
||||
* @param metaAtom
|
||||
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW8
|
||||
*/
|
||||
parseMetadataItemData(metaAtom) {
|
||||
let tagKey = metaAtom.header.name;
|
||||
return metaAtom.readAtoms(this.tokenizer, async (child, remaining) => {
|
||||
const payLoadLength = child.getPayloadLength(remaining);
|
||||
switch (child.header.name) {
|
||||
case 'data': // value atom
|
||||
return this.parseValueAtom(tagKey, child);
|
||||
case 'name': // name atom (optional)
|
||||
const name = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
||||
tagKey += ':' + name.name;
|
||||
break;
|
||||
case 'mean': // name atom (optional)
|
||||
const mean = await this.tokenizer.readToken(new AtomToken.NameAtom(payLoadLength));
|
||||
// console.log(" %s[%s] = %s", tagKey, header.name, mean.name);
|
||||
tagKey += ':' + mean.name;
|
||||
break;
|
||||
default:
|
||||
const dataAtom = await this.tokenizer.readToken(new Token.BufferType(payLoadLength));
|
||||
this.addWarning('Unsupported meta-item: ' + tagKey + '[' + child.header.name + '] => value=' + dataAtom.toString('hex') + ' ascii=' + dataAtom.toString('ascii'));
|
||||
}
|
||||
}, metaAtom.getPayloadLength(0));
|
||||
}
|
||||
async parseValueAtom(tagKey, metaAtom) {
|
||||
const dataAtom = await this.tokenizer.readToken(new AtomToken.DataAtom(Number(metaAtom.header.length) - AtomToken.Header.len));
|
||||
if (dataAtom.type.set !== 0) {
|
||||
throw new Error('Unsupported type-set != 0: ' + dataAtom.type.set);
|
||||
}
|
||||
// Use well-known-type table
|
||||
// Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW35
|
||||
switch (dataAtom.type.type) {
|
||||
case 0: // reserved: Reserved for use where no type needs to be indicated
|
||||
switch (tagKey) {
|
||||
case 'trkn':
|
||||
case 'disk':
|
||||
const num = Token.UINT8.get(dataAtom.value, 3);
|
||||
const of = Token.UINT8.get(dataAtom.value, 5);
|
||||
// console.log(" %s[data] = %s/%s", tagKey, num, of);
|
||||
this.addTag(tagKey, num + '/' + of);
|
||||
break;
|
||||
case 'gnre':
|
||||
const genreInt = Token.UINT8.get(dataAtom.value, 1);
|
||||
const genreStr = ID3v1Parser_1.Genres[genreInt - 1];
|
||||
// console.log(" %s[data] = %s", tagKey, genreStr);
|
||||
this.addTag(tagKey, genreStr);
|
||||
break;
|
||||
default:
|
||||
// console.log(" reserved-data: name=%s, len=%s, set=%s, type=%s, locale=%s, value{ hex=%s, ascii=%s }",
|
||||
// header.name, header.length, dataAtom.type.set, dataAtom.type.type, dataAtom.locale, dataAtom.value.toString('hex'), dataAtom.value.toString('ascii'));
|
||||
}
|
||||
break;
|
||||
case 1: // UTF-8: Without any count or NULL terminator
|
||||
case 18: // Unknown: Found in m4b in combination with a '©gen' tag
|
||||
this.addTag(tagKey, dataAtom.value.toString('utf-8'));
|
||||
break;
|
||||
case 13: // JPEG
|
||||
if (this.options.skipCovers)
|
||||
break;
|
||||
this.addTag(tagKey, {
|
||||
format: 'image/jpeg',
|
||||
data: Buffer.from(dataAtom.value)
|
||||
});
|
||||
break;
|
||||
case 14: // PNG
|
||||
if (this.options.skipCovers)
|
||||
break;
|
||||
this.addTag(tagKey, {
|
||||
format: 'image/png',
|
||||
data: Buffer.from(dataAtom.value)
|
||||
});
|
||||
break;
|
||||
case 21: // BE Signed Integer
|
||||
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, true));
|
||||
break;
|
||||
case 22: // BE Unsigned Integer
|
||||
this.addTag(tagKey, MP4Parser.read_BE_Integer(dataAtom.value, false));
|
||||
break;
|
||||
case 65: // An 8-bit signed integer
|
||||
this.addTag(tagKey, dataAtom.value.readInt8(0));
|
||||
break;
|
||||
case 66: // A big-endian 16-bit signed integer
|
||||
this.addTag(tagKey, dataAtom.value.readInt16BE(0));
|
||||
break;
|
||||
case 67: // A big-endian 32-bit signed integer
|
||||
this.addTag(tagKey, dataAtom.value.readInt32BE(0));
|
||||
break;
|
||||
default:
|
||||
this.addWarning(`atom key=${tagKey}, has unknown well-known-type (data-type): ${dataAtom.type.type}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param sampleDescription
|
||||
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html#//apple_ref/doc/uid/TP40000939-CH205-128916
|
||||
*/
|
||||
parseSoundSampleDescription(sampleDescription) {
|
||||
const ssd = {
|
||||
dataFormat: sampleDescription.dataFormat,
|
||||
dataReferenceIndex: sampleDescription.dataReferenceIndex
|
||||
};
|
||||
let offset = 0;
|
||||
const version = AtomToken.SoundSampleDescriptionVersion.get(sampleDescription.description, offset);
|
||||
offset += AtomToken.SoundSampleDescriptionVersion.len;
|
||||
if (version.version === 0 || version.version === 1) {
|
||||
// Sound Sample Description (Version 0)
|
||||
ssd.description = AtomToken.SoundSampleDescriptionV0.get(sampleDescription.description, offset);
|
||||
}
|
||||
else {
|
||||
debug(`Warning: sound-sample-description ${version} not implemented`);
|
||||
}
|
||||
return ssd;
|
||||
}
|
||||
async parseChapterTrack(chapterTrack, track, len) {
|
||||
if (!chapterTrack.sampleSize) {
|
||||
if (chapterTrack.chunkOffsetTable.length !== chapterTrack.sampleSizeTable.length)
|
||||
throw new Error('Expected equal chunk-offset-table & sample-size-table length.');
|
||||
}
|
||||
const chapters = [];
|
||||
for (let i = 0; i < chapterTrack.chunkOffsetTable.length && len > 0; ++i) {
|
||||
const chunkOffset = chapterTrack.chunkOffsetTable[i];
|
||||
const nextChunkLen = chunkOffset - this.tokenizer.position;
|
||||
const sampleSize = chapterTrack.sampleSize > 0 ? chapterTrack.sampleSize : chapterTrack.sampleSizeTable[i];
|
||||
len -= nextChunkLen + sampleSize;
|
||||
if (len < 0)
|
||||
throw new Error('Chapter chunk exceeding token length');
|
||||
await this.tokenizer.ignore(nextChunkLen);
|
||||
const title = await this.tokenizer.readToken(new AtomToken.ChapterText(sampleSize));
|
||||
debug(`Chapter ${i + 1}: ${title}`);
|
||||
const chapter = {
|
||||
title,
|
||||
sampleOffset: this.findSampleOffset(track, this.tokenizer.position)
|
||||
};
|
||||
debug(`Chapter title=${chapter.title}, offset=${chapter.sampleOffset}/${this.tracks[0].duration}`);
|
||||
chapters.push(chapter);
|
||||
}
|
||||
this.metadata.setFormat('chapters', chapters);
|
||||
await this.tokenizer.ignore(len);
|
||||
}
|
||||
findSampleOffset(track, chapterOffset) {
|
||||
let totalDuration = 0;
|
||||
track.timeToSampleTable.forEach(e => {
|
||||
totalDuration += e.count * e.duration;
|
||||
});
|
||||
debug(`Total duration=${totalDuration}`);
|
||||
let chunkIndex = 0;
|
||||
while (chunkIndex < track.chunkOffsetTable.length && track.chunkOffsetTable[chunkIndex] < chapterOffset) {
|
||||
++chunkIndex;
|
||||
}
|
||||
return this.getChunkDuration(chunkIndex + 1, track);
|
||||
}
|
||||
getChunkDuration(chunkId, track) {
|
||||
let ttsi = 0;
|
||||
let ttsc = track.timeToSampleTable[ttsi].count;
|
||||
let ttsd = track.timeToSampleTable[ttsi].duration;
|
||||
let curChunkId = 1;
|
||||
let samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
||||
let totalDuration = 0;
|
||||
while (curChunkId < chunkId) {
|
||||
const nrOfSamples = Math.min(ttsc, samplesPerChunk);
|
||||
totalDuration += nrOfSamples * ttsd;
|
||||
ttsc -= nrOfSamples;
|
||||
samplesPerChunk -= nrOfSamples;
|
||||
if (samplesPerChunk === 0) {
|
||||
++curChunkId;
|
||||
samplesPerChunk = this.getSamplesPerChunk(curChunkId, track.sampleToChunkTable);
|
||||
}
|
||||
else {
|
||||
++ttsi;
|
||||
ttsc = track.timeToSampleTable[ttsi].count;
|
||||
ttsd = track.timeToSampleTable[ttsi].duration;
|
||||
}
|
||||
}
|
||||
return totalDuration;
|
||||
}
|
||||
getSamplesPerChunk(chunkId, stcTable) {
|
||||
for (let i = 0; i < stcTable.length - 1; ++i) {
|
||||
if (chunkId >= stcTable[i].firstChunk && chunkId < stcTable[i + 1].firstChunk) {
|
||||
return stcTable[i].samplesPerChunk;
|
||||
}
|
||||
}
|
||||
return stcTable[stcTable.length - 1].samplesPerChunk;
|
||||
}
|
||||
}
|
||||
exports.MP4Parser = MP4Parser;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { CaseInsensitiveTagMap } from '../common/CaseInsensitiveTagMap';
|
||||
export declare const tagType = "iTunes";
|
||||
export declare class MP4TagMapper extends CaseInsensitiveTagMap {
|
||||
constructor();
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MP4TagMapper = exports.tagType = void 0;
|
||||
const CaseInsensitiveTagMap_1 = require("../common/CaseInsensitiveTagMap");
|
||||
/**
|
||||
* Ref: https://github.com/sergiomb2/libmp4v2/wiki/iTunesMetadata
|
||||
*/
|
||||
const mp4TagMap = {
|
||||
'©nam': 'title',
|
||||
'©ART': 'artist',
|
||||
aART: 'albumartist',
|
||||
/**
|
||||
* ToDo: Album artist seems to be stored here while Picard documentation says: aART
|
||||
*/
|
||||
'----:com.apple.iTunes:Band': 'albumartist',
|
||||
'©alb': 'album',
|
||||
'©day': 'date',
|
||||
'©cmt': 'comment',
|
||||
'©com': 'comment',
|
||||
trkn: 'track',
|
||||
disk: 'disk',
|
||||
'©gen': 'genre',
|
||||
covr: 'picture',
|
||||
'©wrt': 'composer',
|
||||
'©lyr': 'lyrics',
|
||||
soal: 'albumsort',
|
||||
sonm: 'titlesort',
|
||||
soar: 'artistsort',
|
||||
soaa: 'albumartistsort',
|
||||
soco: 'composersort',
|
||||
'----:com.apple.iTunes:LYRICIST': 'lyricist',
|
||||
'----:com.apple.iTunes:CONDUCTOR': 'conductor',
|
||||
'----:com.apple.iTunes:REMIXER': 'remixer',
|
||||
'----:com.apple.iTunes:ENGINEER': 'engineer',
|
||||
'----:com.apple.iTunes:PRODUCER': 'producer',
|
||||
'----:com.apple.iTunes:DJMIXER': 'djmixer',
|
||||
'----:com.apple.iTunes:MIXER': 'mixer',
|
||||
'----:com.apple.iTunes:LABEL': 'label',
|
||||
'©grp': 'grouping',
|
||||
'----:com.apple.iTunes:SUBTITLE': 'subtitle',
|
||||
'----:com.apple.iTunes:DISCSUBTITLE': 'discsubtitle',
|
||||
cpil: 'compilation',
|
||||
tmpo: 'bpm',
|
||||
'----:com.apple.iTunes:MOOD': 'mood',
|
||||
'----:com.apple.iTunes:MEDIA': 'media',
|
||||
'----:com.apple.iTunes:CATALOGNUMBER': 'catalognumber',
|
||||
tvsh: 'tvShow',
|
||||
tvsn: 'tvSeason',
|
||||
tves: 'tvEpisode',
|
||||
sosn: 'tvShowSort',
|
||||
tven: 'tvEpisodeId',
|
||||
tvnn: 'tvNetwork',
|
||||
pcst: 'podcast',
|
||||
purl: 'podcasturl',
|
||||
'----:com.apple.iTunes:MusicBrainz Album Status': 'releasestatus',
|
||||
'----:com.apple.iTunes:MusicBrainz Album Type': 'releasetype',
|
||||
'----:com.apple.iTunes:MusicBrainz Album Release Country': 'releasecountry',
|
||||
'----:com.apple.iTunes:SCRIPT': 'script',
|
||||
'----:com.apple.iTunes:LANGUAGE': 'language',
|
||||
cprt: 'copyright',
|
||||
'©cpy': 'copyright',
|
||||
'----:com.apple.iTunes:LICENSE': 'license',
|
||||
'©too': 'encodedby',
|
||||
pgap: 'gapless',
|
||||
'----:com.apple.iTunes:BARCODE': 'barcode',
|
||||
'----:com.apple.iTunes:ISRC': 'isrc',
|
||||
'----:com.apple.iTunes:ASIN': 'asin',
|
||||
'----:com.apple.iTunes:NOTES': 'comment',
|
||||
'----:com.apple.iTunes:MusicBrainz Track Id': 'musicbrainz_recordingid',
|
||||
'----:com.apple.iTunes:MusicBrainz Release Track Id': 'musicbrainz_trackid',
|
||||
'----:com.apple.iTunes:MusicBrainz Album Id': 'musicbrainz_albumid',
|
||||
'----:com.apple.iTunes:MusicBrainz Artist Id': 'musicbrainz_artistid',
|
||||
'----:com.apple.iTunes:MusicBrainz Album Artist Id': 'musicbrainz_albumartistid',
|
||||
'----:com.apple.iTunes:MusicBrainz Release Group Id': 'musicbrainz_releasegroupid',
|
||||
'----:com.apple.iTunes:MusicBrainz Work Id': 'musicbrainz_workid',
|
||||
'----:com.apple.iTunes:MusicBrainz TRM Id': 'musicbrainz_trmid',
|
||||
'----:com.apple.iTunes:MusicBrainz Disc Id': 'musicbrainz_discid',
|
||||
'----:com.apple.iTunes:Acoustid Id': 'acoustid_id',
|
||||
'----:com.apple.iTunes:Acoustid Fingerprint': 'acoustid_fingerprint',
|
||||
'----:com.apple.iTunes:MusicIP PUID': 'musicip_puid',
|
||||
'----:com.apple.iTunes:fingerprint': 'musicip_fingerprint',
|
||||
'----:com.apple.iTunes:replaygain_track_gain': 'replaygain_track_gain',
|
||||
'----:com.apple.iTunes:replaygain_track_peak': 'replaygain_track_peak',
|
||||
'----:com.apple.iTunes:replaygain_album_gain': 'replaygain_album_gain',
|
||||
'----:com.apple.iTunes:replaygain_album_peak': 'replaygain_album_peak',
|
||||
'----:com.apple.iTunes:replaygain_track_minmax': 'replaygain_track_minmax',
|
||||
'----:com.apple.iTunes:replaygain_album_minmax': 'replaygain_album_minmax',
|
||||
'----:com.apple.iTunes:replaygain_undo': 'replaygain_undo',
|
||||
// Additional mappings:
|
||||
gnre: 'genre',
|
||||
'----:com.apple.iTunes:ALBUMARTISTSORT': 'albumartistsort',
|
||||
'----:com.apple.iTunes:ARTISTS': 'artists',
|
||||
'----:com.apple.iTunes:ORIGINALDATE': 'originaldate',
|
||||
'----:com.apple.iTunes:ORIGINALYEAR': 'originalyear',
|
||||
// '----:com.apple.iTunes:PERFORMER': 'performer'
|
||||
desc: 'description',
|
||||
ldes: 'longDescription',
|
||||
'©mvn': 'movement',
|
||||
'©mvi': 'movementIndex',
|
||||
'©mvc': 'movementTotal',
|
||||
'©wrk': 'work',
|
||||
catg: 'category',
|
||||
egid: 'podcastId',
|
||||
hdvd: 'hdVideo',
|
||||
keyw: 'keywords',
|
||||
shwm: 'showMovement',
|
||||
stik: 'stik'
|
||||
};
|
||||
exports.tagType = 'iTunes';
|
||||
class MP4TagMapper extends CaseInsensitiveTagMap_1.CaseInsensitiveTagMap {
|
||||
constructor() {
|
||||
super([exports.tagType], mp4TagMap);
|
||||
}
|
||||
}
|
||||
exports.MP4TagMapper = MP4TagMapper;
|
||||
Reference in New Issue
Block a user