diff --git a/bin/compress-videos-in-dir.ts b/bin/compress-videos-in-dir.ts new file mode 100755 index 0000000..fb8e56f --- /dev/null +++ b/bin/compress-videos-in-dir.ts @@ -0,0 +1,121 @@ +#!/usr/bin/env node + +import {spawn} from 'child_process'; +import {readdir} from 'fs/promises'; +import {createInterface} from 'node:readline'; + + +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 { + 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(resolve => { + 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}`); + } else { + console.error(`❌ Failed with code ${code}`); + } + resolve(); + }); + + ffmpeg.stderr.on('data', (data) => { + console.error(`Error: ${data}`); + }); + + } catch (error) { + console.error(`Error: ${error}`); + } + }); +} + +const files = await readdir('.'); +const mp4Files = files.filter(file => file.match(/^VID_.*\.mp4$/)); +const filesCount = mp4Files.length; + +for (let i = 0; i < filesCount; i++){ + const file = mp4Files[i]; + const outputFile = `HEVC_${file.slice(4)}`; + console.log(`\nProcessing: ${file} [${i + 1}/${filesCount}]`); + await compressVideo(file, outputFile); +} diff --git a/tsconfig.json b/tsconfig.json index 2e719ca..e399eda 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "allowImportingTsExtensions": true, "target": "esnext", + "module": "esnext", "moduleResolution": "nodenext" } }