diff --git a/src/config/profiles.ts b/src/config/profiles.ts index e08df6d..3190df1 100644 --- a/src/config/profiles.ts +++ b/src/config/profiles.ts @@ -86,12 +86,10 @@ export const DEFAULT_PROFILES: VideoProfile[] = [ ]; /** - * Select appropriate profiles based on input video resolution and FPS + * Select appropriate profiles based on input video resolution * Only creates profiles that are equal to or smaller than input resolution - * Creates high FPS variants if source supports it (according to FEATURES.md): - * - 60 FPS versions if source >= 45 FPS - * - 90 FPS versions if source >= 75 FPS - * - 120 FPS versions if source >= 95 FPS + * Always generates 30 FPS profiles by default + * For high FPS (>30), user must explicitly specify in customProfiles */ export function selectProfiles( inputWidth: number, @@ -110,31 +108,11 @@ export function selectProfiles( for (const profile of baseProfiles) { profiles.push({ ...profile, - videoBitrate: calculateBitrate(profile.width, profile.height, 30, sourceBitrate) + videoBitrate: calculateBitrate(profile.width, profile.height, 30, sourceBitrate), + fps: 30 }); } - // Add 60 FPS profiles if source >= 45 FPS - if (inputFPS >= 45) { - for (const profile of baseProfiles) { - profiles.push(createHighFPSProfile(profile, 60, sourceBitrate)); - } - } - - // Add 90 FPS profiles if source >= 75 FPS - if (inputFPS >= 75) { - for (const profile of baseProfiles) { - profiles.push(createHighFPSProfile(profile, 90, sourceBitrate)); - } - } - - // Add 120 FPS profiles if source >= 95 FPS - if (inputFPS >= 95) { - for (const profile of baseProfiles) { - profiles.push(createHighFPSProfile(profile, 120, sourceBitrate)); - } - } - return profiles; } @@ -150,7 +128,8 @@ export function createHighFPSProfile( return { ...baseProfile, name: `${baseProfile.name}-${fps}`, - videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, fps, maxBitrate) + videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, fps, maxBitrate), + fps }; } @@ -196,7 +175,8 @@ export function getProfileByName( if (fps === 30) { return { ...baseProfile, - videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, 30, maxBitrate) + videoBitrate: calculateBitrate(baseProfile.width, baseProfile.height, 30, maxBitrate), + fps: 30 }; } @@ -205,37 +185,47 @@ export function getProfileByName( /** * Validate if profile can be created from source - * Returns error message or null if valid + * Returns object with error, warning, and adjusted FPS */ export function validateProfile( profileStr: string, sourceWidth: number, sourceHeight: number, sourceFPS: number -): string | null { +): { error?: string; warning?: string; adjustedFps?: number } { const parsed = parseProfileString(profileStr); if (!parsed) { - return `Invalid profile format: ${profileStr}. Use format like: 360, 720@60, 1080-60`; + return { error: `Invalid profile format: ${profileStr}. Use format like: 360, 720@60, 1080-60` }; } const profile = getProfileByName(parsed.resolution, parsed.fps); if (!profile) { - return `Unknown resolution: ${parsed.resolution}. Available: 360, 480, 720, 1080, 1440, 2160`; + return { error: `Unknown resolution: ${parsed.resolution}. Available: 360, 480, 720, 1080, 1440, 2160` }; } // Check if source supports this resolution if (profile.width > sourceWidth || profile.height > sourceHeight) { - return `Source resolution (${sourceWidth}x${sourceHeight}) is lower than ${profileStr} (${profile.width}x${profile.height})`; + return { error: `Source resolution (${sourceWidth}x${sourceHeight}) is lower than ${profileStr} (${profile.width}x${profile.height})` }; } - // Check if source supports this FPS + // Check if requested FPS exceeds source FPS + const MAX_FPS = 120; + let adjustedFps = parsed.fps; + let warning: string | undefined; + if (parsed.fps > sourceFPS) { - return `Source FPS (${sourceFPS}) is lower than requested ${parsed.fps} FPS in ${profileStr}`; + // 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 null; // Valid + return warning ? { warning, adjustedFps } : {}; // Valid } /** @@ -248,29 +238,37 @@ export function createProfilesFromStrings( sourceHeight: number, sourceFPS: number, sourceBitrate?: number -): { profiles: VideoProfile[]; errors: string[] } { +): { profiles: VideoProfile[]; errors: string[]; warnings: string[] } { const profiles: VideoProfile[] = []; const errors: string[] = []; + const warnings: string[] = []; for (const profileStr of profileStrings) { // Validate - const error = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS); + const result = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS); - if (error) { - errors.push(error); + 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 - const profile = getProfileByName(parsed.resolution, parsed.fps, sourceBitrate); + // 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 }; + return { profiles, errors, warnings }; } diff --git a/src/core/converter.ts b/src/core/converter.ts index 2edb011..e08bb30 100644 --- a/src/core/converter.ts +++ b/src/core/converter.ts @@ -140,17 +140,26 @@ async function convertToDashInternal( // Show errors if any if (result.errors.length > 0) { - console.warn('\n⚠️ Profile warnings:'); + console.warn('\n❌ Profile errors:'); for (const error of result.errors) { console.warn(` - ${error}`); } console.warn(''); } + // Show warnings if any + if (result.warnings.length > 0) { + console.warn('\n⚠️ Profile warnings:'); + for (const warning of result.warnings) { + console.warn(` - ${warning}`); + } + console.warn(''); + } + profiles = result.profiles; if (profiles.length === 0) { - throw new Error('No valid profiles found in custom list. Check warnings above.'); + throw new Error('No valid profiles found in custom list. Check errors above.'); } } else if (userProfiles) { // Programmatic API usage @@ -222,7 +231,6 @@ async function convertToDashInternal( codecPreset, metadata.duration, segmentDuration, - metadata.fps || 25, metadata.audioBitrate, parallel, maxConcurrent, diff --git a/src/core/encoding.ts b/src/core/encoding.ts index 74cabbd..34d975e 100644 --- a/src/core/encoding.ts +++ b/src/core/encoding.ts @@ -14,7 +14,6 @@ export async function encodeProfileToMP4( preset: string, duration: number, segmentDuration: number, - fps: number, sourceAudioBitrate: number | undefined, codecType: 'h264' | 'av1', optimizations?: VideoOptimizations, @@ -70,6 +69,7 @@ export async function encodeProfileToMP4( // Set GOP size for DASH segments // Keyframes must align with segment boundaries + const fps = profile.fps || 30; const gopSize = Math.round(fps * segmentDuration); args.push( '-g', String(gopSize), // GOP size (e.g., 25 fps * 2 sec = 50 frames) @@ -126,7 +126,6 @@ export async function encodeProfilesToMP4( preset: string, duration: number, segmentDuration: number, - fps: number, sourceAudioBitrate: number | undefined, parallel: boolean, maxConcurrent: number, @@ -149,7 +148,6 @@ export async function encodeProfilesToMP4( preset, duration, segmentDuration, - fps, sourceAudioBitrate, codecType, optimizations, @@ -178,7 +176,6 @@ export async function encodeProfilesToMP4( preset, duration, segmentDuration, - fps, sourceAudioBitrate, codecType, optimizations, diff --git a/src/types/index.ts b/src/types/index.ts index 7c76ea7..dd96f58 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -65,6 +65,9 @@ export interface VideoProfile { /** Audio bitrate (e.g., "128k") */ audioBitrate: string; + + /** Target FPS for this profile (default: 30) */ + fps?: number; } /**