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

## Install

Expand Down Expand Up @@ -100,11 +101,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 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.
`mp4a`/`mp4v`, `avs3`, `av3a`, `mha1`/`mha2`/`mhm1`/`mhm2`, `uncv`/`unci`) 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
`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 Down Expand Up @@ -132,6 +133,8 @@ console.log(info.toString()); // 'avc1.640028'
- `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`).
- `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`,
`MatrixCoefficients`, `VideoFullRangeFlag`) are re-exported from both namespaces.

Expand Down
31 changes: 31 additions & 0 deletions src/codec/uncv/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Uncompressed video / images (ISO/IEC 23001-17), sample entries uncv (video) and unci (image).
// Codec string: <fourCC>[.<profile>] where <profile> is a registered uncompressed-video profile
// 4CC (mp4ra.org/registered-types/uncv-profiles), e.g. uncv.rgba, uncv.i420. Bare uncv is the
// generic (non-profiled) form.

export type UncvFourCC = 'uncv' | 'unci';
export const UNCV_FOUR_CCS: readonly UncvFourCC[] = ['uncv', 'unci'];

const UNCV_PROFILES: Readonly<Record<string, string>> = {
'2vuy': '8-bit YUV 4:2:2 (Cb Y0 Cr Y1)',
'yuv2': '8-bit YUV 4:2:2 (Y0 Cb Y1 Cr)',
'yvyu': '8-bit YUV 4:2:2 (Y0 Cr Y1 Cb)',
'vyuy': '8-bit YUV 4:2:2 (Cr Y0 Cb Y1)',
'yuv1': '8-bit YUV 4:1:1 (Y0 Y1 Cb Y2 Y3 Cr)',
'v308': '8-bit YUV 4:4:4 (Cr Y Cb)',
'v408': '8-bit YUVA 4:4:4:4 (Cb Y Cr A)',
'v216': 'YUV 4:2:2 (10-16 bit)',
'v210': '10-bit YUV 4:2:2',
'v410': '10-bit YUV 4:4:4',
'y210': '10-bit YUV 4:2:2 (little-endian)',
'i420': 'YUV 4:2:0 8-bit planar',
'nv12': 'YUV 4:2:0 8-bit semi-planar (Y CbCr)',
'nv21': 'YUV 4:2:0 8-bit semi-planar (Y CrCb)',
'rgb3': 'RGB 24-bit packed',
'rgba': 'RGBA 32-bit packed',
'abgr': 'ABGR 32-bit packed',
};

export function hProfile(profile: string): string {
return UNCV_PROFILES[profile.toLowerCase()] ?? 'unknown';
}
3 changes: 3 additions & 0 deletions src/codec/uncv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {UNCV_FOUR_CCS, hProfile} from './enums';
export type {UncvFourCC} from './enums';
export * from './uncv-info';
83 changes: 83 additions & 0 deletions src/codec/uncv/uncv-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {UncvInfo} from "./uncv-info";

describe('UncvInfo', () => {
describe('parse / round-trip', () => {
it.each([
'uncv',
'uncv.rgba',
'uncv.i420',
'uncv.2vuy',
'uncv.v210',
'unci.rgb3',
'uncv.abcd',
])('parses and round-trips %s', (str) => {
expect(UncvInfo.fromString(str).toString()).toBe(str);
});

it('decodes the fields', () => {
const info = UncvInfo.fromString('uncv.rgba');
expect(info.fourCC).toBe('uncv');
expect(info.profile).toBe('rgba');
});

it('parses the bare generic form', () => {
const info = UncvInfo.fromString('uncv');
expect(info.profile).toBeUndefined();
});

it('sets codecName from the sample entry', () => {
expect(UncvInfo.fromString('unci.rgb3').codecName).toBe('unci');
});
});

describe('human-readable', () => {
it.each([
['uncv.rgba', 'rgba', 'RGBA 32-bit packed'],
['uncv.i420', 'i420', 'YUV 4:2:0 8-bit planar'],
['uncv.v210', 'v210', '10-bit YUV 4:2:2'],
['uncv.abcd', 'abcd', 'unknown'],
])('%s -> %s / %s', (str, profile, format) => {
const hr = UncvInfo.fromString(str).toHumanReadable();
expect(hr.profile).toBe(profile);
expect(hr.format).toBe(format);
});

it('reports the generic form', () => {
expect(UncvInfo.fromString('uncv').toHumanReadable()).toEqual({
fourCC: 'uncv',
profile: 'none',
format: 'generic',
});
});
});

describe('build', () => {
it('assembles a profiled codec string', () => {
const info = new UncvInfo();
info.fourCC = 'uncv';
info.profile = 'rgba';
expect(info.toString()).toBe('uncv.rgba');
});

it('assembles the generic form', () => {
expect(new UncvInfo().toString()).toBe('uncv');
});

it('rejects a profile that is not four characters', () => {
expect(() => { new UncvInfo().profile = 'rgb'; }).toThrow('four-character code');
});
});

describe('invalid input', () => {
it.each(['uncv.rgba.x', 'uncv.rgb'])(
'rejects malformed string %s',
(str) => {
expect(() => UncvInfo.fromString(str)).toThrow('Invalid uncv');
},
);

it('rejects an unknown 4CC', () => {
expect(() => UncvInfo.fromString('uncx.rgba')).toThrow('Unknown codec');
});
});
});
66 changes: 66 additions & 0 deletions src/codec/uncv/uncv-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {CodecInfo} from "../codec-info";
import {UNCV_FOUR_CCS, UncvFourCC, hProfile} from "./enums";

// Uncompressed video/image codec information. Codec string: <fourCC>[.<profile 4CC>].
export class UncvInfo extends CodecInfo {
codecName = 'uncv';

private _fourCC: UncvFourCC = 'uncv';
private _profile: string | undefined = undefined;

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

set fourCC(fourCC: UncvFourCC) {
if (!UNCV_FOUR_CCS.includes(fourCC)) {
throw new Error(`Invalid uncompressed sample entry: ${fourCC}`);
}
this._fourCC = fourCC;
this.codecName = fourCC;
}

// The uncompressed-video profile 4CC (e.g. "rgba"), or undefined for the generic form.
get profile(): string | undefined {
return this._profile;
}

set profile(profile: string | undefined) {
if (profile !== undefined && !/^[0-9a-zA-Z]{4}$/.test(profile)) {
throw new Error('uncv profile must be a four-character code');
}
this._profile = profile;
}

static fromString(codecString: string): UncvInfo {
const parts = codecString.split('.');
const fourCC = parts[0] as UncvFourCC;
if (!UNCV_FOUR_CCS.includes(fourCC)) {
throw new Error('Unknown codec');
}
if (parts.length > 2) {
throw new Error('Invalid uncv codec string, expected <fourCC>[.<profile>]');
}
const info = new UncvInfo();
info.fourCC = fourCC;
if (parts[1] !== undefined) {
if (!/^[0-9a-zA-Z]{4}$/.test(parts[1])) {
throw new Error('Invalid uncv profile, expected a four-character code');
}
info.profile = parts[1];
}
return info;
}

toString(): string {
return this._profile !== undefined ? `${this._fourCC}.${this._profile}` : this._fourCC;
}

toHumanReadable() {
return {
fourCC: this._fourCC,
profile: this._profile ?? 'none',
format: this._profile !== undefined ? hProfile(this._profile) : 'generic',
} as const;
}
}
10 changes: 9 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} from './index';
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3, mpegh, simple, uncv} from './index';

describe('codecInfoFactory', () => {
it('dispatches vp8 strings to Vp8Info', () => {
Expand Down Expand Up @@ -109,6 +109,14 @@ describe('codecInfoFactory', () => {
expect(codecInfoFactory('vc-1').codecName).toBe('vc-1');
});

it('dispatches uncv/unci strings to UncvInfo', () => {
const info = codecInfoFactory('uncv.rgba');
expect(info).toBeInstanceOf(uncv.UncvInfo);
expect(info.codecName).toBe('uncv');
expect((info as uncv.UncvInfo).profile).toBe('rgba');
expect(codecInfoFactory('uncv')).toBeInstanceOf(uncv.UncvInfo);
});

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

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

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