This commit is contained in:
2025-11-09 03:28:43 +03:00
parent 79bab28e0c
commit 3086d6907c
5 changed files with 184 additions and 35 deletions

View File

@@ -10,8 +10,9 @@
* dvc ./video.mp4 ./output
*/
import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box } from './index';
import { convertToDash, checkFFmpeg, checkNvenc, checkMP4Box, getVideoMetadata } from './index';
import cliProgress from 'cli-progress';
import { statSync } from 'node:fs';
const input = process.argv[2];
const outputDir = process.argv[3] || './output';
@@ -41,8 +42,26 @@ if (!hasMP4Box) {
process.exit(1);
}
console.log(`📹 Input: ${input}`);
console.log(`📁 Output: ${outputDir}\n`);
// Get video metadata and file size
console.log('📊 Analyzing video...\n');
const metadata = await getVideoMetadata(input);
const fileStats = statSync(input);
const fileSizeMB = (fileStats.size / (1024 * 1024)).toFixed(2);
console.log('📹 Video Information:');
console.log(` File: ${input}`);
console.log(` Size: ${fileSizeMB} MB`);
console.log(` Resolution: ${metadata.width}x${metadata.height}`);
console.log(` FPS: ${metadata.fps.toFixed(2)}`);
console.log(` Duration: ${Math.floor(metadata.duration / 60)}m ${Math.floor(metadata.duration % 60)}s`);
console.log(` Codec: ${metadata.codec}`);
if (metadata.videoBitrate) {
console.log(` Video Bitrate: ${(metadata.videoBitrate / 1000).toFixed(2)} Mbps`);
}
if (metadata.audioBitrate) {
console.log(` Audio Bitrate: ${metadata.audioBitrate} kbps`);
}
console.log(`\n📁 Output: ${outputDir}\n`);
console.log('🚀 Starting conversion...\n');
// Create multibar container

View File

@@ -1,45 +1,156 @@
import type { VideoProfile } from '../types';
/**
* Default video quality profiles
* 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: '1080p',
width: 1920,
height: 1080,
videoBitrate: '5000k',
audioBitrate: '256k'
},
{
name: '720p',
width: 1280,
height: 720,
videoBitrate: '3000k',
audioBitrate: '256k'
name: '360p',
width: 640,
height: 360,
videoBitrate: calculateBitrate(640, 360, 30),
audioBitrate: '192k'
},
{
name: '480p',
width: 854,
height: 480,
videoBitrate: '1500k',
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: '360p',
width: 640,
height: 360,
videoBitrate: '800k',
name: '1440p',
width: 2560,
height: 1440,
videoBitrate: calculateBitrate(2560, 1440, 30),
audioBitrate: '256k'
},
{
name: '4K',
width: 3840,
height: 2160,
videoBitrate: calculateBitrate(3840, 2160, 30),
audioBitrate: '256k'
}
];
/**
* Select appropriate profiles based on input video resolution
* Select appropriate profiles based on input video resolution and FPS
* 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
*/
export function selectProfiles(inputWidth: number, inputHeight: number): VideoProfile[] {
return DEFAULT_PROFILES.filter(profile => {
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.width <= inputWidth && 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)
});
}
// 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;
}
/**
* 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)
};
}

View File

@@ -112,7 +112,12 @@ async function convertToDashInternal(
}
// Select profiles
const profiles = userProfiles || selectProfiles(metadata.width, metadata.height);
const profiles = userProfiles || selectProfiles(
metadata.width,
metadata.height,
metadata.fps,
metadata.videoBitrate
);
if (profiles.length === 0) {
throw new Error('No suitable profiles found for input video resolution');

View File

@@ -123,6 +123,7 @@ export interface VideoMetadata {
fps: number;
codec: string;
audioBitrate?: number; // Битрейт аудио в kbps
videoBitrate?: number; // Битрейт видео в kbps
}
/**

View File

@@ -8,10 +8,7 @@ export async function getVideoMetadata(inputPath: string): Promise<VideoMetadata
return new Promise((resolve, reject) => {
const proc = spawn('ffprobe', [
'-v', 'error',
'-select_streams', 'v:0',
'-show_entries', 'stream=width,height,duration,r_frame_rate,codec_name',
'-select_streams', 'a:0',
'-show_entries', 'stream=bit_rate',
'-show_entries', 'stream=width,height,duration,r_frame_rate,codec_name,codec_type,bit_rate',
'-show_entries', 'format=duration',
'-of', 'json',
inputPath
@@ -36,13 +33,23 @@ export async function getVideoMetadata(inputPath: string): Promise<VideoMetadata
try {
const data = JSON.parse(output);
const videoStream = data.streams.find((s: any) => s.width !== undefined);
const audioStream = data.streams.find((s: any) => s.bit_rate !== undefined && s.width === undefined);
const videoStream = data.streams.find((s: any) => s.codec_type === 'video');
const audioStream = data.streams.find((s: any) => s.codec_type === 'audio' && s.bit_rate);
const format = data.format;
// Parse frame rate
if (!videoStream) {
reject(new Error('No video stream found in input file'));
return;
}
// Parse frame rate (handle missing or malformed r_frame_rate)
let fps = 30; // default fallback
if (videoStream.r_frame_rate) {
const [num, den] = videoStream.r_frame_rate.split('/').map(Number);
const fps = num / den;
if (num && den && den !== 0) {
fps = num / den;
}
}
// Get duration from stream or format
const duration = parseFloat(videoStream.duration || format.duration || '0');
@@ -52,13 +59,19 @@ export async function getVideoMetadata(inputPath: string): Promise<VideoMetadata
? Math.round(parseInt(audioStream.bit_rate) / 1000)
: undefined;
// Get video bitrate in kbps
const videoBitrate = videoStream.bit_rate
? Math.round(parseInt(videoStream.bit_rate) / 1000)
: undefined;
resolve({
width: videoStream.width,
height: videoStream.height,
duration,
fps,
codec: videoStream.codec_name,
audioBitrate
audioBitrate,
videoBitrate
});
} catch (err) {
reject(new Error(`Failed to parse ffprobe output: ${err}`));