diff --git a/src/codec.ts b/src/codec.ts index bc400ad..5117df7 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -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) => { diff --git a/src/isobmff/isobmff-boxes.ts b/src/isobmff/isobmff-boxes.ts index f983f72..bda787e 100644 --- a/src/isobmff/isobmff-boxes.ts +++ b/src/isobmff/isobmff-boxes.ts @@ -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;