162 lines
4.2 KiB
TypeScript
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}`));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|