From 846179ccb965c5d4cebc495150402ce762736843 Mon Sep 17 00:00:00 2001 From: Igor Sheko Date: Wed, 15 Jul 2026 21:46:40 +0100 Subject: [PATCH] feat: add tx3g, AMR, CEA-608/708, DRA and lav3 codec string support Add the remaining niche non-Dolby codecs. Parameterless (via SimpleCodecInfo): tx3g (3GPP timed text), samr/sawb (AMR / AMR-WB), c608/c708 (CEA-608/708 closed captions), dra1 (DRA audio). Also accept the AVS3 library-track sample entry lav3, which shares the avs3 profile/level format (Avs3VideoInfo now reports codecName from the 4CC). The 'unknown codec' test example moved off tx3g. --- README.md | 10 +++++----- src/codec/avs3/avs3-video-info.spec.ts | 7 +++++++ src/codec/avs3/avs3-video-info.ts | 1 + src/codec/avs3/enums.ts | 5 +++-- src/codec/simple/enums.ts | 8 ++++++++ src/codec/simple/simple-codec-info.spec.ts | 5 ++++- src/index.spec.ts | 11 +++++++++-- src/index.ts | 2 +- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 504a0a0..a549666 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,10 @@ Useful for reading codec parameters out of a `codecs=` string, checking support | EVC | ✅ | ✅ | | L-HEVC | ✅ | ✅ | | MP4 (mp4a/mp4v — AAC, MP3, MPEG-4/2) | ✅ | ✅ | -| AVS3 video/audio (avs3/av3a) | ✅ | ✅ | +| AVS3 video/audio (avs3/lav3/av3a) | ✅ | ✅ | | AVS2 audio (cavs) | ✅ | ✅ | | MPEG-H 3D Audio (mha1/mhm1) | ✅ | ✅ | -| Parameterless (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM) | ✅ | ✅ | +| Parameterless (DTS, VC-1, Opus, FLAC, Vorbis, ALAC, PCM, AMR, 3GPP text, CEA-608/708, DRA) | ✅ | ✅ | | Uncompressed (uncv/unci) | ✅ | ✅ | | Subtitles (WebVTT wvtt, TTML stpp) | ✅ | ✅ | @@ -103,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`, `stpp`) and returns the matching + `mp4a`/`mp4v`, `avs3`/`lav3`, `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 @@ -129,7 +129,7 @@ console.log(info.toString()); // 'avc1.640028' `hAudioObjectType`). Covers the RFC 6381 `mp4a`/`mp4v` ObjectTypeIndication scheme — AAC, MP3, MPEG-4 Visual, MPEG-2 video/audio and more (`mp4a.40.2`, `mp4a.69`, `mp4v.20.9`, …). - `avs3` — namespace exporting `Avs3VideoInfo` (`avs3..`) and `Avs3AudioInfo` - (`av3a.`) for the AVS3 video/audio standard. + (`av3a.`) for the AVS3 video/audio standard. `lav3` (library track) shares the video format. - `avs2` — namespace exporting `Avs2AudioInfo` for AVS2 audio (`cavs.`). - `stpp` — namespace exporting `StppInfo` for TTML / timed-text subtitles (`stpp[.[.]]`, e.g. `stpp.ttml.im1t`), with the TTML profile decoded to a @@ -138,7 +138,7 @@ console.log(info.toString()); // 'avc1.640028' `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`, `wvtt` (WebVTT), - and PCM variants such as `ipcm`/`fpcm`/`twos`/`sowt`). + `tx3g`, `c608`/`c708`, `samr`/`sawb`, `dra1`, 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`, diff --git a/src/codec/avs3/avs3-video-info.spec.ts b/src/codec/avs3/avs3-video-info.spec.ts index b8b1ce2..c0802b8 100644 --- a/src/codec/avs3/avs3-video-info.spec.ts +++ b/src/codec/avs3/avs3-video-info.spec.ts @@ -7,10 +7,17 @@ describe('Avs3VideoInfo', () => { 'avs3.32.50', 'avs3.22.20', 'avs3.ab.cd', + 'lav3.20.10', ])('parses and round-trips %s', (str) => { expect(Avs3VideoInfo.fromString(str).toString()).toBe(str); }); + it('accepts the lav3 library-track sample entry', () => { + const info = Avs3VideoInfo.fromString('lav3.20.10'); + expect(info.fourCC).toBe('lav3'); + expect(info.codecName).toBe('lav3'); + }); + it('decodes the fields', () => { const info = Avs3VideoInfo.fromString('avs3.20.10'); expect(info.fourCC).toBe('avs3'); diff --git a/src/codec/avs3/avs3-video-info.ts b/src/codec/avs3/avs3-video-info.ts index d25e209..4506985 100644 --- a/src/codec/avs3/avs3-video-info.ts +++ b/src/codec/avs3/avs3-video-info.ts @@ -26,6 +26,7 @@ export class Avs3VideoInfo extends CodecInfo { throw new Error(`Invalid AVS3 video sample entry: ${fourCC}`); } this._fourCC = fourCC; + this.codecName = fourCC; } get profileId(): number { diff --git a/src/codec/avs3/enums.ts b/src/codec/avs3/enums.ts index 838ac3e..6dca399 100644 --- a/src/codec/avs3/enums.ts +++ b/src/codec/avs3/enums.ts @@ -2,8 +2,9 @@ // avs3.. (video, hex bytes) e.g. avs3.20.10, avs3.32.50 // av3a. (audio, hex byte) e.g. av3a.00 -export type Avs3VideoFourCC = 'avs3'; -export const AVS3_VIDEO_FOUR_CCS: readonly Avs3VideoFourCC[] = ['avs3']; +// avs3 = regular video track; lav3 = AVS3 "library track" (same profile/level string). +export type Avs3VideoFourCC = 'avs3' | 'lav3'; +export const AVS3_VIDEO_FOUR_CCS: readonly Avs3VideoFourCC[] = ['avs3', 'lav3']; export type Avs3AudioFourCC = 'av3a'; export const AVS3_AUDIO_FOUR_CCS: readonly Avs3AudioFourCC[] = ['av3a']; diff --git a/src/codec/simple/enums.ts b/src/codec/simple/enums.ts index d0079b6..13a4e94 100644 --- a/src/codec/simple/enums.ts +++ b/src/codec/simple/enums.ts @@ -18,6 +18,14 @@ export const SIMPLE_CODECS: Readonly> = { 'alac': 'Apple Lossless (ALAC)', // WebVTT subtitles (parameterless; TTML/stpp is handled separately as it carries a profile) 'wvtt': 'WebVTT', + // Other text / caption sample entries + 'tx3g': '3GPP Timed Text', + 'c608': 'CEA-608 Closed Captions', + 'c708': 'CEA-708 Closed Captions', + // 3GPP mobile audio and DRA + 'samr': 'AMR (narrow-band)', + 'sawb': 'AMR-WB (wide-band)', + 'dra1': 'DRA Audio', // PCM variants 'ipcm': 'Uncompressed PCM', 'fpcm': 'Floating-point PCM', diff --git a/src/codec/simple/simple-codec-info.spec.ts b/src/codec/simple/simple-codec-info.spec.ts index 55bf663..6116460 100644 --- a/src/codec/simple/simple-codec-info.spec.ts +++ b/src/codec/simple/simple-codec-info.spec.ts @@ -5,7 +5,7 @@ describe('SimpleCodecInfo', () => { it.each([ 'opus', 'flac', 'vorbis', 'alac', 'dtsc', 'dtse', 'dtsh', 'dtsl', 'dtsx', 'dtsy', - 'vc-1', 'wvtt', + 'vc-1', 'wvtt', 'tx3g', 'c608', 'c708', 'samr', 'sawb', 'dra1', 'twos', 'sowt', 'lpcm', 'ipcm', 'fpcm', 'in24', 'in32', 'fl32', 'fl64', ])('parses and round-trips %s', (str) => { expect(SimpleCodecInfo.fromString(str).toString()).toBe(str); @@ -34,6 +34,9 @@ describe('SimpleCodecInfo', () => { ['twos', 'PCM (big-endian)'], ['sowt', 'PCM (little-endian)'], ['wvtt', 'WebVTT'], + ['tx3g', '3GPP Timed Text'], + ['samr', 'AMR (narrow-band)'], + ['c608', 'CEA-608 Closed Captions'], ])('%s -> %s', (str, name) => { expect(SimpleCodecInfo.fromString(str).toHumanReadable().codec).toBe(name); }); diff --git a/src/index.spec.ts b/src/index.spec.ts index 6979825..bb7f6ef 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -79,11 +79,18 @@ describe('codecInfoFactory', () => { expect(video.codecName).toBe('mp4v'); }); - it('dispatches avs3 strings to Avs3VideoInfo', () => { + it('dispatches avs3 and lav3 strings to Avs3VideoInfo', () => { const info = codecInfoFactory('avs3.20.10'); expect(info).toBeInstanceOf(avs3.Avs3VideoInfo); expect(info.codecName).toBe('avs3'); expect((info as avs3.Avs3VideoInfo).profileId).toBe(0x20); + expect(codecInfoFactory('lav3.20.10').codecName).toBe('lav3'); + }); + + it('dispatches tx3g to SimpleCodecInfo', () => { + const info = codecInfoFactory('tx3g'); + expect(info).toBeInstanceOf(simple.SimpleCodecInfo); + expect((info as simple.SimpleCodecInfo).toHumanReadable().codec).toBe('3GPP Timed Text'); }); it('dispatches av3a strings to Avs3AudioInfo', () => { @@ -139,7 +146,7 @@ describe('codecInfoFactory', () => { it('throws on an unknown codec', () => { expect(() => codecInfoFactory('theora')).toThrow('Unknown codec'); - expect(() => codecInfoFactory('tx3g')).toThrow('Unknown codec'); + expect(() => codecInfoFactory('nope')).toThrow('Unknown codec'); }); }); diff --git a/src/index.ts b/src/index.ts index 0625064..7c4d978 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,7 +66,7 @@ export const codecInfoFactory = (codecString: string) => { if (codecString.startsWith('mp4a') || codecString.startsWith('mp4v')) { return Mp4Info.fromString(codecString); } - if (codecString.startsWith('avs3')) { + if (codecString.startsWith('avs3') || codecString.startsWith('lav3')) { return Avs3VideoInfo.fromString(codecString); } if (codecString.startsWith('av3a')) {