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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Useful for reading codec parameters out of a `codecs=` string, checking support
| MP4 (mp4a/mp4v — AAC, MP3, MPEG-4/2) | ✅ | ✅ |
| AVS3 video/audio (avs3/av3a) | ✅ | ✅ |
| MPEG-H 3D Audio (mha1/mhm1) | ✅ | ✅ |
| Parameterless (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) | ✅ | ✅ |

## Install

Expand Down Expand Up @@ -99,9 +100,11 @@ 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`, `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.
`mp4a`/`mp4v`, `avs3`, `av3a`, `mha1`/`mha2`/`mhm1`/`mhm2`) and returns the matching info object
(`Vp8Info`, `Vp9Info`, `Av1Info`, `H264Info`, `H265Info`, `H266Info`, `LcevcInfo`, `ApvInfo`,
`EvcInfo`, `LhevcInfo`, `Mp4Info`, `Avs3VideoInfo`, `Avs3AudioInfo`, `MpeghInfo`). Recognised
parameterless 4CCs (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) return a `SimpleCodecInfo`. 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 @@ -126,6 +129,9 @@ console.log(info.toString()); // 'avc1.640028'
(`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`).
- `simple` — namespace exporting `SimpleCodecInfo` and the `SIMPLE_CODECS` registry for
parameterless 4CCs (DTS `dtsc`/`dtse`/…, `vc-1`, `opus`, `flac`, `vorbis`, `alac`, and PCM
variants such as `ipcm`/`fpcm`/`twos`/`sowt`).
- Shared ISO/IEC 23001-8:2016 colour enums (`ColourPrimaries`, `TransferCharacteristics`,
`MatrixCoefficients`, `VideoFullRangeFlag`) are re-exported from both namespaces.

Expand Down
37 changes: 37 additions & 0 deletions src/codec/simple/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Parameterless codecs: the codecs= string is just the sample-entry 4CC, with no dot-separated
// parameters to parse. Each recognised 4CC maps to a human-readable name.

export const SIMPLE_CODECS: Readonly<Record<string, string>> = {
// DTS / DTS-HD / DTS:X (a separate company, not Dolby)
'dtsc': 'DTS-HD Core',
'dtse': 'DTS Express',
'dtsh': 'DTS-HD (with core)',
'dtsl': 'DTS-HD Lossless',
'dtsx': 'DTS:X (Profile 2)',
'dtsy': 'DTS:X (Profile 3)',
// VC-1
'vc-1': 'SMPTE VC-1',
// Lossy / lossless audio
'opus': 'Opus',
'flac': 'FLAC',
'vorbis': 'Vorbis',
'alac': 'Apple Lossless (ALAC)',
// PCM variants
'ipcm': 'Uncompressed PCM',
'fpcm': 'Floating-point PCM',
'twos': 'PCM (big-endian)',
'sowt': 'PCM (little-endian)',
'lpcm': 'Linear PCM',
'in24': 'PCM 24-bit',
'in32': 'PCM 32-bit',
'fl32': 'PCM 32-bit float',
'fl64': 'PCM 64-bit float',
};

export function isSimpleCodec(codecString: string): boolean {
return codecString.toLowerCase() in SIMPLE_CODECS;
}

export function hSimpleCodec(fourCC: string): string {
return SIMPLE_CODECS[fourCC.toLowerCase()] ?? 'unknown';
}
2 changes: 2 additions & 0 deletions src/codec/simple/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export {SIMPLE_CODECS, isSimpleCodec, hSimpleCodec} from './enums';
export * from './simple-codec-info';
60 changes: 60 additions & 0 deletions src/codec/simple/simple-codec-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {SimpleCodecInfo} from "./simple-codec-info";

describe('SimpleCodecInfo', () => {
describe('parse / round-trip', () => {
it.each([
'opus', 'flac', 'vorbis', 'alac',
'dtsc', 'dtse', 'dtsh', 'dtsl', 'dtsx', 'dtsy',
'vc-1',
'twos', 'sowt', 'lpcm', 'ipcm', 'fpcm', 'in24', 'in32', 'fl32', 'fl64',
])('parses and round-trips %s', (str) => {
expect(SimpleCodecInfo.fromString(str).toString()).toBe(str);
});

it('preserves the input casing but lowercases codecName', () => {
const info = SimpleCodecInfo.fromString('fLaC');
expect(info.fourCC).toBe('fLaC');
expect(info.toString()).toBe('fLaC');
expect(info.codecName).toBe('flac');
});

it('accepts a case-insensitive 4CC', () => {
expect(SimpleCodecInfo.fromString('OPUS').codecName).toBe('opus');
});
});

describe('human-readable', () => {
it.each([
['opus', 'Opus'],
['flac', 'FLAC'],
['alac', 'Apple Lossless (ALAC)'],
['dtsc', 'DTS-HD Core'],
['dtsx', 'DTS:X (Profile 2)'],
['vc-1', 'SMPTE VC-1'],
['twos', 'PCM (big-endian)'],
['sowt', 'PCM (little-endian)'],
])('%s -> %s', (str, name) => {
expect(SimpleCodecInfo.fromString(str).toHumanReadable().codec).toBe(name);
});
});

describe('build', () => {
it('sets a recognised 4CC', () => {
const info = new SimpleCodecInfo();
info.fourCC = 'opus';
expect(info.toString()).toBe('opus');
expect(info.toHumanReadable().codec).toBe('Opus');
});
});

describe('invalid input', () => {
it('rejects a codec with parameters', () => {
expect(() => SimpleCodecInfo.fromString('opus.1')).toThrow('Unknown codec');
});

it('rejects an unrecognised 4CC', () => {
expect(() => SimpleCodecInfo.fromString('theora')).toThrow('Unknown codec');
expect(() => { new SimpleCodecInfo().fourCC = 'nope'; }).toThrow('Unknown codec');
});
});
});
42 changes: 42 additions & 0 deletions src/codec/simple/simple-codec-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {CodecInfo} from "../codec-info";
import {SIMPLE_CODECS, hSimpleCodec} from "./enums";

// A parameterless codec (Opus, FLAC, Vorbis, ALAC, PCM variants, DTS, VC-1): the codec string is
// exactly the 4CC, with nothing to parse beyond recognising it.
export class SimpleCodecInfo extends CodecInfo {
codecName = 'unk';

private _fourCC = '';

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

set fourCC(fourCC: string) {
if (!(fourCC.toLowerCase() in SIMPLE_CODECS)) {
throw new Error(`Unknown codec: ${fourCC}`);
}
this._fourCC = fourCC;
this.codecName = fourCC.toLowerCase();
}

static fromString(codecString: string): SimpleCodecInfo {
if (codecString.includes('.')) {
throw new Error(`Unknown codec: ${codecString}`);
}
const info = new SimpleCodecInfo();
info.fourCC = codecString;
return info;
}

toString(): string {
return this._fourCC;
}

toHumanReadable() {
return {
fourCC: this._fourCC,
codec: hSimpleCodec(this._fourCC),
} as const;
}
}
12 changes: 11 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, mpegh} from './index';
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3, mpegh, simple} from './index';

describe('codecInfoFactory', () => {
it('dispatches vp8 strings to Vp8Info', () => {
Expand Down Expand Up @@ -99,6 +99,16 @@ describe('codecInfoFactory', () => {
expect((info as mpegh.MpeghInfo).profileLevelId).toBe(0x0c);
});

it('dispatches parameterless codecs to SimpleCodecInfo', () => {
const opus = codecInfoFactory('opus');
expect(opus).toBeInstanceOf(simple.SimpleCodecInfo);
expect(opus.codecName).toBe('opus');
expect((opus as simple.SimpleCodecInfo).toHumanReadable().codec).toBe('Opus');

expect(codecInfoFactory('dtsc')).toBeInstanceOf(simple.SimpleCodecInfo);
expect(codecInfoFactory('vc-1').codecName).toBe('vc-1');
});

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 @@ -10,6 +10,7 @@ import {LhevcInfo} from "./codec/lhevc";
import {Mp4Info} from "./codec/mp4";
import {Avs3VideoInfo, Avs3AudioInfo} from "./codec/avs3";
import {MpeghInfo} from "./codec/mpegh";
import {SimpleCodecInfo, isSimpleCodec} from "./codec/simple";

export * as vpx from "./codec/vpx";
export * as av1 from "./codec/av1";
Expand All @@ -23,6 +24,7 @@ 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 * as simple from "./codec/simple";
export * from './codec/codec-info';

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