130 lines
3.8 KiB
JavaScript
Executable File
130 lines
3.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import {spawn} from 'child_process';
|
|
import {readdir} from 'fs/promises';
|
|
import {createInterface} from 'node:readline';
|
|
import {runInBackground} from '../lib/ai-generated.ts';
|
|
import { extname } from 'node:path';
|
|
|
|
|
|
function formatTime(seconds: number): string {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
async function getDuration(inputFile: string): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const ffprobe = spawn('ffprobe', [
|
|
'-v', 'quiet',
|
|
'-show_entries', 'format=duration',
|
|
'-of', 'csv=p=0',
|
|
inputFile
|
|
]);
|
|
|
|
let output = '';
|
|
|
|
ffprobe.stdout.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
ffprobe.on('close', (code) => {
|
|
if (code === 0) {
|
|
const duration = parseFloat(output.trim());
|
|
resolve(isNaN(duration) ? 0 : duration);
|
|
} else {
|
|
reject(new Error(`ffprobe failed with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
async function compressVideo(inputFile: string, outputFile: string) {
|
|
const duration = await getDuration(inputFile);
|
|
return new Promise<void>((resolve, reject) => {
|
|
try {
|
|
const ffmpeg = spawn('ffmpeg', [
|
|
'-hwaccel', 'cuda',
|
|
'-hide_banner',
|
|
'-loglevel', 'error',
|
|
'-progress', 'pipe:1',
|
|
'-i', inputFile,
|
|
// '-c:v', 'h264_nvenc',
|
|
// '-cq', '23',
|
|
'-c:v', 'hevc_nvenc',
|
|
'-cq', '28',
|
|
'-preset', 'p4',
|
|
'-c:a', 'copy',
|
|
outputFile
|
|
], {
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
const rl = createInterface({
|
|
input: ffmpeg.stdout
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
rl.on('line', (line: string) => {
|
|
if (line.startsWith('out_time_us=')) {
|
|
const currentUs = parseInt(line.split('=')[1]);
|
|
const currentSec = Math.floor(currentUs / 1000000);
|
|
|
|
if (duration > 0) {
|
|
const percent = Math.min(100, Math.floor((currentSec * 100) / duration));
|
|
|
|
// Вычисляем оставшееся время
|
|
const elapsedMs = Date.now() - startTime;
|
|
const elapsedSec = Math.floor(elapsedMs / 1000);
|
|
|
|
let remainingStr = '';
|
|
if (percent > 0 && elapsedSec > 3) { // Показываем только после 3 секунд для точности
|
|
const totalEstimatedSec = Math.floor((elapsedSec * 100) / percent);
|
|
const remainingSec = Math.max(0, totalEstimatedSec - elapsedSec);
|
|
remainingStr = ` | ETA: ${formatTime(remainingSec)}`;
|
|
}
|
|
|
|
process.stdout.write(`\r${percent.toString().padStart(3)}%${remainingStr}`);
|
|
}
|
|
}
|
|
});
|
|
ffmpeg.on('close', (code) => {
|
|
console.log(); // Новая строка в конце
|
|
if (code === 0) {
|
|
console.log(`✅ Completed: ${outputFile}`);
|
|
resolve();
|
|
} else {
|
|
console.error(`❌ Failed with code ${code}`);
|
|
reject();
|
|
}
|
|
});
|
|
|
|
ffmpeg.stderr.on('data', (data) => {
|
|
console.error(`Error: ${data}`);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`Error: ${error}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
const files = (await readdir('.')).filter(name => {
|
|
return (!name.startsWith('HEVC') && ['mp4', 'mov'].includes(extname(name).toLowerCase().slice(1)))
|
|
});
|
|
let filesCount = files.length;
|
|
|
|
for (let i = 0; i < filesCount; i++){
|
|
const file = files[i];
|
|
|
|
// const outputFile = `HEVC_${file.slice(4)}`;
|
|
const outputFile = `HEVC_${file}`;
|
|
console.log(`\nProcessing: ${file} [${i + 1}/${filesCount}]`);
|
|
await compressVideo(file, outputFile);
|
|
// runInBackground('identity', [file, outputFile])
|
|
}
|