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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | ✅ | ✅ |
| AVS2 audio (cavs) | ✅ | ✅ |
| MPEG-H 3D Audio (mha1/mhm1) | ✅ | ✅ |
| Parameterless (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) | ✅ | ✅ |
| Uncompressed (uncv/unci) | ✅ | ✅ |
Expand Down Expand Up @@ -101,7 +102,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`, `mha1`/`mha2`/`mhm1`/`mhm2`, `uncv`/`unci`) and returns the matching
`mp4a`/`mp4v`, `avs3`, `av3a`, `cavs`, `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
Expand All @@ -128,6 +129,7 @@ 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.<profile>.<level>`) and `Avs3AudioInfo`
(`av3a.<codec_id>`) for the AVS3 video/audio standard.
- `avs2` — namespace exporting `Avs2AudioInfo` for AVS2 audio (`cavs.<audio_codec_id>`).
- `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
Expand Down
49 changes: 49 additions & 0 deletions src/codec/avs2/avs2-audio-info.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {Avs2AudioInfo} from "./avs2-audio-info";

describe('Avs2AudioInfo', () => {
describe('parse / round-trip', () => {
it.each(['cavs.00', 'cavs.01'])('parses and round-trips %s', (str) => {
expect(Avs2AudioInfo.fromString(str).toString()).toBe(str);
});

it('decodes the audio codec id', () => {
const info = Avs2AudioInfo.fromString('cavs.01');
expect(info.fourCC).toBe('cavs');
expect(info.audioCodecId).toBe(1);
});
});

describe('human-readable', () => {
it.each([
['cavs.00', 'General Audio Coding'],
['cavs.01', 'Lossless Audio Coding'],
])('%s -> %s', (str, codec) => {
expect(Avs2AudioInfo.fromString(str).toHumanReadable().codec).toBe(codec);
});
});

describe('build', () => {
it('assembles a codec string from parts', () => {
const info = new Avs2AudioInfo();
info.audioCodecId = 1;
expect(info.toString()).toBe('cavs.01');
});

it('rejects out-of-range bytes', () => {
expect(() => { new Avs2AudioInfo().audioCodecId = 256; }).toThrow('audio_codec_id');
});
});

describe('invalid input', () => {
it.each(['cavs', 'cavs.00.01', 'cavs.GG'])(
'rejects malformed string %s',
(str) => {
expect(() => Avs2AudioInfo.fromString(str)).toThrow('Invalid AVS2 audio codec string');
},
);

it('rejects an unknown 4CC', () => {
expect(() => Avs2AudioInfo.fromString('cav2.00')).toThrow('Unknown codec');
});
});
});
64 changes: 64 additions & 0 deletions src/codec/avs2/avs2-audio-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {CodecInfo} from "../codec-info";
import {padStart} from "../pad-start";
import {AVS2_AUDIO_FOUR_CCS, Avs2AudioFourCC, hAudioCodec} from "./enums";

function assertByte(value: number, name: string): number {
if (!Number.isInteger(value) || value < 0 || value > 255) {
throw new Error(`${name} must be a byte in the range 0-255`);
}
return value;
}

// AVS2 audio codec information. Codec string: cavs.<audio_codec_id> (one hex byte).
export class Avs2AudioInfo extends CodecInfo {
codecName = 'cavs';

private _fourCC: Avs2AudioFourCC = 'cavs';
private _audioCodecId = 0;

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

set fourCC(fourCC: Avs2AudioFourCC) {
if (!AVS2_AUDIO_FOUR_CCS.includes(fourCC)) {
throw new Error(`Invalid AVS2 audio sample entry: ${fourCC}`);
}
this._fourCC = fourCC;
}

get audioCodecId(): number {
return this._audioCodecId;
}

set audioCodecId(audioCodecId: number) {
this._audioCodecId = assertByte(audioCodecId, 'audio_codec_id');
}

static fromString(codecString: string): Avs2AudioInfo {
const parts = codecString.split('.');
const fourCC = parts[0] as Avs2AudioFourCC;
if (!AVS2_AUDIO_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 AVS2 audio codec string, expected cavs.<audio_codec_id>');
}
const info = new Avs2AudioInfo();
info.fourCC = fourCC;
info.audioCodecId = parseInt(parts[1], 16);
return info;
}

toString(): string {
return `${this._fourCC}.${padStart(this._audioCodecId.toString(16), 2, '0')}`;
}

toHumanReadable() {
return {
fourCC: this._fourCC,
codec: hAudioCodec(this._audioCodecId),
audioCodecId: this._audioCodecId,
} as const;
}
}
13 changes: 13 additions & 0 deletions src/codec/avs2/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// AVS2 audio (China AVS working group). Codec string: cavs.<audio_codec_id> (one hex byte).
// e.g. cavs.00 (General Audio Coding), cavs.01 (Lossless Audio Coding).

export type Avs2AudioFourCC = 'cavs';
export const AVS2_AUDIO_FOUR_CCS: readonly Avs2AudioFourCC[] = ['cavs'];

export function hAudioCodec(audioCodecId: number): string {
switch (audioCodecId) {
case 0x00: return 'General Audio Coding';
case 0x01: return 'Lossless Audio Coding';
default: return 'unknown';
}
}
3 changes: 3 additions & 0 deletions src/codec/avs2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {AVS2_AUDIO_FOUR_CCS, hAudioCodec} from './enums';
export type {Avs2AudioFourCC} from './enums';
export * from './avs2-audio-info';
9 changes: 8 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} from './index';
import {codecInfoFactory, version, vpx, av1, h264, h265, h266, lcevc, apv, evc, lhevc, mp4, avs3, mpegh, simple, uncv, avs2} from './index';

describe('codecInfoFactory', () => {
it('dispatches vp8 strings to Vp8Info', () => {
Expand Down Expand Up @@ -117,6 +117,13 @@ describe('codecInfoFactory', () => {
expect(codecInfoFactory('uncv')).toBeInstanceOf(uncv.UncvInfo);
});

it('dispatches cavs strings to Avs2AudioInfo', () => {
const info = codecInfoFactory('cavs.01');
expect(info).toBeInstanceOf(avs2.Avs2AudioInfo);
expect(info.codecName).toBe('cavs');
expect((info as avs2.Avs2AudioInfo).audioCodecId).toBe(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 @@ -12,6 +12,7 @@ import {Avs3VideoInfo, Avs3AudioInfo} from "./codec/avs3";
import {MpeghInfo} from "./codec/mpegh";
import {SimpleCodecInfo, isSimpleCodec} from "./codec/simple";
import {UncvInfo} from "./codec/uncv";
import {Avs2AudioInfo} from "./codec/avs2";

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

export const version = '__lib_version__'; // Version will be injected on the build
Expand Down Expand Up @@ -68,6 +70,9 @@ export const codecInfoFactory = (codecString: string) => {
if (codecString.startsWith('av3a')) {
return Avs3AudioInfo.fromString(codecString);
}
if (codecString.startsWith('cavs')) {
return Avs2AudioInfo.fromString(codecString);
}
if (codecString.startsWith('mha') || codecString.startsWith('mhm')) {
return MpeghInfo.fromString(codecString);
}
Expand Down
Loading