diff --git a/README.md b/README.md index 472c931..de56a87 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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..`) and `Avs3AudioInfo` (`av3a.`) 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. diff --git a/src/codec/mpegh/enums.ts b/src/codec/mpegh/enums.ts new file mode 100644 index 0000000..4f4b574 --- /dev/null +++ b/src/codec/mpegh/enums.ts @@ -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: . 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'; + } +} diff --git a/src/codec/mpegh/index.ts b/src/codec/mpegh/index.ts new file mode 100644 index 0000000..acf7e53 --- /dev/null +++ b/src/codec/mpegh/index.ts @@ -0,0 +1,3 @@ +export {MPEGH_FOUR_CCS, hProfileLevel} from './enums'; +export type {MpeghFourCC} from './enums'; +export * from './mpegh-info'; diff --git a/src/codec/mpegh/mpegh-info.spec.ts b/src/codec/mpegh/mpegh-info.spec.ts new file mode 100644 index 0000000..56c85c8 --- /dev/null +++ b/src/codec/mpegh/mpegh-info.spec.ts @@ -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'); + }); + }); +}); diff --git a/src/codec/mpegh/mpegh-info.ts b/src/codec/mpegh/mpegh-info.ts new file mode 100644 index 0000000..e181e80 --- /dev/null +++ b/src/codec/mpegh/mpegh-info.ts @@ -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: . (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 .'); + } + 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; + } +} diff --git a/src/index.spec.ts b/src/index.spec.ts index 62ecefa..0a44843 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -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', () => { @@ -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'); diff --git a/src/index.ts b/src/index.ts index a984c2a..76657d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; @@ -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 @@ -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'); } \ No newline at end of file