Files
create-vod/src/config/profiles.ts

274 lines
7.4 KiB
TypeScript
Raw Normal View History

import type { VideoProfile } from '../types';
/**
* Get optimal BPP (Bits Per Pixel) based on resolution
* Lower resolutions need higher BPP for good quality
* Higher resolutions can use lower BPP due to more pixels
*/
function getBPP(width: number, height: number): number {
const pixels = width * height;
if (pixels <= 640 * 360) return 0.08; // 360p - higher quality needed
if (pixels <= 854 * 480) return 0.075; // 480p
if (pixels <= 1280 * 720) return 0.07; // 720p
if (pixels <= 1920 * 1080) return 0.065; // 1080p
if (pixels <= 2560 * 1440) return 0.06; // 1440p (2K)
return 0.055; // 4K - lower BPP but still quality
}
/**
* Calculate optimal video bitrate based on resolution and FPS
* Formula: width × height × fps × bpp
*/
function calculateBitrate(
width: number,
height: number,
fps: number = 30,
maxBitrate?: number
): string {
const bpp = getBPP(width, height);
let bitrate = Math.round((width * height * fps * bpp) / 1000);
// Don't exceed source bitrate (no point in upscaling quality)
if (maxBitrate && bitrate > maxBitrate) {
bitrate = maxBitrate;
}
return `${bitrate}k`;
}
/**
* Default video quality profiles for 30 FPS
*/
export const DEFAULT_PROFILES: VideoProfile[] = [
{
name: '360p',
width: 640,
height: 360,
videoBitrate: calculateBitrate(640, 360, 30),
audioBitrate: '192k'
},
{
name: '480p',
width: 854,
height: 480,
videoBitrate: calculateBitrate(854, 480, 30),
audioBitrate: '192k'
},
{
name: '720p',
width: 1280,
height: 720,
videoBitrate: calculateBitrate(1280, 720, 30),
audioBitrate: '192k'
},
{
name: '1080p',
width: 1920,
height: 1080,
videoBitrate: calculateBitrate(1920, 1080, 30),
audioBitrate: '256k'
},
{
name: '1440p',
width: 2560,
height: 1440,
videoBitrate: calculateBitrate(2560, 1440, 30),
audioBitrate: '256k'
},
{
name: '2160p',
width: 3840,
height: 2160,
videoBitrate: calculateBitrate(3840, 2160, 30),
audioBitrate: '256k'
}
];
/**
* Select appropriate profiles based on input video resolution
* Oriented by height: only profiles with height <= source height
* Always generates 30 FPS profiles by default
* For high FPS (>30), user must explicitly specify in customProfiles
*/
export function selectProfiles(
inputWidth: number,
inputHeight: number,
inputFPS: number = 30,
sourceBitrate?: number
): VideoProfile[] {
const profiles: VideoProfile[] = [];
// Standard 30 FPS profiles (always created)
const baseProfiles = DEFAULT_PROFILES.filter(profile => {
return profile.height <= inputHeight;
});
// Add standard 30fps profiles with bitrate limit
for (const profile of baseProfiles) {
profiles.push({
...profile,
videoBitrate: calculateBitrate(profile.width, profile.height, 30, sourceBitrate),
fps: 30
});
}
return profiles;
}
/**
* Create high FPS profile variant
* Used for creating 60fps, 90fps, 120fps versions
*/
export function createHighFPSProfile(
baseProfile: VideoProfile,
fps: number,
maxBitrate?: number
): VideoProfile {
return {
...baseProfile,
name: `${baseProfile.name}-${fps}`,
videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, fps, maxBitrate),
fps
};
}
/**
* Parse profile string into resolution and FPS
* Examples:
* '360' => { resolution: '360p', fps: 30 }
* '720@60' => { resolution: '720p', fps: 60 }
* '1080-60' => { resolution: '1080p', fps: 60 }
* '360p', '720p@60' also supported (with 'p')
*/
function parseProfileString(profileStr: string): { resolution: string; fps: number } | null {
const trimmed = profileStr.trim();
// Match patterns: 360, 720@60, 1080-60, 360p, 720p@60, 1080p-60
const match = trimmed.match(/^(\d+)p?(?:[@-](\d+))?$/i);
if (!match) {
return null;
}
const resolution = match[1] + 'p'; // Always add 'p'
const fps = match[2] ? parseInt(match[2]) : 30;
return { resolution, fps };
}
/**
* Get profile by resolution name and FPS
* Returns VideoProfile or null if not found
*/
export function getProfileByName(
resolution: string,
fps: number = 30,
maxBitrate?: number
): VideoProfile | null {
const baseProfile = DEFAULT_PROFILES.find(p => p.name === resolution);
if (!baseProfile) {
return null;
}
if (fps === 30) {
return {
...baseProfile,
videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, 30, maxBitrate),
fps: 30
};
}
return createHighFPSProfile(baseProfile, fps, maxBitrate);
}
/**
* Validate if profile can be created from source
* Returns object with error, warning, and adjusted FPS
*/
export function validateProfile(
profileStr: string,
sourceWidth: number,
sourceHeight: number,
sourceFPS: number
): { error?: string; warning?: string; adjustedFps?: number } {
const parsed = parseProfileString(profileStr);
if (!parsed) {
return { error: `Invalid profile format: ${profileStr}. Use format like: 360, 720@60, 1080-60` };
}
const profile = getProfileByName(parsed.resolution, parsed.fps);
if (!profile) {
return { error: `Unknown resolution: ${parsed.resolution}. Available: 360, 480, 720, 1080, 1440, 2160` };
}
// Check if source supports this resolution
if (profile.height > sourceHeight) {
return { error: `Source height (${sourceHeight}px) is lower than requested ${profileStr} height (${profile.height}px)` };
}
// Check if requested FPS exceeds source FPS
const MAX_FPS = 120;
let adjustedFps = parsed.fps;
let warning: string | undefined;
if (parsed.fps > sourceFPS) {
// Cap to source FPS (but not more than MAX_FPS)
adjustedFps = Math.min(sourceFPS, MAX_FPS);
warning = `Requested ${parsed.fps} FPS in ${profileStr}, but source is ${sourceFPS} FPS. Using ${adjustedFps} FPS instead`;
} else if (parsed.fps > MAX_FPS) {
// Cap to MAX_FPS
adjustedFps = MAX_FPS;
warning = `Requested ${parsed.fps} FPS in ${profileStr} exceeds maximum ${MAX_FPS} FPS. Using ${adjustedFps} FPS instead`;
}
return warning ? { warning, adjustedFps } : {}; // Valid
}
/**
* Create profiles from custom string list
* Example: ['360p', '720p@60', '1080p'] => VideoProfile[]
*/
export function createProfilesFromStrings(
profileStrings: string[],
sourceWidth: number,
sourceHeight: number,
sourceFPS: number,
sourceBitrate?: number
): { profiles: VideoProfile[]; errors: string[]; warnings: string[] } {
const profiles: VideoProfile[] = [];
const errors: string[] = [];
const warnings: string[] = [];
for (const profileStr of profileStrings) {
// Validate
const result = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS);
if (result.error) {
errors.push(result.error);
continue;
}
if (result.warning) {
warnings.push(result.warning);
}
// Parse and create
const parsed = parseProfileString(profileStr);
if (!parsed) continue; // Already validated, shouldn't happen
// Use adjusted FPS if available (when requested FPS > source FPS)
const targetFps = result.adjustedFps !== undefined ? result.adjustedFps : parsed.fps;
const profile = getProfileByName(parsed.resolution, targetFps, sourceBitrate);
if (profile) {
profiles.push(profile);
}
}
return { profiles, errors, warnings };
}