Address a bunch of TODOs and add parameter validation everywhere

This commit is contained in:
Vanilagy
2024-11-24 21:17:00 +01:00
parent 29749451bb
commit b51b2b1b45
14 changed files with 1846 additions and 1059 deletions
+169 -47
View File
@@ -1,4 +1,6 @@
import { COLOR_PRIMARIES_MAP, isAllowSharedBufferSource, MATRIX_COEFFICIENTS_MAP, TRANSFER_CHARACTERISTICS_MAP } from "./misc";
import { AudioCodec, VideoCodec } from "./source";
import { SubtitleMetadata } from "./subtitles";
export const buildVideoCodecString = (codec: VideoCodec, width: number, height: number) => {
if (codec === 'avc') {
@@ -63,56 +65,56 @@ export const buildVideoCodecString = (codec: VideoCodec, width: number, height:
return 'vp8'; // Easy, this one
} else if (codec === 'vp9') {
// Default to Profile 0 (most common)
const profile = "00";
// Determine level based on resolution
// VP9 levels are specified as two digits: major.minor
let level;
if (width <= 854 && height <= 480) {
level = "21"; // Level 2.1
} else if (width <= 1280 && height <= 720) {
level = "31"; // Level 3.1
} else if (width <= 1920 && height <= 1080) {
level = "41"; // Level 4.1
} else if (width <= 3840 && height <= 2160) {
level = "51"; // Level 5.1
} else {
level = "61"; // Level 6.1
}
// Default to 8-bit depth
const bitDepth = "08";
return `vp09.${profile}.${level}.${bitDepth}`;
const profile = "00";
// Determine level based on resolution
// VP9 levels are specified as two digits: major.minor
let level;
if (width <= 854 && height <= 480) {
level = "21"; // Level 2.1
} else if (width <= 1280 && height <= 720) {
level = "31"; // Level 3.1
} else if (width <= 1920 && height <= 1080) {
level = "41"; // Level 4.1
} else if (width <= 3840 && height <= 2160) {
level = "51"; // Level 5.1
} else {
level = "61"; // Level 6.1
}
// Default to 8-bit depth
const bitDepth = "08";
return `vp09.${profile}.${level}.${bitDepth}`;
} else if (codec === 'av1') {
// Default to Main Profile (0)
const profile = 0;
// Determine level based on resolution
// Using a simplified level selection based on common resolutions
let level;
if (width <= 854 && height <= 480) {
level = "01"; // Level 2.1
} else if (width <= 1280 && height <= 720) {
level = "03"; // Level 2.3
} else if (width <= 1920 && height <= 1080) {
level = "04"; // Level 3.0
} else if (width <= 3840 && height <= 2160) {
level = "07"; // Level 4.0
} else {
level = "09"; // Level 4.2
}
// Default to Main tier
const tier = "M";
// Default to 8-bit depth
const bitDepth = "08";
const profile = 0;
// Determine level based on resolution
// Using a simplified level selection based on common resolutions
let level;
if (width <= 854 && height <= 480) {
level = "01"; // Level 2.1
} else if (width <= 1280 && height <= 720) {
level = "03"; // Level 2.3
} else if (width <= 1920 && height <= 1080) {
level = "04"; // Level 3.0
} else if (width <= 3840 && height <= 2160) {
level = "07"; // Level 4.0
} else {
level = "09"; // Level 4.2
}
// Default to Main tier
const tier = "M";
// Default to 8-bit depth
const bitDepth = "08";
return `av01.${profile}.${level}${tier}.${bitDepth}`;
return `av01.${profile}.${level}${tier}.${bitDepth}`;
}
throw new Error(`Unhandled codec '${codec}'.`);
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: number, sampleRate: number) => {
@@ -135,5 +137,125 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
return 'vorbis'; // Also easy, this one
}
throw new Error(`Unhandled codec '${codec}'.`);
};
throw new TypeError(`Unhandled codec '${codec}'.`);
};
export const validateVideoChunkMetadata = (metadata: EncodedVideoChunkMetadata | undefined) => {
if (!metadata) {
throw new TypeError("Video chunk metadata must be provided.");
}
if (typeof metadata !== 'object') {
throw new TypeError("Video chunk metadata must be an object.");
}
if (!metadata.decoderConfig) {
throw new TypeError("Video chunk metadata must include a decoder configuration.");
}
if (typeof metadata.decoderConfig !== 'object') {
throw new TypeError("Video chunk metadata decoder configuration must be an object.");
}
if (typeof metadata.decoderConfig.codec !== 'string') {
throw new TypeError("Video chunk metadata decoder configuration must specify a codec string.");
}
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).");
}
if (!Number.isInteger(metadata.decoderConfig.codedHeight) || metadata.decoderConfig.codedHeight! <= 0) {
throw new TypeError("Video chunk metadata decoder configuration must specify a valid codedHeight (positive integer).");
}
if (metadata.decoderConfig.description !== undefined) {
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
throw new TypeError("Video chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
}
}
if (metadata.decoderConfig.colorSpace !== undefined) {
let { colorSpace } = metadata.decoderConfig;
if (typeof colorSpace !== 'object') {
throw new TypeError("Video chunk metadata decoder configuration colorSpace, when provided, must be an object.");
}
let primariesValues = Object.keys(COLOR_PRIMARIES_MAP);
if (colorSpace.primaries != null && !primariesValues.includes(colorSpace.primaries)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace primaries, when defined, must be one of ${primariesValues.join(', ')}.`);
}
let transferValues = Object.keys(TRANSFER_CHARACTERISTICS_MAP);
if (colorSpace.transfer != null && !transferValues.includes(colorSpace.transfer)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace transfer, when defined, must be one of ${transferValues.join(', ')}.`);
}
let matrixValues = Object.keys(MATRIX_COEFFICIENTS_MAP);
if (colorSpace.matrix != null && !matrixValues.includes(colorSpace.matrix)) {
throw new TypeError(`Video chunk metadata decoder configuration colorSpace matrix, when defined, must be one of ${matrixValues.join(', ')}.`);
}
if (colorSpace.fullRange != null && typeof colorSpace.fullRange !== 'boolean') {
throw new TypeError("Video chunk metadata decoder configuration colorSpace fullRange, when defined, must be a boolean.");
}
}
if ((metadata.decoderConfig.codec.startsWith('avc1') || metadata.decoderConfig.codec.startsWith('avc3')) && !metadata.decoderConfig.description) {
// TODO Technically not correct, they can also lack a description in which case the chunks are annexb. Generally,
// go through all the codec registrations and catch these edge cases.
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) {
// TODO Same as above
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.");
}
// No added requirements for AV1 (based)
};
export const validateAudioChunkMetadata = (metadata: EncodedAudioChunkMetadata | undefined) => {
if (!metadata) {
throw new TypeError("Audio chunk metadata must be provided.");
}
if (typeof metadata !== 'object') {
throw new TypeError("Audio chunk metadata must be an object.");
}
if (!metadata.decoderConfig) {
throw new TypeError("Audio chunk metadata must include a decoder configuration.");
}
if (typeof metadata.decoderConfig !== 'object') {
throw new TypeError("Audio chunk metadata decoder configuration must be an object.");
}
if (typeof metadata.decoderConfig.codec !== 'string') {
throw new TypeError("Audio chunk metadata decoder configuration must specify a codec string.");
}
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).");
}
if (!Number.isInteger(metadata.decoderConfig.numberOfChannels) || metadata.decoderConfig.numberOfChannels! <= 0) {
throw new TypeError("Audio chunk metadata decoder configuration must specify a valid numberOfChannels (positive integer).");
}
if (metadata.decoderConfig.description !== undefined) {
if (!isAllowSharedBufferSource(metadata.decoderConfig.description)) {
throw new TypeError("Audio chunk metadata decoder configuration description, when defined, must be an ArrayBuffer or an ArrayBuffer view.");
}
}
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.');
}
};
export const validateSubtitleMetadata = (metadata: SubtitleMetadata | undefined) => {
if (!metadata) {
throw new TypeError("Subtitle metadata must be provided.");
}
if (typeof metadata !== 'object') {
throw new TypeError("Subtitle metadata must be an object.");
}
if (!metadata.config) {
throw new TypeError("Subtitle metadata must include a config object.");
}
if (typeof metadata.config !== 'object') {
throw new TypeError("Subtitle metadata config must be an object.");
}
if (typeof metadata.config.description !== 'string') {
throw new TypeError("Subtitle metadata config description must be a string.");
}
}