Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Useful for reading codec parameters out of a `codecs=` string, checking support
| L-HEVC | ✅ | ✅ |
| MP4 (mp4a/mp4v — AAC, MP3, MPEG-4/2) | ✅ | ✅ |
| AVS3 video/audio (avs3/av3a) | ✅ | ✅ |
| MPEG-H 3D Audio (mha1/mhm1) | ✅ | ✅ |

## Install

Expand Down Expand Up @@ -98,9 +99,9 @@ console.log(info.toString()); // 'avc1.640028'

- `codecInfoFactory(codecString)` — dispatches by prefix (`vp08`/`vp8`, `vp09`/`vp9`, `av01`,
`avc1`/`avc2`/`avc3`/`avc4`, `hev1`/`hvc1`, `vvc1`/`vvi1`, `lvc1`, `apv1`, `evc1`, `lhv1`/`lhe1`,
`mp4a`/`mp4v`, `avs3`, `av3a`) and returns a `Vp8Info`, `Vp9Info`, `Av1Info`, `H264Info`, `H265Info`,
`H266Info`, `LcevcInfo`, `ApvInfo`, `EvcInfo`, `LhevcInfo`, `Mp4Info`, `Avs3VideoInfo`, or
`Avs3AudioInfo`. Throws `Unknown codec` for anything else.
`mp4a`/`mp4v`, `avs3`, `av3a`, `mha1`/`mha2`/`mhm1`/`mhm2`) and returns a `Vp8Info`, `Vp9Info`,
`Av1Info`, `H264Info`, `H265Info`, `H266Info`, `LcevcInfo`, `ApvInfo`, `EvcInfo`, `LhevcInfo`,
`Mp4Info`, `Avs3VideoInfo`, `Avs3AudioInfo`, or `MpeghInfo`. Throws `Unknown codec` for anything else.
- `vpx` — namespace exporting `Vp8Info`, `Vp9Info`, `vpxInfoFactory`, and the `Vpx*` enums.
- `av1` — namespace exporting `Av1Info` and the `Av1*` enums.
- `h264` — namespace exporting `H264Info`, `AvcProfileIdc`, and the `hProfile`/`hLevel` helpers.
Expand All @@ -123,6 +124,8 @@ console.log(info.toString()); // 'avc1.640028'
MPEG-4 Visual, MPEG-2 video/audio and more (`mp4a.40.2`, `mp4a.69`, `mp4v.20.9`, …).
- `avs3` — namespace exporting `Avs3VideoInfo` (`avs3.<profile>.<level>`) and `Avs3AudioInfo`
(`av3a.<codec_id>`) for the AVS3 video/audio standard.
- `mpegh` — namespace exporting `MpeghInfo` (MPEG-H 3D Audio, `mha1`/`mha2`/`mhm1`/`mhm2` + a
`profileLevelId`, e.g. `mhm1.0c`).
- Shared ISO/IEC 23001-8:2016 colour enums (`ColourPrimaries`, `TransferCharacteristics`,
`MatrixCoefficients`, `VideoFullRangeFlag`) are re-exported from both namespaces.

Expand Down
19 changes: 19 additions & 0 deletions src/codec/mpegh/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// MPEG-H 3D Audio (ISO/IEC 23008-3), carried per ISO/IEC 14496-15 / the MPEG-H file format.
// Codec string: <fourCC>.<mpegh3daProfileLevelIndication> e.g. mhm1.0c
// mha1/mha2 — config out-of-band; mhm1/mhm2 — MHAS in-band (single / multi-stream).

export type MpeghFourCC = 'mha1' | 'mha2' | 'mhm1' | 'mhm2';
export const MPEGH_FOUR_CCS: readonly MpeghFourCC[] = ['mha1', 'mha2', 'mhm1', 'mhm2'];

// mpegh3daProfileLevelIndication values (only the ones with a confirmed mapping are named).
export function hProfileLevel(profileLevelId: number): string {
switch (profileLevelId) {
case 0x0b: return 'LC Profile Level 1';
case 0x0c: return 'LC Profile Level 2';
case 0x0d: return 'LC Profile Level 3';
case 0x10: return 'BL Profile Level 1';
case 0x11: return 'BL Profile Level 2';
case 0x12: return 'BL Profile Level 3';
default: return 'unknown';
}
}
3 changes: 3 additions & 0 deletions src/codec/mpegh/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {MPEGH_FOUR_CCS, hProfileLevel} from './enums';
export type {MpeghFourCC} from './enums';
export * from './mpegh-info';
63 changes: 63 additions & 0 deletions src/codec/mpegh/mpegh-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {MpeghInfo} from "./mpegh-info";

describe('MpeghInfo', () => {
describe('parse / round-trip', () => {
it.each([
'mhm1.0c',
'mhm2.12',
'mha1.0b',
'mha2.0d',
'mhm1.10',
])('parses and round-trips %s', (str) => {
expect(MpeghInfo.fromString(str).toString()).toBe(str);
});

it('decodes the fields', () => {
const info = MpeghInfo.fromString('mhm1.0c');
expect(info.fourCC).toBe('mhm1');
expect(info.profileLevelId).toBe(0x0c);
});

it('normalizes hex to lowercase', () => {
expect(MpeghInfo.fromString('mhm1.0C').toString()).toBe('mhm1.0c');
});
});

describe('human-readable', () => {
it.each([
['mhm1.0b', 'LC Profile Level 1'],
['mhm1.0c', 'LC Profile Level 2'],
['mhm1.0d', 'LC Profile Level 3'],
['mhm1.10', 'BL Profile Level 1'],
['mhm1.12', 'BL Profile Level 3'],
])('%s -> %s', (str, profileLevel) => {
expect(MpeghInfo.fromString(str).toHumanReadable().profileLevel).toBe(profileLevel);
});
});

describe('build', () => {
it('assembles a codec string from parts', () => {
const info = new MpeghInfo();
info.fourCC = 'mhm1';
info.profileLevelId = 0x0c;
expect(info.toString()).toBe('mhm1.0c');
});

it('rejects an out-of-range profile-level id', () => {
expect(() => { new MpeghInfo().profileLevelId = 256; }).toThrow('mpegh3daProfileLevelIndication');
});
});

describe('invalid input', () => {
it.each(['mhm1', 'mhm1.0c.1', 'mhm1.gg'])(
'rejects malformed string %s',
(str) => {
expect(() => MpeghInfo.fromString(str)).toThrow('Invalid MPEG-H codec string');
},
);

it('rejects an unknown 4CC', () => {
expect(() => MpeghInfo.fromString('mhx1.0c')).toThrow('Unknown codec');
});
});
});
60 changes: 60 additions & 0 deletions src/codec/mpegh/mpegh-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {CodecInfo} from "../codec-info";
import {padStart} from "../pad-start";
import {MPEGH_FOUR_CCS, MpeghFourCC, hProfileLevel} from "./enums";

// MPEG-H 3D Audio codec information. Codec string: <fourCC>.<profileLevelId> (one hex byte).
export class MpeghInfo extends CodecInfo {
codecName = 'mpegh';

private _fourCC: MpeghFourCC = 'mhm1';
private _profileLevelId = 0;

get fourCC(): MpeghFourCC {
return this._fourCC;
}

set fourCC(fourCC: MpeghFourCC) {
if (!MPEGH_FOUR_CCS.includes(fourCC)) {
throw new Error(`Invalid MPEG-H sample entry: ${fourCC}`);
}
this._fourCC = fourCC;
}

get profileLevelId(): number {
return this._profileLevelId;
}

set profileLevelId(profileLevelId: number) {
if (!Number.isInteger(profileLevelId) || profileLevelId < 0 || profileLevelId > 255) {
throw new Error('mpegh3daProfileLevelIndication must be a byte in the range 0-255');
}
this._profileLevelId = profileLevelId;
}

static fromString(codecString: string): MpeghInfo {
const parts = codecString.split('.');
const fourCC = parts[0] as MpeghFourCC;
if (!MPEGH_FOUR_CCS.includes(fourCC)) {
throw new Error('Unknown codec');
}
if (parts.length !== 2 || !/^[0-9a-fA-F]{1,2}$/.test(parts[1])) {
throw new Error('Invalid MPEG-H codec string, expected <fourCC>.<profileLevelId>');
}
const info = new MpeghInfo();
info.fourCC = fourCC;
info.profileLevelId = parseInt(parts[1], 16);
return info;
}

toString(): string {
return `${this._fourCC}.${padStart(this._profileLevelId.toString(16), 2, '0')}`;
}

toHumanReadable() {
return {
fourCC: this._fourCC,
profileLevel: hProfileLevel(this._profileLevelId),
profileLevelId: this._profileLevelId,
} as const;
}
}
9 changes: 8 additions & 1 deletion src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3} from './index';
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3, mpegh} from './index';

describe('codecInfoFactory', () => {
it('dispatches vp8 strings to Vp8Info', () => {
Expand Down Expand Up @@ -92,6 +92,13 @@ describe('codecInfoFactory', () => {
expect(info.codecName).toBe('av3a');
});

it('dispatches mha/mhm strings to MpeghInfo', () => {
const info = codecInfoFactory('mhm1.0c');
expect(info).toBeInstanceOf(mpegh.MpeghInfo);
expect(info.codecName).toBe('mpegh');
expect((info as mpegh.MpeghInfo).profileLevelId).toBe(0x0c);
});

it('throws on an unknown codec', () => {
expect(() => codecInfoFactory('theora')).toThrow('Unknown codec');
expect(() => codecInfoFactory('tx3g')).toThrow('Unknown codec');
Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {EvcInfo} from "./codec/evc";
import {LhevcInfo} from "./codec/lhevc";
import {Mp4Info} from "./codec/mp4";
import {Avs3VideoInfo, Avs3AudioInfo} from "./codec/avs3";
import {MpeghInfo} from "./codec/mpegh";

export * as vpx from "./codec/vpx";
export * as av1 from "./codec/av1";
Expand All @@ -21,6 +22,7 @@ export * as evc from "./codec/evc";
export * as lhevc from "./codec/lhevc";
export * as mp4 from "./codec/mp4";
export * as avs3 from "./codec/avs3";
export * as mpegh from "./codec/mpegh";
export * from './codec/codec-info';

export const version = '__lib_version__'; // Version will be injected on the build
Expand Down Expand Up @@ -62,5 +64,8 @@ export const codecInfoFactory = (codecString: string) => {
if (codecString.startsWith('av3a')) {
return Avs3AudioInfo.fromString(codecString);
}
if (codecString.startsWith('mha') || codecString.startsWith('mhm')) {
return MpeghInfo.fromString(codecString);
}
throw new Error('Unknown codec');
}
Loading