diff --git a/README.md b/README.md index de56a87..b220f43 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -126,6 +129,9 @@ console.log(info.toString()); // 'avc1.640028' (`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`). +- `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. diff --git a/src/codec/simple/enums.ts b/src/codec/simple/enums.ts new file mode 100644 index 0000000..252cf71 --- /dev/null +++ b/src/codec/simple/enums.ts @@ -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> = { + // 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'; +} diff --git a/src/codec/simple/index.ts b/src/codec/simple/index.ts new file mode 100644 index 0000000..13cc3ca --- /dev/null +++ b/src/codec/simple/index.ts @@ -0,0 +1,2 @@ +export {SIMPLE_CODECS, isSimpleCodec, hSimpleCodec} from './enums'; +export * from './simple-codec-info'; diff --git a/src/codec/simple/simple-codec-info.spec.ts b/src/codec/simple/simple-codec-info.spec.ts new file mode 100644 index 0000000..befdeb4 --- /dev/null +++ b/src/codec/simple/simple-codec-info.spec.ts @@ -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'); + }); + }); +}); diff --git a/src/codec/simple/simple-codec-info.ts b/src/codec/simple/simple-codec-info.ts new file mode 100644 index 0000000..6aa4226 --- /dev/null +++ b/src/codec/simple/simple-codec-info.ts @@ -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; + } +} diff --git a/src/index.spec.ts b/src/index.spec.ts index 0a44843..65cf473 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, 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', () => { @@ -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'); diff --git a/src/index.ts b/src/index.ts index 76657d6..5e505d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; @@ -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 @@ -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'); } \ No newline at end of file