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 {};
};
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) => {
if (!metadata) {
throw new TypeError('Video chunk metadata must be provided.');
@@ -486,6 +492,12 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
if (typeof metadata.decoderConfig.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) {
throw new TypeError(
'Video chunk metadata decoder configuration must specify a valid codedWidth (positive integer).',
@@ -544,31 +556,66 @@ export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata |
}
}
if (
(metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3'))
&& !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.',
);
// AVC-specific validation
if (metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) {
if (!AVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string for AVC must be a valid AVC codec string as'
+ ' specified in Section 3.4 of RFC 6381.',
);
}
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'))
&& !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.',
);
// HEVC-specific validation
if (metadata.decoderConfig.codec.startsWith('hev1') || metadata.decoderConfig.codec.startsWith('hvc1')) {
if (!HEVC_CODEC_STRING_REGEX.test(metadata.decoderConfig.codec)) {
throw new TypeError(
'Video chunk metadata decoder configuration codec string for HEVC must be a valid HEVC codec string as'
+ ' 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'))
&& metadata.decoderConfig.colorSpace === undefined
) {
throw new TypeError('Video chunk metadata decoder configuration for VP8/VP9 must include a colorSpace.');
// VP8-specific validation
if (metadata.decoderConfig.codec.startsWith('vp8')) {
if (metadata.decoderConfig.codec !== 'vp8') {
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) => {
+4 -5
View File
@@ -594,7 +594,6 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
}
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 profile = Number(parts[1]);
@@ -602,22 +601,22 @@ export const vpcC = (trackData: IsobmffVideoTrackData) => {
const bitDepth = Number(parts[3]);
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 colourPrimaries = parts[5]
? Number(parts[5])
: decoderConfig.colorSpace.primaries
: decoderConfig.colorSpace?.primaries
? COLOR_PRIMARIES_MAP[decoderConfig.colorSpace.primaries]
: 2; // Default to undetermined
const transferCharacteristics = parts[6]
? Number(parts[6])
: decoderConfig.colorSpace.transfer
: decoderConfig.colorSpace?.transfer
? TRANSFER_CHARACTERISTICS_MAP[decoderConfig.colorSpace.transfer]
: 2;
const matrixCoefficients = parts[7]
? Number(parts[7])
: decoderConfig.colorSpace.matrix
: decoderConfig.colorSpace?.matrix
? MATRIX_COEFFICIENTS_MAP[decoderConfig.colorSpace.matrix]
: 2;