1
0
mirror of synced 2026-09-01 00:04:45 +00:00
Files
Dan Grier d2a7d6f3e4 fix(mp4): locate each sample entry after the first correctly
A SampleEntry extends Box, so its size field covers itself
(ISO/IEC 14496-12, 8.5.2). StsdAtom advanced the cursor by the full size
after having already stepped over the 4-byte size field, overshooting by
4 bytes for every entry.

The first entry is unaffected, so this only shows up once a sample
description holds more than one entry, which no existing test sample
does. From the second entry onwards the data format is read from the
reserved bytes, giving an all-zero FourCC:

  FieldDecodingError: FourCC contains invalid characters: 00 00 00 00

#2340 corrected the length passed to SampleDescriptionTable on the line
above, but left the cursor advance inconsistent with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:48:20 +02:00

1095 lines
31 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as Token from 'token-types';
import initDebug from 'debug';
import { FourCcToken } from '../common/FourCC.js';
import type { IToken, IGetToken } from 'strtok3';
import { makeUnexpectedFileContentError } from '../ParseError.js';
import * as util from '../common/Util.js';
import { FieldDecodingError } from '../ParseError.js';
const debug = initDebug('music-metadata:parser:MP4:atom');
export class Mp4ContentError extends makeUnexpectedFileContentError('MP4'){
}
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 movies sound.
* A value of 1.0 indicates full volume.
*/
preferredVolume: number,
/**
* Reserved: Ten bytes reserved for use by Apple. Set to 0.
*/
// reserved: number,
/**
* 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.
*/
// matrixStructure: ???;
/**
* 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 const Header: IToken<IAtomHeader> = {
len: 8,
get: (buf: Uint8Array, off: number): IAtomHeader => {
const length = Token.UINT32_BE.get(buf, off);
if (length < 0)
throw new Mp4ContentError('Invalid atom header length');
return {
length: BigInt(length),
name: new Token.StringType(4, 'latin1').get(buf, off + 4)
};
},
put: (buf: Uint8Array, off: number, hdr: IAtomHeader) => {
Token.UINT32_BE.put(buf, off, Number(hdr.length));
return 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
*/
export const ExtendedSize: IToken<bigint> = Token.UINT64_BE;
export const ftyp: IGetToken<IAtomFtyp> = {
len: 4,
get: (buf: Uint8Array, off: number): IAtomFtyp => {
return {
type: new Token.StringType(4, 'ascii').get(buf, off)
};
}
};
/**
* Token: Movie Header Atom
*/
export const mhdr: IGetToken<IMovieHeaderAtom> = {
len: 8,
get: (buf: Uint8Array, off: number): IMovieHeaderAtom => {
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
*/
export abstract class FixedLengthAtom {
public 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
* @param atomId Atom ID
*/
protected constructor(len: number, expLen: number, atomId: string) {
if (len < expLen) {
throw new Mp4ContentError(`Atom ${atomId} expected to be ${expLen}, but specifies ${len} bytes long.`);
}if (len > expLen) {
debug(`Warning: atom ${atomId} expected to be ${expLen}, but was actually ${len} bytes long.`);
}
this.len = len;
}
}
/**
* 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
}
/**
* Timestamp stored in seconds since Mac Epoch (1 January 1904)
*/
const SecondsSinceMacEpoch: IGetToken<Date> = {
len: 4,
get: (buf: Uint8Array, off: number): Date => {
const secondsSinceUnixEpoch = Token.UINT32_BE.get(buf, off) - 2082844800;
return new Date(secondsSinceUnixEpoch * 1000);
}};
const SecondsSinceMacEpoch64: IGetToken<Date> = {
len: 8,
get: (buf: Uint8Array, off: number): Date => {
const secondsSinceUnixEpoch = Number(Token.UINT64_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
*/
export class MdhdAtom extends FixedLengthAtom implements IGetToken<IAtomMdhd> {
public constructor(len: number) {
super(len, 24, 'mdhd');
}
public get(buf: Uint8Array, off: number): IAtomMdhd {
const version = Token.UINT8.get(buf, off + 0);
const flags = Token.UINT24_BE.get(buf, off + 1);
switch (version) {
case 0:
// Version 0: 32-bit fields
return {
version,
flags,
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)
};
case 1:
return {
version,
flags,
creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
timeScale: Token.UINT32_BE.get(buf, off + 20),
duration: Number(Token.UINT64_BE.get(buf, off + 24)),
language: Token.UINT16_BE.get(buf, off + 32),
quality: Token.UINT16_BE.get(buf, off + 34)
};
default:
throw new FieldDecodingError('Invalid mdhd version header');
}
}
}
/**
* Token: Movie Header Atom
*/
export class MvhdAtom extends FixedLengthAtom implements IGetToken<IAtomMvhd> {
public constructor(len: number) {
super(len, 100, 'mvhd');
}
public get(buf: Uint8Array, off: number): IAtomMvhd {
const version = Token.UINT8.get(buf, off);
const flags = Token.UINT24_BE.get(buf, off + 1);
if (version === 1) {
// Version 1: 64-bit creation/modification times and duration
return {
version,
flags,
creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
timeScale: Token.UINT32_BE.get(buf, off + 20),
duration: Number(Token.UINT64_BE.get(buf, off + 24)),
preferredRate: Token.UINT32_BE.get(buf, off + 32),
preferredVolume: Token.UINT16_BE.get(buf, off + 36),
// ignore reserved: 10 bytes
// ignore matrix structure: 36 bytes
previewTime: Token.UINT32_BE.get(buf, off + 84),
previewDuration: Token.UINT32_BE.get(buf, off + 88),
posterTime: Token.UINT32_BE.get(buf, off + 92),
selectionTime: Token.UINT32_BE.get(buf, off + 96),
selectionDuration: Token.UINT32_BE.get(buf, off + 100),
currentTime: Token.UINT32_BE.get(buf, off + 104),
nextTrackID: Token.UINT32_BE.get(buf, off + 108)
};
}
// Version 0: 32-bit fields
return {
version,
flags,
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 reserved: 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)
};
}
}
/**
* 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, // ToDo: enum?
type: number
},
/**
* Locale Indicator
*/
locale: number,
/**
* An array of bytes containing the value of the metadata.
*/
value: Uint8Array;
}
/**
* Data Atom Structure
*/
export class DataAtom implements IGetToken<IDataAtom> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): IDataAtom {
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: new Token.Uint8ArrayType(this.len - 8).get(buf, off + 8)
};
}
}
/**
* 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 class NameAtom implements IGetToken<INameAtom> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): INameAtom {
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)
};
}
}
/**
* 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 movies time coordinate system).
* Note that this property is derived from the tracks edits. The value of this field is equal to the sum of the
* durations of all of the tracks 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 tracks 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 tracks sound is to be played.
* A value of 1.0 indicates normal volume.
*/
volume: number
}
/**
* Track Header Atoms structure (`tkhd`)
* Ref: https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25550
*/
export class TrackHeaderAtom implements IGetToken<ITrackHeaderAtom> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): ITrackHeaderAtom {
const version = Token.UINT8.get(buf, off);
const flags = Token.UINT24_BE.get(buf, off + 1);
switch (version) {
case 0:
// Version 0: 32-bit fields
return {
version,
flags,
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),
// reserved 8 bytes
layer: Token.UINT16_BE.get(buf, off + 32),
alternateGroup: Token.UINT16_BE.get(buf, off + 34),
volume: Token.UINT16_BE.get(buf, off + 36) // ToDo: fixed point
// ToDo: add remaining fields
};
case 1:
// Version 1: 64-bit creation/modification times and duration
return {
version,
flags,
creationTime: SecondsSinceMacEpoch64.get(buf, off + 4),
modificationTime: SecondsSinceMacEpoch64.get(buf, off + 12),
trackId: Token.UINT32_BE.get(buf, off + 20),
// reserved 4 bytes
duration: Number(Token.UINT64_BE.get(buf, off + 28)),
// reserved 8 bytes
layer: Token.UINT16_BE.get(buf, off + 44),
alternateGroup: Token.UINT16_BE.get(buf, off + 46),
volume: Token.UINT16_BE.get(buf, off + 48) // ToDo: fixed point
// ToDo: add remaining fields
};
default:
throw new FieldDecodingError('Invalid tkhd version header');
}
}
}
/**
* Atom: Sample Description Atom ('stsd')
*/
interface IAtomStsdHeader extends IVersionAndFlags {
numberOfEntries: number;
}
/**
* 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: IGetToken<IAtomStsdHeader> = {
len: 8,
get: (buf: Uint8Array, off: number): IAtomStsdHeader => {
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')
*/
export interface ISampleDescription {
dataFormat: string;
dataReferenceIndex: number;
description: Uint8Array | undefined;
}
export interface IAtomStsd {
header: IAtomStsdHeader;
table: ISampleDescription[];
}
/**
* Atom: Sample Description Atom ('stsd')
* Ref: https://developer.apple.com/documentation/quicktime-file-format/sample_description_atom
*/
class SampleDescriptionTable implements IGetToken<ISampleDescription> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): ISampleDescription {
const descrLen = this.len - 12;
return {
dataFormat: FourCcToken.get(buf, off),
dataReferenceIndex: Token.UINT16_BE.get(buf, off + 10),
description: descrLen > 0 ? new Token.Uint8ArrayType(descrLen).get(buf, off + 12) : undefined
};
}
}
/**
* 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 class StsdAtom implements IGetToken<IAtomStsd> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): IAtomStsd {
const header = stsdHeader.get(buf, off);
off += stsdHeader.len;
const table: ISampleDescription[] = [];
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 - Token.UINT32_BE.len).get(buf, off));
// A SampleEntry extends Box, so its size covers the size field already stepped over
off += size - Token.UINT32_BE.len;
}
return {
header,
table
};
}
}
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 const SoundSampleDescriptionVersion: IGetToken<ISoundSampleDescriptionVersion> = {
len: 8,
get(buf: Uint8Array, off: number): ISoundSampleDescriptionVersion {
return {
version: Token.INT16_BE.get(buf, off),
revision: Token.INT16_BE.get(buf, off + 2),
vendor: Token.INT32_BE.get(buf, off + 4)
};
}
};
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 const SoundSampleDescriptionV0: IGetToken<ISoundSampleDescriptionV0> = {
len: 12,
get(buf: Uint8Array, off: number): ISoundSampleDescriptionV0 {
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
};
}
};
export interface ITableAtom<T> extends IVersionAndFlags {
numberOfEntries: number;
entries: T[]
}
class SimpleTableAtom<T> implements IGetToken<ITableAtom<T>> {
public len: number;
private token: IGetToken<T>
public constructor(len: number, token: IGetToken<T>) {
this.len = len;
this.token = token;
}
public get(buf: Uint8Array, off: number): ITableAtom<T> {
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)
};
}
}
export interface ITimeToSampleToken {
count: number;
duration: number;
}
export const TimeToSampleToken: IGetToken<ITimeToSampleToken> = {
len: 8,
get(buf: Uint8Array, off: number): ITimeToSampleToken {
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 medias samples.
* Ref: https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25696
*/
export class SttsAtom extends SimpleTableAtom<ITimeToSampleToken> {
public constructor(len: number) {
super(len, TimeToSampleToken);
}
}
/**
* Sample-to-Chunk ('stsc') atom table entry interface
*/
export interface ISampleToChunk {
firstChunk: number;
samplesPerChunk: number;
sampleDescriptionId: number;
}
export const SampleToChunkToken: IGetToken<ISampleToChunk> = {
len: 12,
get(buf: Uint8Array, off: number): ISampleToChunk {
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
*/
export class StscAtom extends SimpleTableAtom<ISampleToChunk> {
public constructor(len: number) {
super(len, SampleToChunkToken);
}
}
/**
* 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 class StszAtom implements IGetToken<IStszAtom> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): IStszAtom {
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)
};
}
}
/**
* 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 class StcoAtom extends SimpleTableAtom<number> {
public len: number;
public constructor(len: number) {
super(len, Token.INT32_BE);
this.len = len;
}
}
/**
* Token used to decode text-track from 'mdat' atom (raw data stream)
*/
export class ChapterText implements IGetToken<string> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): string {
const titleLen = Token.INT16_BE.get(buf, off + 0);
const str = new Token.StringType(titleLen, 'utf-8');
return str.get(buf, off + 2);
}
}
function readTokenTable<T>(buf: Uint8Array, token: IGetToken<T>, off: number, remainingLen: number, numberOfEntries: number): T[] {
debug(`remainingLen=${remainingLen}, numberOfEntries=${numberOfEntries} * token-len=${token.len}`);
if (remainingLen === 0)
return [];
if (remainingLen !== numberOfEntries * token.len) throw new Mp4ContentError('mismatch number-of-entries with remaining atom-length');
const entries: T[] = [];
// parse offset-table
for (let n = 0; n < numberOfEntries; ++n) {
entries.push(token.get(buf, off));
off += token.len;
}
return entries;
}
interface ITrackFragmentHeaderBoxFlags {
baseDataOffsetPresent: boolean;
sampleDescriptionIndexPresent: boolean;
defaultSampleDurationPresent: boolean;
defaultSampleSizePresent: boolean;
defaultSampleFlagsPresent: boolean;
defaultDurationIsEmpty: boolean;
defaultBaseIsMoof: boolean;
}
export interface ITrackFragmentHeaderBox {
version: number;
flags: ITrackFragmentHeaderBoxFlags;
trackId: number;
baseDataOffset?: bigint;
sampleDescriptionIndex?: number;
defaultSampleDuration?: number;
defaultSampleSize?: number;
defaultSampleFlags?: number;
}
/**
* Sample-size ('tfhd') TrackFragmentHeaderBox
*/
export class TrackFragmentHeaderBox implements IGetToken<ITrackFragmentHeaderBox> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): ITrackFragmentHeaderBox {
const flagOffset = off + 1;
const header: ITrackFragmentHeaderBox = {
version: Token.INT8.get(buf, off),
flags: {
baseDataOffsetPresent: util.getBit(buf, flagOffset + 2, 0),
sampleDescriptionIndexPresent: util.getBit(buf, flagOffset + 2, 1),
defaultSampleDurationPresent: util.getBit(buf, flagOffset + 2, 3),
defaultSampleSizePresent: util.getBit(buf, flagOffset + 2, 4),
defaultSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 5),
defaultDurationIsEmpty: util.getBit(buf, flagOffset, 0),
defaultBaseIsMoof: util.getBit(buf, flagOffset, 1)
},
trackId: Token.UINT32_BE.get(buf, 4)
};
let dynOffset = 8;
if (header.flags.baseDataOffsetPresent) {
header.baseDataOffset = Token.UINT64_BE.get(buf, dynOffset);
dynOffset += 8;
}
if (header.flags.sampleDescriptionIndexPresent) {
header.sampleDescriptionIndex = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleDurationPresent) {
header.defaultSampleDuration = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleSizePresent) {
header.defaultSampleSize = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (header.flags.defaultSampleFlagsPresent) {
header.defaultSampleFlags = Token.UINT32_BE.get(buf, dynOffset);
}
return header;
}
}
interface ITrackRunBoxFlags {
dataOffsetPresent: boolean
firstSampleFlagsPresent: boolean;
sampleDurationPresent: boolean;
sampleSizePresent: boolean;
sampleFlagsPresent: boolean;
sampleCompositionTimeOffsetsPresent: boolean;
}
/**
* trun atom
*/
export interface ITrackRunBox {
version: number;
flags: ITrackRunBoxFlags;
samples: ITrackRunBoxSample[];
sampleCount: number;
dataOffset?: number;
firstSampleFlags?: number;
}
interface ITrackRunBoxSample {
sampleDuration?: number;
sampleSize?: number;
sampleFlags?: number;
sampleCompositionTimeOffset?: number;
}
/**
* Sample-size ('trun') TrackRunBox
*/
export class TrackRunBox implements IGetToken<ITrackRunBox> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): ITrackRunBox {
const flagOffset = off + 1;
const trun: ITrackRunBox = {
version: Token.INT8.get(buf, off),
flags: {
dataOffsetPresent: util.getBit(buf, flagOffset + 2, 0),
firstSampleFlagsPresent: util.getBit(buf, flagOffset + 2, 2),
sampleDurationPresent: util.getBit(buf, flagOffset + 1, 0),
sampleSizePresent: util.getBit(buf, flagOffset + 1, 1),
sampleFlagsPresent: util.getBit(buf, flagOffset + 1, 2),
sampleCompositionTimeOffsetsPresent: util.getBit(buf, flagOffset + 1, 3)
},
sampleCount: Token.UINT32_BE.get(buf, off + 4),
samples: []
}
let dynOffset = off + 8;
if (trun.flags.dataOffsetPresent) {
trun.dataOffset = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.firstSampleFlagsPresent) {
trun.firstSampleFlags = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
for(let n= 0; n < trun.sampleCount; ++n) {
if (dynOffset >= this.len) {
debug("TrackRunBox size mismatch");
break;
}
const sample: ITrackRunBoxSample = {};
if (trun.flags.sampleDurationPresent) {
sample.sampleDuration = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleSizePresent) {
sample.sampleSize = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleFlagsPresent) {
sample.sampleFlags = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
if (trun.flags.sampleCompositionTimeOffsetsPresent) {
sample.sampleCompositionTimeOffset = Token.UINT32_BE.get(buf, dynOffset);
dynOffset += 4;
}
trun.samples.push(sample);
}
return trun;
}
}
export interface IHandlerBox {
version: number;
flags: number;
componentType: string;
handlerType: string;
componentName: string;
}
/**
* HandlerBox (`hdlr`)
*/
export class HandlerBox implements IGetToken<IHandlerBox> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): IHandlerBox {
const _flagOffset = off + 1;
const charTypeToken = new Token.StringType(4, 'utf-8');
return {
version: Token.INT8.get(buf, off),
flags: Token.UINT24_BE.get(buf, off + 1),
componentType: charTypeToken.get(buf, off + 4),
handlerType: charTypeToken.get(buf, off + 8),
componentName: new Token.StringType(this.len - 28, 'utf-8').get(buf, off + 28),
}
}
}
/**
* Chapter Track Reference Box (`chap`)
*/
export class ChapterTrackReferenceBox implements IGetToken<number[]> {
public len: number;
public constructor(len: number) {
this.len = len;
}
public get(buf: Uint8Array, off: number): number[] {
let dynOffset = 0;
const trackIds: number[] = [];
while (dynOffset < this.len) {
trackIds.push(Token.UINT32_BE.get(buf, off + dynOffset));
dynOffset += 4;
}
return trackIds;
}
}