Files
adaptive-video-converter/src/utils/system.ts
2025-11-11 21:07:51 +03:00

162 lines
4.2 KiB
TypeScript

import { spawn } from 'node:child_process';
/**
* Check if FFmpeg is available
*/
export async function checkFFmpeg(): Promise<boolean> {
return new Promise((resolve) => {
const proc = spawn('ffmpeg', ['-version']);
proc.on('error', () => resolve(false));
proc.on('close', (code) => resolve(code === 0));
});
}
/**
* Check if MP4Box is available
*/
export async function checkMP4Box(): Promise<boolean> {
return new Promise((resolve) => {
const proc = spawn('MP4Box', ['-version']);
proc.on('error', () => resolve(false));
proc.on('close', (code) => resolve(code === 0));
});
}
/**
* Check if NVENC is available
*/
export async function checkNvenc(): Promise<boolean> {
return new Promise((resolve) => {
const proc = spawn('ffmpeg', ['-hide_banner', '-encoders']);
let output = '';
proc.stdout.on('data', (data) => {
output += data.toString();
});
proc.on('error', () => resolve(false));
proc.on('close', (code) => {
if (code !== 0) {
resolve(false);
} else {
resolve(output.includes('h264_nvenc') || output.includes('hevc_nvenc'));
}
});
});
}
/**
* Check if AV1 hardware encoding is available
* Supports: NVENC (RTX 40xx), QSV (Intel 11+), AMF (AMD RX 7000)
*/
export async function checkAV1Support(): Promise<{
available: boolean;
encoder?: 'av1_nvenc' | 'av1_qsv' | 'av1_amf';
}> {
return new Promise((resolve) => {
const proc = spawn('ffmpeg', ['-hide_banner', '-encoders']);
let output = '';
proc.stdout.on('data', (data) => {
output += data.toString();
});
proc.on('error', () => resolve({ available: false }));
proc.on('close', (code) => {
if (code !== 0) {
resolve({ available: false });
} else {
// Check for hardware AV1 encoders in order of preference
if (output.includes('av1_nvenc')) {
resolve({ available: true, encoder: 'av1_nvenc' });
} else if (output.includes('av1_qsv')) {
resolve({ available: true, encoder: 'av1_qsv' });
} else if (output.includes('av1_amf')) {
resolve({ available: true, encoder: 'av1_amf' });
} else {
resolve({ available: false });
}
}
});
});
}
/**
* Execute FFmpeg command with progress tracking
*/
export async function execFFmpeg(
args: string[],
onProgress?: (percent: number) => void,
duration?: number
): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('ffmpeg', args);
let stderrData = '';
proc.stderr.on('data', (data) => {
const text = data.toString();
stderrData += text;
if (onProgress && duration) {
// Parse time from FFmpeg output: time=00:01:23.45
const timeMatch = text.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
const seconds = parseFloat(timeMatch[3]);
const currentTime = hours * 3600 + minutes * 60 + seconds;
const percent = Math.min(100, (currentTime / duration) * 100);
onProgress(percent);
}
}
});
proc.on('error', (err) => {
reject(new Error(`FFmpeg error: ${err.message}`));
});
proc.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`FFmpeg failed with exit code ${code}\n${stderrData}`));
}
});
});
}
/**
* Execute MP4Box command
*/
export async function execMP4Box(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('MP4Box', args);
let stdoutData = '';
let stderrData = '';
proc.stdout.on('data', (data) => {
stdoutData += data.toString();
});
proc.stderr.on('data', (data) => {
stderrData += data.toString();
});
proc.on('error', (err) => {
reject(new Error(`MP4Box error: ${err.message}`));
});
proc.on('close', (code) => {
if (code === 0) {
resolve();
} else {
const output = stderrData || stdoutData;
reject(new Error(`MP4Box failed with exit code ${code}\n${output}`));
}
});
});
}