Дефолтный ФПС 30 + ограничения максимального в 120 и целевого видео
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,6 +65,9 @@ export interface VideoProfile {
|
||||
|
||||
/** Audio bitrate (e.g., "128k") */
|
||||
audioBitrate: string;
|
||||
|
||||
/** Target FPS for this profile (default: 30) */
|
||||
fps?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user