Validate video codec strings using regexes

This commit is contained in:
David Payr
2024-12-30 00:47:01 +01:00
parent 06cedf1a26
commit 8f861f3cfe
2 changed files with 73 additions and 27 deletions
+69 -22
View File
@@ -470,6 +470,12 @@ export const getAudioEncoderConfigExtension = (codec: AudioCodec) => {
return {}; return {};
}; };
const VALID_VIDEO_CODEC_STRING_PREFIXES = ['avc1', 'avc3', 'hev1', 'hvc1', 'vp8', 'vp09', 'av01'];
const AVC_CODEC_STRING_REGEX = /^(avc1|avc3)\.[0-9a-fA-F]{6}$/;
const HEVC_CODEC_STRING_REGEX = /^(hev1|hvc1)\.(?:[ABC]?\d+)\.[0-9a-fA-F]{1,8}\.[LH]\d+(?:\.[0-9a-fA-F]{1,2}){0,6}$/;
const VP9_CODEC_STRING_REGEX = /^vp09(?:\.\d{2}){3}(?:(?:\.\d{2}){5})?$/;
const AV1_CODEC_STRING_REGEX = /^av01\.\d\.\d{2}[MH]\.\d{2}(?:\.\d\.\d{3}\.\d{2}\.\d{2}\.\d{2}\.\d)?$/;
export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | undefined) => { export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | undefined) => {
if (!metadata) { if (!metadata) {
throw new TypeError('Video chunk metadata must be provided.'); throw new TypeError('Video chunk metadata must be provided.');
@@ -486,6 +492,12 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (typeof metadata.decoderConfig.codec !== 'string') { if (typeof metadata.decoderConfig.codec !== 'string') {
throw new TypeError('Video chunk metadata decoder configuration must specify a codec string.'); throw new TypeError('Video chunk metadata decoder configuration must specify a codec string.');
} }
if (!VALID_VIDEO_CODEC_STRING_PREFIXES.some(prefix => metadata.decoderConfig!.codec.startsWith(prefix))) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string must be a valid video codec string as specified in'
+ ' the WebCodecs codec registry.',
);
}
if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) { if (!Number.isInteger(metadata.decoderConfig.codedWidth) || metadata.decoderConfig.codedWidth! <= 0) {
throw new TypeError( throw new TypeError(
'Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).', 'Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).',
@@ -544,31 +556,66 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
} }
} }
if ( // AVC-specific validation
(metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) if (metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) {
&& !metadata.decoderConfig.description if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
) { throw new TypeError(
throw new TypeError( 'Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as'
'Video chunk metadata decoder configuration for AVC must include a description, which is expected to be an' + ' specified in Section 3.4 of RFC 6381.',
+ ' AVCDecoderConfigurationRecord as specified in ISO 14496-15.', );
); }
if (!metadata.decoderConfig.description) {
throw new TypeError(
'Video chunk metadata decoder configuration for AVC must include a description, which is expected to be'
+ ' an AVCDecoderConfigurationRecord as specified in ISO 14496-15.',
);
}
} }
if (
(metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) // HEVC-specific validation
&& !metadata.decoderConfig.description if (metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) {
) { if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError( throw new TypeError(
'Video chunk metadata decoder configuration for HEVC must include a description, which is expected to be an' 'Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as'
+ ' HEVCDecoderConfigurationRecord as specified in ISO 14496-15.', + ' specified in Section E.3 of ISO 14496-15.',
); );
}
if (!metadata.decoderConfig.description) {
throw new TypeError(
'Video chunk metadata decoder configuration for HEVC must include a description, which is expected to'
+ ' be an HEVCDecoderConfigurationRecord as specified in ISO 14496-15.',
);
}
} }
if (
(metadata.decoderConfig.codec === 'vp8' || metadata.decoderConfig.codec.startsWith('vp09')) // VP8-specific validation
&& metadata.decoderConfig.colorSpace === undefined if (metadata.decoderConfig.codec.startsWith('vp8')) {
) { if (metadata.decoderConfig.codec !== 'vp8') {
throw new TypeError('Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.'); throw new TypeError('Video chunk metadata decoder configuration codec string for VP8 must be "vp8".');
}
}
// VP9-specific validation
if (metadata.decoderConfig.codec.startsWith('vp09')) {
if (!VP9_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string for VP9 must be a valid VP9 codec string as'
+ ' specified in Section "Codecs Parameter String" of https://www.webmproject.org/vp9/mp4/.',
);
}
}
// AV1-specific validation
if (metadata.decoderConfig.codec.startsWith('av01')) {
if (!AV1_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string for AV1 must be a valid AV1 codec string as'
+ ' specified in Section "Codecs Parameter String" of https://aomediacodec.github.io/av1-isobmff/.',
);
}
} }
// No added requirements for AV1 (based)
}; };
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => { export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
+4 -5
View File
@@ -594,7 +594,6 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
} }
const decoderConfig = trackData.info.decoderConfig; const decoderConfig = trackData.info.decoderConfig;
assert(decoderConfig.colorSpace); // This is guaranteed by an earlier validation step
const parts = decoderConfig.codec.split('.'); // We can derive the required values from the codec string const parts = decoderConfig.codec.split('.'); // We can derive the required values from the codec string
const profile = Number(parts[1]); const profile = Number(parts[1]);
@@ -602,22 +601,22 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
const bitDepth = Number(parts[3]); const bitDepth = Number(parts[3]);
const chromaSubsampling = parts[4] ? Number(parts[4]) : 1; // 4:2:0 colocated with luma (0,0) const chromaSubsampling = parts[4] ? Number(parts[4]) : 1; // 4:2:0 colocated with luma (0,0)
const videoFullRangeFlag = parts[8] ? Number(parts[8]) : Number(decoderConfig.colorSpace.fullRange); const videoFullRangeFlag = parts[8] ? Number(parts[8]) : Number(decoderConfig.colorSpace?.fullRange ?? 0);
const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + videoFullRangeFlag; const thirdByte = (bitDepth << 4) + (chromaSubsampling << 1) + videoFullRangeFlag;
const colourPrimaries = parts[5] const colourPrimaries = parts[5]
? Number(parts[5]) ? Number(parts[5])
: decoderConfig.colorSpace.primaries : decoderConfig.colorSpace?.primaries
? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries] ? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries]
: 2; // Default to undetermined : 2; // Default to undetermined
const transferCharacteristics = parts[6] const transferCharacteristics = parts[6]
? Number(parts[6]) ? Number(parts[6])
: decoderConfig.colorSpace.transfer : decoderConfig.colorSpace?.transfer
? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer] ? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer]
: 2; : 2;
const matrixCoefficients = parts[7] const matrixCoefficients = parts[7]
? Number(parts[7]) ? Number(parts[7])
: decoderConfig.colorSpace.matrix : decoderConfig.colorSpace?.matrix
? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix] ? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix]
: 2; : 2;