From 2471f55f20d67f5482dcd21b12609032dc8b027f Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Thu, 2 Jan 2025 20:21:49 +0100 Subject: [PATCH] Validate audio codec strings --- src/codec.ts | 115 ++++++++++++++++++++++++++++----- src/isobmff/isobmff-boxes.ts | 4 +- src/isobmff/isobmff-demuxer.ts | 18 +++--- src/matroska/matroska-muxer.ts | 2 +- src/media-drain.ts | 4 +- todo.txt | 1 - 6 files changed, 115 insertions(+), 29 deletions(-) diff --git a/src/codec.ts b/src/codec.ts index 21cb074..3aa1171 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -23,14 +23,14 @@ export const VIDEO_CODECS = [ export const PCM_CODECS = [ 'pcm-u8', 'pcm-s8', + 'pcm-s16', // We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings 'pcm-s16be', - 'pcm-s16le', + 'pcm-s24', 'pcm-s24be', - 'pcm-s24le', + 'pcm-s32', 'pcm-s32be', - 'pcm-s32le', + 'pcm-f32', 'pcm-f32be', - 'pcm-f32le', ] as const; export const AUDIO_CODECS = [ 'aac', @@ -673,6 +673,8 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | } }; +const VALID_AUDIO_CODEC_STRING_PREFIXES = ['mp4a', 'mp3', 'opus', 'vorbis', 'flac', 'ulaw', 'alaw', 'pcm']; + export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => { if (!metadata) { throw new TypeError('Audio chunk metadata must be provided.'); @@ -689,6 +691,12 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | if (typeof metadata.decoderConfig.codec !== 'string') { throw new TypeError('Audio chunk metadata decoder configuration must specify a codec string.'); } + if (!VALID_AUDIO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) { + throw new TypeError( + 'Audio chunk metadata decoder configuration codec string must be a valid audio codec string as specified in' + + ' the WebCodecs codec registry.', + ); + } if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) { throw new TypeError( 'Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).', @@ -708,18 +716,95 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | } } - if (metadata.decoderConfig.codec.startsWith('mp4a') && !metadata.decoderConfig.description) { - throw new TypeError( - 'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an' - + ' AudioSpecificConfig as specified in ISO 14496-3.', - ); + // AAC-specific validation + if (metadata.decoderConfig.codec.startsWith('mp4a')) { + const validStrings = ['mp4a.40.2', 'mp4a.40.02', 'mp4a.40.5', 'mp4a.40.05', 'mp4a.40.29', 'mp4a.67']; + if (!validStrings.includes(metadata.decoderConfig.codec)) { + throw new TypeError( + 'Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as' + + ' specified in https://www.w3.org/TR/webcodecs-aac-codec-registration/.', + ); + } + + if (!metadata.decoderConfig.description) { + throw new TypeError( + 'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be' + + ' an AudioSpecificConfig as specified in ISO 14496-3.', + ); + } } - if ( - metadata.decoderConfig.codec === 'opus' - && metadata.decoderConfig.description - && metadata.decoderConfig.description.byteLength < 18 - ) { - throw new TypeError('Invalid decoder description provided for Opus; must be at least 18 bytes long.'); + + // MP3-specific validation + if (metadata.decoderConfig.codec === 'mp3') { + if (metadata.decoderConfig.codec !== 'mp3') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for MP3 must be "mp3".'); + } + } + + // Opus-specific validation + if (metadata.decoderConfig.codec === 'opus') { + if (metadata.decoderConfig.codec !== 'opus') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for Opus must be "opus".'); + } + + if (!metadata.decoderConfig.description) { + throw new TypeError( + 'Audio chunk metadata decoder configuration for Opus must include a description, which is expected to' + + ' be an Identification Header as specified in Section 5.1 of RFC 7845.', + ); + } + } + + // Vorbis-specific validation + if (metadata.decoderConfig.codec === 'vorbis') { + if (metadata.decoderConfig.codec !== 'vorbis') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for Vorbis must be "vorbis".'); + } + + if (!metadata.decoderConfig.description) { + throw new TypeError( + 'Audio chunk metadata decoder configuration for Vorbis must include a description, which is expected to' + + ' adhere to the format described in https://www.w3.org/TR/webcodecs-vorbis-codec-registration/.', + ); + } + } + + // FLAC-specific validation + if (metadata.decoderConfig.codec === 'flac') { + if (metadata.decoderConfig.codec !== 'flac') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for FLAC must be "flac".'); + } + + if (!metadata.decoderConfig.description) { + throw new TypeError( + 'Audio chunk metadata decoder configuration for FLAC must include a description, which is expected to' + + ' adhere to the format described in https://www.w3.org/TR/webcodecs-flac-codec-registration/.', + ); + } + } + + // ulaw-specific validation + if (metadata.decoderConfig.codec === 'ulaw') { + if (metadata.decoderConfig.codec !== 'ulaw') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for uLaw must be "ulaw".'); + } + } + + // alaw-specific validation + if (metadata.decoderConfig.codec === 'alaw') { + if (metadata.decoderConfig.codec !== 'alaw') { + throw new TypeError('Audio chunk metadata decoder configuration codec string for A-law must be "alaw".'); + } + } + + // PCM-specific validation + if (metadata.decoderConfig.codec.startsWith('pcm')) { + if (!(PCM_CODECS as readonly string[]).includes(metadata.decoderConfig.codec)) { + throw new TypeError( + 'Audio chunk metadata decoder configuration codec string for PCM must be one of the supported PCM' + + ` codecs (${PCM_CODECS.join(', ')}).`, + ); + } } }; diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index 4b81a8e..e6258be 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -742,7 +742,9 @@ export const dOps = (trackData: IsobmffAudioTrackData) => { // https://www.rfc-editor.org/rfc/rfc7845#section-5 const description = trackData.info.decoderConfig?.description; if (description) { - assert(description.byteLength >= 18); // Is validated in an earlier step + if (description.byteLength < 18) { + throw new Error('Opus decoder description is too short (must be at least 18 bytes).'); + } const view = ArrayBuffer.isView(description) ? new DataView(description.buffer, description.byteOffset, description.byteLength) diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts index 3a781b1..d73e97d 100644 --- a/src/isobmff/isobmff-demuxer.ts +++ b/src/isobmff/isobmff-demuxer.ts @@ -766,18 +766,18 @@ export class IsobmffDemuxer extends Demuxer { if (sampleSize > 0 && sampleSize <= 64) { if (isFloat) { if (sampleSize === 32 && !isBigEndian) { - track.info.codec = isBigEndian ? 'pcm-f32be' : 'pcm-f32le'; + track.info.codec = isBigEndian ? 'pcm-f32be' : 'pcm-f32'; } } else { if (sFlags & (1 << (bytesPerSample - 1))) { if (bytesPerSample === 1) { track.info.codec = 'pcm-s8'; } else if (bytesPerSample === 2) { - track.info.codec = isBigEndian ? 'pcm-s16be' : 'pcm-s16le'; + track.info.codec = isBigEndian ? 'pcm-s16be' : 'pcm-s16'; } else if (bytesPerSample === 3) { - track.info.codec = isBigEndian ? 'pcm-s24be' : 'pcm-s24le'; + track.info.codec = isBigEndian ? 'pcm-s24be' : 'pcm-s24'; } else if (bytesPerSample === 4) { - track.info.codec = isBigEndian ? 'pcm-s32be' : 'pcm-s32le'; + track.info.codec = isBigEndian ? 'pcm-s32be' : 'pcm-s32'; } } else { if (bytesPerSample === 1) { @@ -809,7 +809,7 @@ export class IsobmffDemuxer extends Demuxer { if (sampleSize === 8) { track.info.codec = 'pcm-s8'; } else if (sampleSize === 16) { - track.info.codec = 'pcm-s16le'; + track.info.codec = 'pcm-s16'; } else { throw new Error(`Unsupported sample size ${sampleSize} for codec 'sowt'.`); } @@ -1014,13 +1014,13 @@ export class IsobmffDemuxer extends Demuxer { if (littleEndian) { if (track.info.codec === 'pcm-s16be') { - track.info.codec = 'pcm-s16le'; + track.info.codec = 'pcm-s16'; } else if (track.info.codec === 'pcm-s24be') { - track.info.codec = 'pcm-s24le'; + track.info.codec = 'pcm-s24'; } else if (track.info.codec === 'pcm-s32be') { - track.info.codec = 'pcm-s32le'; + track.info.codec = 'pcm-s32'; } else if (track.info.codec === 'pcm-f32be') { - track.info.codec = 'pcm-f32le'; + track.info.codec = 'pcm-f32'; } } }; break; diff --git a/src/matroska/matroska-muxer.ts b/src/matroska/matroska-muxer.ts index 46c9b89..83316f4 100644 --- a/src/matroska/matroska-muxer.ts +++ b/src/matroska/matroska-muxer.ts @@ -107,7 +107,7 @@ const CODEC_STRING_MAP: Record vp8: 'V_VP8', vp9: 'V_VP9', av1: 'V_AV1', - aac: 'A_AAC', + aac: 'A_AAC', // TODO is this even correct opus: 'A_OPUS', webvtt: 'S_TEXT/WEBVTT', }; diff --git a/src/media-drain.ts b/src/media-drain.ts index 184e0d3..c38e75f 100644 --- a/src/media-drain.ts +++ b/src/media-drain.ts @@ -809,7 +809,7 @@ class AudioDecoderWrapper extends DecoderWrapper { } } -const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be|le)?$/; +const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; // There are a lot of PCM variants not natively supported by the browser and by AudioData. Therefore we need a simple // decoder that maps any input PCM format into a PCM format supported by the browser. @@ -846,7 +846,7 @@ class PcmAudioDecoderWrapper extends DecoderWrapper