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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Useful for reading codec parameters out of a `codecs=` string, checking support
| MPEG-H 3D Audio (mha1/mhm1) | ✅ | ✅ |
| Parameterless (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) | ✅ | ✅ |
| Uncompressed (uncv/unci) | ✅ | ✅ |
| Subtitles (WebVTT wvtt, TTML stpp) | ✅ | ✅ |

## Install

Expand Down Expand Up @@ -102,7 +103,7 @@ 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`, `cavs`, `mha1`/`mha2`/`mhm1`/`mhm2`, `uncv`/`unci`) and returns the matching
`mp4a`/`mp4v`, `avs3`, `av3a`, `cavs`, `mha1`/`mha2`/`mhm1`/`mhm2`, `uncv`/`unci`, `stpp`) and returns the matching
info object (`Vp8Info`, `Vp9Info`, `Av1Info`, `H264Info`, `H265Info`, `H266Info`, `LcevcInfo`,
`ApvInfo`, `EvcInfo`, `LhevcInfo`, `Mp4Info`, `Avs3VideoInfo`, `Avs3AudioInfo`, `MpeghInfo`,
`UncvInfo`). Recognised parameterless 4CCs (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) return a
Expand Down Expand Up @@ -130,11 +131,14 @@ console.log(info.toString()); // 'avc1.640028'
- `avs3` — namespace exporting `Avs3VideoInfo` (`avs3.<profile>.<level>`) and `Avs3AudioInfo`
(`av3a.<codec_id>`) for the AVS3 video/audio standard.
- `avs2` — namespace exporting `Avs2AudioInfo` for AVS2 audio (`cavs.<audio_codec_id>`).
- `stpp` — namespace exporting `StppInfo` for TTML / timed-text subtitles
(`stpp[.<mode>[.<profile>]]`, e.g. `stpp.ttml.im1t`), with the TTML profile decoded to a
description. WebVTT (`wvtt`) is parameterless and handled by `SimpleCodecInfo`.
- `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`).
parameterless 4CCs (DTS `dtsc`/`dtse`/…, `vc-1`, `opus`, `flac`, `vorbis`, `alac`, `wvtt` (WebVTT),
and PCM variants such as `ipcm`/`fpcm`/`twos`/`sowt`).
- `uncv` — namespace exporting `UncvInfo` for uncompressed video/images (`uncv`/`unci`), with an
optional profile 4CC (`uncv.rgba`, `uncv.i420`, …) decoded to a pixel-format description.
- Shared ISO/IEC 23001-8:2016 colour enums (`ColourPrimaries`, `TransferCharacteristics`,
Expand Down
2 changes: 2 additions & 0 deletions src/codec/simple/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const SIMPLE_CODECS: Readonly<Record<string, string>> = {
'flac': 'FLAC',
'vorbis': 'Vorbis',
'alac': 'Apple Lossless (ALAC)',
// WebVTT subtitles (parameterless; TTML/stpp is handled separately as it carries a profile)
'wvtt': 'WebVTT',
// PCM variants
'ipcm': 'Uncompressed PCM',
'fpcm': 'Floating-point PCM',
Expand Down
3 changes: 2 additions & 1 deletion src/codec/simple/simple-codec-info.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('SimpleCodecInfo', () => {
it.each([
'opus', 'flac', 'vorbis', 'alac',
'dtsc', 'dtse', 'dtsh', 'dtsl', 'dtsx', 'dtsy',
'vc-1',
'vc-1', 'wvtt',
'twos', 'sowt', 'lpcm', 'ipcm', 'fpcm', 'in24', 'in32', 'fl32', 'fl64',
])('parses and round-trips %s', (str) => {
expect(SimpleCodecInfo.fromString(str).toString()).toBe(str);
Expand Down Expand Up @@ -33,6 +33,7 @@ describe('SimpleCodecInfo', () => {
['vc-1', 'SMPTE VC-1'],
['twos', 'PCM (big-endian)'],
['sowt', 'PCM (little-endian)'],
['wvtt', 'WebVTT'],
])('%s -> %s', (str, name) => {
expect(SimpleCodecInfo.fromString(str).toHumanReadable().codec).toBe(name);
});
Expand Down
35 changes: 35 additions & 0 deletions src/codec/stpp/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// TTML / XML timed-text subtitles, sample entry stpp (ISO/IEC 14496-30). Codec string:
// stpp[.<mode>[.<profile>]] e.g. stpp.ttml.im1t (IMSC1 text), stpp.ttml.etx1 (EBU-TT v1.0)
// The only defined mode is "ttml"; the profile is a registered TTML profile designator.

export type StppFourCC = 'stpp';
export const STPP_FOUR_CCS: readonly StppFourCC[] = ['stpp'];

const STPP_TTML_PROFILES: Readonly<Record<string, string>> = {
'im1t': 'IMSC1 text',
'im1i': 'IMSC1 image',
'im2t': 'IMSC2 text',
'im2i': 'IMSC2 image',
'tt1f': 'TTML1 full',
'tt1p': 'TTML1 presentation',
'tt1s': 'TTML1 simple delivery (US closed captions)',
'tt1t': 'TTML1 transformation',
'tt2f': 'TTML2 full',
'tt2p': 'TTML2 presentation',
'tt2t': 'TTML2 transformation',
'etx1': 'EBU Subtitling Format v1.0',
'etx2': 'EBU Subtitling Format v1.1',
'etx3': 'EBU Subtitling Format v1.2',
'etd1': 'EBU-TT Distribution v1.0',
'etd2': 'EBU-TT Distribution v1.0.1',
'etl1': 'EBU-TT Live',
'ede1': 'IRT EBU-TT-D',
'cfi1': 'DECE Image Subtitle Profile',
'cft1': 'DECE Test Subtitle Profile',
};

export function hProfile(mode: string | undefined, profile: string | undefined): string {
if (profile === undefined) return 'none';
if (mode === 'ttml') return STPP_TTML_PROFILES[profile.toLowerCase()] ?? 'unknown';
return 'unknown';
}
3 changes: 3 additions & 0 deletions src/codec/stpp/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {STPP_FOUR_CCS, hProfile} from './enums';
export type {StppFourCC} from './enums';
export * from './stpp-info';
73 changes: 73 additions & 0 deletions src/codec/stpp/stpp-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {StppInfo} from "./stpp-info";

describe('StppInfo', () => {
describe('parse / round-trip', () => {
it.each([
'stpp',
'stpp.ttml',
'stpp.ttml.im1t',
'stpp.ttml.etx1',
'stpp.ttml.tt2f',
'stpp.ttml.abcd',
])('parses and round-trips %s', (str) => {
expect(StppInfo.fromString(str).toString()).toBe(str);
});

it('decodes the mode and profile', () => {
const info = StppInfo.fromString('stpp.ttml.im1t');
expect(info.mode).toBe('ttml');
expect(info.profile).toBe('im1t');
});

it('parses the bare and mode-only forms', () => {
expect(StppInfo.fromString('stpp').mode).toBeUndefined();
const modeOnly = StppInfo.fromString('stpp.ttml');
expect(modeOnly.mode).toBe('ttml');
expect(modeOnly.profile).toBeUndefined();
});
});

describe('human-readable', () => {
it.each([
['stpp.ttml.im1t', 'IMSC1 text'],
['stpp.ttml.im1i', 'IMSC1 image'],
['stpp.ttml.tt2f', 'TTML2 full'],
['stpp.ttml.etx1', 'EBU Subtitling Format v1.0'],
['stpp.ttml.abcd', 'unknown'],
])('%s -> %s', (str, description) => {
expect(StppInfo.fromString(str).toHumanReadable().description).toBe(description);
});

it('reports the bare form', () => {
expect(StppInfo.fromString('stpp').toHumanReadable()).toEqual({
fourCC: 'stpp',
mode: 'none',
profile: 'none',
description: 'none',
});
});
});

describe('build', () => {
it('assembles a codec string from parts', () => {
const info = new StppInfo();
info.mode = 'ttml';
info.profile = 'im1t';
expect(info.toString()).toBe('stpp.ttml.im1t');
});

it('requires a mode before a profile', () => {
expect(() => { new StppInfo().profile = 'im1t'; }).toThrow('requires a mode');
});
});

describe('invalid input', () => {
it('rejects an empty mode', () => {
expect(() => StppInfo.fromString('stpp..im1t')).toThrow('Invalid stpp mode');
});

it('rejects an unknown 4CC', () => {
expect(() => StppInfo.fromString('sbtt.ttml')).toThrow('Unknown codec');
});
});
});
78 changes: 78 additions & 0 deletions src/codec/stpp/stpp-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {CodecInfo} from "../codec-info";
import {hProfile} from "./enums";

// TTML / timed-text (stpp) codec information. Codec string: stpp[.<mode>[.<profile>]].
export class StppInfo extends CodecInfo {
codecName = 'stpp';

private _mode: string | undefined = undefined;
private _profile: string | undefined = undefined;

// The subtitle mode, e.g. "ttml" (undefined for a bare "stpp").
get mode(): string | undefined {
return this._mode;
}

set mode(mode: string | undefined) {
if (mode !== undefined && !/^[a-zA-Z0-9]+$/.test(mode)) {
throw new Error('stpp mode must be an alphanumeric token');
}
this._mode = mode;
if (mode === undefined) {
this._profile = undefined;
}
}

// The TTML profile designator, e.g. "im1t" (undefined when absent).
get profile(): string | undefined {
return this._profile;
}

set profile(profile: string | undefined) {
if (profile === undefined) {
this._profile = undefined;
return;
}
if (this._mode === undefined) {
throw new Error('stpp profile requires a mode (e.g. "ttml") to be set first');
}
if (!/^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$/.test(profile)) {
throw new Error('stpp profile must be an alphanumeric designator');
}
this._profile = profile;
}

static fromString(codecString: string): StppInfo {
const parts = codecString.split('.');
if (parts[0] !== 'stpp') {
throw new Error('Unknown codec');
}
const info = new StppInfo();
if (parts[1] !== undefined) {
if (!/^[a-zA-Z0-9]+$/.test(parts[1])) {
throw new Error('Invalid stpp mode');
}
info.mode = parts[1];
}
if (parts.length > 2) {
info.profile = parts.slice(2).join('.');
}
return info;
}

toString(): string {
const parts = ['stpp'];
if (this._mode !== undefined) parts.push(this._mode);
if (this._profile !== undefined) parts.push(this._profile);
return parts.join('.');
}

toHumanReadable() {
return {
fourCC: 'stpp' as const,
mode: this._mode ?? 'none',
profile: this._profile ?? 'none',
description: hProfile(this._mode, this._profile),
} as const;
}
}
15 changes: 14 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, simple, uncv, avs2} from './index';
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3, mpegh, simple, uncv, avs2, stpp} from './index';

describe('codecInfoFactory', () => {
it('dispatches vp8 strings to Vp8Info', () => {
Expand Down Expand Up @@ -124,6 +124,19 @@ describe('codecInfoFactory', () => {
expect((info as avs2.Avs2AudioInfo).audioCodecId).toBe(1);
});

it('dispatches stpp strings to StppInfo', () => {
const info = codecInfoFactory('stpp.ttml.im1t');
expect(info).toBeInstanceOf(stpp.StppInfo);
expect(info.codecName).toBe('stpp');
expect((info as stpp.StppInfo).profile).toBe('im1t');
});

it('dispatches wvtt to SimpleCodecInfo', () => {
const info = codecInfoFactory('wvtt');
expect(info).toBeInstanceOf(simple.SimpleCodecInfo);
expect((info as simple.SimpleCodecInfo).toHumanReadable().codec).toBe('WebVTT');
});

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 @@ -13,6 +13,7 @@ import {MpeghInfo} from "./codec/mpegh";
import {SimpleCodecInfo, isSimpleCodec} from "./codec/simple";
import {UncvInfo} from "./codec/uncv";
import {Avs2AudioInfo} from "./codec/avs2";
import {StppInfo} from "./codec/stpp";

export * as vpx from "./codec/vpx";
export * as av1 from "./codec/av1";
Expand All @@ -29,6 +30,7 @@ export * as mpegh from "./codec/mpegh";
export * as simple from "./codec/simple";
export * as uncv from "./codec/uncv";
export * as avs2 from "./codec/avs2";
export * as stpp from "./codec/stpp";
export * from './codec/codec-info';

export const version = '__lib_version__'; // Version will be injected on the build
Expand Down Expand Up @@ -79,6 +81,9 @@ export const codecInfoFactory = (codecString: string) => {
if (codecString.startsWith('uncv') || codecString.startsWith('unci')) {
return UncvInfo.fromString(codecString);
}
if (codecString.startsWith('stpp')) {
return StppInfo.fromString(codecString);
}
if (isSimpleCodec(codecString)) {
return SimpleCodecInfo.fromString(codecString);
}
Expand Down
Loading