Validate audio codec strings

This commit is contained in:
Vanilagy
2025-01-02 20:21:49 +01:00
parent 9ac0519217
commit 2471f55f20
6 changed files with 115 additions and 29 deletions
+100 -15
View File
@@ -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(', ')}).`,
);
}
}
};
+3 -1
View File
@@ -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)
+9 -9
View File
@@ -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;
+1 -1
View File
@@ -107,7 +107,7 @@ const CODEC_STRING_MAP: Record<VideoCodec | AudioCodec | SubtitleCodec, string>
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',
};
+2 -2
View File
@@ -809,7 +809,7 @@ class AudioDecoderWrapper extends DecoderWrapper<EncodedAudioChunk, AudioData> {
}
}
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<EncodedAudioChunk, AudioData
}
this.inputSampleSize = (Number(match[2]) / 8) as 1 | 2 | 3 | 4;
const littleEndian = match[3] === 'le';
const littleEndian = match[3] !== 'be';
switch (this.inputSampleSize) {
case 1: {
-1
View File
@@ -1,6 +1,5 @@
- Test tree shaking
- Throw if edit list detected
- Audio codec string regex validation
- Add the new audio codecs to muxers
- Mov muxer!!! Only mov can hold PCM audio, MP4 cannot
- Metadata methods for computing average fps and bitrate