Дефолтный ФПС 30 + ограничения максимального в 120 и целевого видео

This commit is contained in:
2025-11-11 21:25:37 +03:00
parent 2da2b584fa
commit 8cf4210d20
4 changed files with 56 additions and 50 deletions

View File

@@ -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 };
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -65,6 +65,9 @@ export interface VideoProfile {
/** Audio bitrate (e.g., "128k") */
audioBitrate: string;
/** Target FPS for this profile (default: 30) */
fps?: number;
}
/**