Дефолтный ФПС 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
|
* Only creates profiles that are equal to or smaller than input resolution
|
||||||
* Creates high FPS variants if source supports it (according to FEATURES.md):
|
* Always generates 30 FPS profiles by default
|
||||||
* - 60 FPS versions if source >= 45 FPS
|
* For high FPS (>30), user must explicitly specify in customProfiles
|
||||||
* - 90 FPS versions if source >= 75 FPS
|
|
||||||
* - 120 FPS versions if source >= 95 FPS
|
|
||||||
*/
|
*/
|
||||||
export function selectProfiles(
|
export function selectProfiles(
|
||||||
inputWidth: number,
|
inputWidth: number,
|
||||||
@@ -110,31 +108,11 @@ export function selectProfiles(
|
|||||||
for (const profile of baseProfiles) {
|
for (const profile of baseProfiles) {
|
||||||
profiles.push({
|
profiles.push({
|
||||||
...profile,
|
...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;
|
return profiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +128,8 @@ export function createHighFPSProfile(
|
|||||||
return {
|
return {
|
||||||
...baseProfile,
|
...baseProfile,
|
||||||
name: `${baseProfile.name}-${fps}`,
|
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) {
|
if (fps === 30) {
|
||||||
return {
|
return {
|
||||||
...baseProfile,
|
...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
|
* 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(
|
export function validateProfile(
|
||||||
profileStr: string,
|
profileStr: string,
|
||||||
sourceWidth: number,
|
sourceWidth: number,
|
||||||
sourceHeight: number,
|
sourceHeight: number,
|
||||||
sourceFPS: number
|
sourceFPS: number
|
||||||
): string | null {
|
): { error?: string; warning?: string; adjustedFps?: number } {
|
||||||
const parsed = parseProfileString(profileStr);
|
const parsed = parseProfileString(profileStr);
|
||||||
|
|
||||||
if (!parsed) {
|
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);
|
const profile = getProfileByName(parsed.resolution, parsed.fps);
|
||||||
|
|
||||||
if (!profile) {
|
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
|
// Check if source supports this resolution
|
||||||
if (profile.width > sourceWidth || profile.height > sourceHeight) {
|
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) {
|
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,
|
sourceHeight: number,
|
||||||
sourceFPS: number,
|
sourceFPS: number,
|
||||||
sourceBitrate?: number
|
sourceBitrate?: number
|
||||||
): { profiles: VideoProfile[]; errors: string[] } {
|
): { profiles: VideoProfile[]; errors: string[]; warnings: string[] } {
|
||||||
const profiles: VideoProfile[] = [];
|
const profiles: VideoProfile[] = [];
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
for (const profileStr of profileStrings) {
|
for (const profileStr of profileStrings) {
|
||||||
// Validate
|
// Validate
|
||||||
const error = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS);
|
const result = validateProfile(profileStr, sourceWidth, sourceHeight, sourceFPS);
|
||||||
|
|
||||||
if (error) {
|
if (result.error) {
|
||||||
errors.push(error);
|
errors.push(result.error);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.warning) {
|
||||||
|
warnings.push(result.warning);
|
||||||
|
}
|
||||||
|
|
||||||
// Parse and create
|
// Parse and create
|
||||||
const parsed = parseProfileString(profileStr);
|
const parsed = parseProfileString(profileStr);
|
||||||
if (!parsed) continue; // Already validated, shouldn't happen
|
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) {
|
if (profile) {
|
||||||
profiles.push(profile);
|
profiles.push(profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { profiles, errors };
|
return { profiles, errors, warnings };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,17 +140,26 @@ async function convertToDashInternal(
|
|||||||
|
|
||||||
// Show errors if any
|
// Show errors if any
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
console.warn('\n⚠️ Profile warnings:');
|
console.warn('\n❌ Profile errors:');
|
||||||
for (const error of result.errors) {
|
for (const error of result.errors) {
|
||||||
console.warn(` - ${error}`);
|
console.warn(` - ${error}`);
|
||||||
}
|
}
|
||||||
console.warn('');
|
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;
|
profiles = result.profiles;
|
||||||
|
|
||||||
if (profiles.length === 0) {
|
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) {
|
} else if (userProfiles) {
|
||||||
// Programmatic API usage
|
// Programmatic API usage
|
||||||
@@ -222,7 +231,6 @@ async function convertToDashInternal(
|
|||||||
codecPreset,
|
codecPreset,
|
||||||
metadata.duration,
|
metadata.duration,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
metadata.fps || 25,
|
|
||||||
metadata.audioBitrate,
|
metadata.audioBitrate,
|
||||||
parallel,
|
parallel,
|
||||||
maxConcurrent,
|
maxConcurrent,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export async function encodeProfileToMP4(
|
|||||||
preset: string,
|
preset: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
fps: number,
|
|
||||||
sourceAudioBitrate: number | undefined,
|
sourceAudioBitrate: number | undefined,
|
||||||
codecType: 'h264' | 'av1',
|
codecType: 'h264' | 'av1',
|
||||||
optimizations?: VideoOptimizations,
|
optimizations?: VideoOptimizations,
|
||||||
@@ -70,6 +69,7 @@ export async function encodeProfileToMP4(
|
|||||||
|
|
||||||
// Set GOP size for DASH segments
|
// Set GOP size for DASH segments
|
||||||
// Keyframes must align with segment boundaries
|
// Keyframes must align with segment boundaries
|
||||||
|
const fps = profile.fps || 30;
|
||||||
const gopSize = Math.round(fps * segmentDuration);
|
const gopSize = Math.round(fps * segmentDuration);
|
||||||
args.push(
|
args.push(
|
||||||
'-g', String(gopSize), // GOP size (e.g., 25 fps * 2 sec = 50 frames)
|
'-g', String(gopSize), // GOP size (e.g., 25 fps * 2 sec = 50 frames)
|
||||||
@@ -126,7 +126,6 @@ export async function encodeProfilesToMP4(
|
|||||||
preset: string,
|
preset: string,
|
||||||
duration: number,
|
duration: number,
|
||||||
segmentDuration: number,
|
segmentDuration: number,
|
||||||
fps: number,
|
|
||||||
sourceAudioBitrate: number | undefined,
|
sourceAudioBitrate: number | undefined,
|
||||||
parallel: boolean,
|
parallel: boolean,
|
||||||
maxConcurrent: number,
|
maxConcurrent: number,
|
||||||
@@ -149,7 +148,6 @@ export async function encodeProfilesToMP4(
|
|||||||
preset,
|
preset,
|
||||||
duration,
|
duration,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
fps,
|
|
||||||
sourceAudioBitrate,
|
sourceAudioBitrate,
|
||||||
codecType,
|
codecType,
|
||||||
optimizations,
|
optimizations,
|
||||||
@@ -178,7 +176,6 @@ export async function encodeProfilesToMP4(
|
|||||||
preset,
|
preset,
|
||||||
duration,
|
duration,
|
||||||
segmentDuration,
|
segmentDuration,
|
||||||
fps,
|
|
||||||
sourceAudioBitrate,
|
sourceAudioBitrate,
|
||||||
codecType,
|
codecType,
|
||||||
optimizations,
|
optimizations,
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ export interface VideoProfile {
|
|||||||
|
|
||||||
/** Audio bitrate (e.g., "128k") */
|
/** Audio bitrate (e.g., "128k") */
|
||||||
audioBitrate: string;
|
audioBitrate: string;
|
||||||
|
|
||||||
|
/** Target FPS for this profile (default: 30) */
|
||||||
|
fps?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user