mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Validate audio codec strings
This commit is contained in:
+98
-13
@@ -23,14 +23,14 @@ export const VIDEO_CODECS = [
|
|||||||
export const PCM_CODECS = [
|
export const PCM_CODECS = [
|
||||||
'pcm-u8',
|
'pcm-u8',
|
||||||
'pcm-s8',
|
'pcm-s8',
|
||||||
|
'pcm-s16', // We don't prefix 'le' so we're compatible with the WebCodecs-registered PCM codec strings
|
||||||
'pcm-s16be',
|
'pcm-s16be',
|
||||||
'pcm-s16le',
|
'pcm-s24',
|
||||||
'pcm-s24be',
|
'pcm-s24be',
|
||||||
'pcm-s24le',
|
'pcm-s32',
|
||||||
'pcm-s32be',
|
'pcm-s32be',
|
||||||
'pcm-s32le',
|
'pcm-f32',
|
||||||
'pcm-f32be',
|
'pcm-f32be',
|
||||||
'pcm-f32le',
|
|
||||||
] as const;
|
] as const;
|
||||||
export const AUDIO_CODECS = [
|
export const AUDIO_CODECS = [
|
||||||
'aac',
|
'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) => {
|
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
throw new TypeError('Audio chunk metadata must be provided.');
|
throw new TypeError('Audio chunk metadata must be provided.');
|
||||||
@@ -689,6 +691,12 @@ export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata |
|
|||||||
if (typeof metadata.decoderConfig.codec !== 'string') {
|
if (typeof metadata.decoderConfig.codec !== 'string') {
|
||||||
throw new TypeError('Audio chunk metadata decoder configuration must specify a 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) {
|
if (!Number.isInteger(metadata.decoderConfig.sampleRate) || metadata.decoderConfig.sampleRate <= 0) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'Audio chunk metadata decoder configuration must specify a valid sampleRate (positive integer).',
|
'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) {
|
// 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(
|
throw new TypeError(
|
||||||
'Audio chunk metadata decoder configuration for AAC must include a description, which is expected to be an'
|
'Audio chunk metadata decoder configuration codec string for AAC must be a valid AAC codec string as'
|
||||||
+ ' AudioSpecificConfig as specified in ISO 14496-3.',
|
+ ' 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.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(', ')}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -742,7 +742,9 @@ export const dOps = (trackData: IsobmffAudioTrackData) => {
|
|||||||
// https://www.rfc-editor.org/rfc/rfc7845#section-5
|
// https://www.rfc-editor.org/rfc/rfc7845#section-5
|
||||||
const description = trackData.info.decoderConfig?.description;
|
const description = trackData.info.decoderConfig?.description;
|
||||||
if (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)
|
const view = ArrayBuffer.isView(description)
|
||||||
? new DataView(description.buffer, description.byteOffset, description.byteLength)
|
? new DataView(description.buffer, description.byteOffset, description.byteLength)
|
||||||
|
|||||||
@@ -766,18 +766,18 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
if (sampleSize > 0 && sampleSize <= 64) {
|
if (sampleSize > 0 && sampleSize <= 64) {
|
||||||
if (isFloat) {
|
if (isFloat) {
|
||||||
if (sampleSize === 32 && !isBigEndian) {
|
if (sampleSize === 32 && !isBigEndian) {
|
||||||
track.info.codec = isBigEndian ? 'pcm-f32be' : 'pcm-f32le';
|
track.info.codec = isBigEndian ? 'pcm-f32be' : 'pcm-f32';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sFlags & (1 << (bytesPerSample - 1))) {
|
if (sFlags & (1 << (bytesPerSample - 1))) {
|
||||||
if (bytesPerSample === 1) {
|
if (bytesPerSample === 1) {
|
||||||
track.info.codec = 'pcm-s8';
|
track.info.codec = 'pcm-s8';
|
||||||
} else if (bytesPerSample === 2) {
|
} else if (bytesPerSample === 2) {
|
||||||
track.info.codec = isBigEndian ? 'pcm-s16be' : 'pcm-s16le';
|
track.info.codec = isBigEndian ? 'pcm-s16be' : 'pcm-s16';
|
||||||
} else if (bytesPerSample === 3) {
|
} else if (bytesPerSample === 3) {
|
||||||
track.info.codec = isBigEndian ? 'pcm-s24be' : 'pcm-s24le';
|
track.info.codec = isBigEndian ? 'pcm-s24be' : 'pcm-s24';
|
||||||
} else if (bytesPerSample === 4) {
|
} else if (bytesPerSample === 4) {
|
||||||
track.info.codec = isBigEndian ? 'pcm-s32be' : 'pcm-s32le';
|
track.info.codec = isBigEndian ? 'pcm-s32be' : 'pcm-s32';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (bytesPerSample === 1) {
|
if (bytesPerSample === 1) {
|
||||||
@@ -809,7 +809,7 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
if (sampleSize === 8) {
|
if (sampleSize === 8) {
|
||||||
track.info.codec = 'pcm-s8';
|
track.info.codec = 'pcm-s8';
|
||||||
} else if (sampleSize === 16) {
|
} else if (sampleSize === 16) {
|
||||||
track.info.codec = 'pcm-s16le';
|
track.info.codec = 'pcm-s16';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
throw new Error(`Unsupported sample size ${sampleSize} for codec 'sowt'.`);
|
||||||
}
|
}
|
||||||
@@ -1014,13 +1014,13 @@ export class IsobmffDemuxer extends Demuxer {
|
|||||||
|
|
||||||
if (littleEndian) {
|
if (littleEndian) {
|
||||||
if (track.info.codec === 'pcm-s16be') {
|
if (track.info.codec === 'pcm-s16be') {
|
||||||
track.info.codec = 'pcm-s16le';
|
track.info.codec = 'pcm-s16';
|
||||||
} else if (track.info.codec === 'pcm-s24be') {
|
} else if (track.info.codec === 'pcm-s24be') {
|
||||||
track.info.codec = 'pcm-s24le';
|
track.info.codec = 'pcm-s24';
|
||||||
} else if (track.info.codec === 'pcm-s32be') {
|
} else if (track.info.codec === 'pcm-s32be') {
|
||||||
track.info.codec = 'pcm-s32le';
|
track.info.codec = 'pcm-s32';
|
||||||
} else if (track.info.codec === 'pcm-f32be') {
|
} else if (track.info.codec === 'pcm-f32be') {
|
||||||
track.info.codec = 'pcm-f32le';
|
track.info.codec = 'pcm-f32';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}; break;
|
}; break;
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const CODEC_STRING_MAP: Record<VideoCodec | AudioCodec | SubtitleCodec, string>
|
|||||||
vp8: 'V_VP8',
|
vp8: 'V_VP8',
|
||||||
vp9: 'V_VP9',
|
vp9: 'V_VP9',
|
||||||
av1: 'V_AV1',
|
av1: 'V_AV1',
|
||||||
aac: 'A_AAC',
|
aac: 'A_AAC', // TODO is this even correct
|
||||||
opus: 'A_OPUS',
|
opus: 'A_OPUS',
|
||||||
webvtt: 'S_TEXT/WEBVTT',
|
webvtt: 'S_TEXT/WEBVTT',
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -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
|
// 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.
|
// 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;
|
this.inputSampleSize = (Number(match[2]) / 8) as 1 | 2 | 3 | 4;
|
||||||
const littleEndian = match[3] === 'le';
|
const littleEndian = match[3] !== 'be';
|
||||||
|
|
||||||
switch (this.inputSampleSize) {
|
switch (this.inputSampleSize) {
|
||||||
case 1: {
|
case 1: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
- Test tree shaking
|
- Test tree shaking
|
||||||
- Throw if edit list detected
|
- Throw if edit list detected
|
||||||
- Audio codec string regex validation
|
|
||||||
- Add the new audio codecs to muxers
|
- Add the new audio codecs to muxers
|
||||||
- Mov muxer!!! Only mov can hold PCM audio, MP4 cannot
|
- Mov muxer!!! Only mov can hold PCM audio, MP4 cannot
|
||||||
- Metadata methods for computing average fps and bitrate
|
- Metadata methods for computing average fps and bitrate
|
||||||
Reference in New Issue
Block a user