Initial clean project import
This commit is contained in:
@@ -0,0 +1,726 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import * as fs from 'fs';
|
||||
import { shell } from 'electron';
|
||||
import { AppEnv, waitAppEnvReady } from '../env';
|
||||
import { Log } from '../log';
|
||||
import { getFFmpegExecutablePath, getFFprobeExecutablePath, getPythonExecutablePath, resourceExists, isDevelopment } from '../../lib/resource-path';
|
||||
import { normalizePath, normalizeOutputPath } from '../../lib/path-util';
|
||||
import { getMacOSPythonPath } from '../../lib/python-setup';
|
||||
|
||||
const isWin = process.platform === 'win32';
|
||||
const isMac = process.platform === 'darwin';
|
||||
|
||||
// Python路径缓存,避免重复验证
|
||||
let cachedPythonPath: string | null = null;
|
||||
|
||||
const ensureWritableDirectory = (candidates: Array<string | null | undefined>): string => {
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate || typeof candidate !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(candidate, { recursive: true });
|
||||
return candidate;
|
||||
} catch (error) {
|
||||
Log.warn('ensureWritableDirectory: failed to create directory', {
|
||||
path: candidate,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = process.env.TEMP || process.env.TMP || process.cwd();
|
||||
fs.mkdirSync(fallback, { recursive: true });
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const getRuntimeTempRoot = (): string => {
|
||||
const appRoot = AppEnv.appRoot || process.env.APP_ROOT || process.cwd();
|
||||
return ensureWritableDirectory([
|
||||
AppEnv.tempRoot,
|
||||
AppEnv.dataRoot ? path.join(AppEnv.dataRoot, 'temp') : null,
|
||||
AppEnv.userData ? path.join(AppEnv.userData, 'temp') : null,
|
||||
appRoot ? path.join(appRoot, 'data', 'temp') : null,
|
||||
path.join(process.cwd(), 'data', 'temp'),
|
||||
]);
|
||||
};
|
||||
|
||||
export const getRuntimeFontconfigCacheDir = (): string => {
|
||||
return ensureWritableDirectory([
|
||||
path.join(getRuntimeTempRoot(), 'fontconfig-cache'),
|
||||
AppEnv.userData ? path.join(AppEnv.userData, 'fontconfig-cache') : null,
|
||||
path.join(process.cwd(), 'data', 'temp', 'fontconfig-cache'),
|
||||
]);
|
||||
};
|
||||
|
||||
export const buildRuntimeProcessEnv = (extraEnv?: Record<string, string | undefined>): NodeJS.ProcessEnv => {
|
||||
const tempRoot = getRuntimeTempRoot();
|
||||
const fontCacheDir = getRuntimeFontconfigCacheDir();
|
||||
|
||||
return {
|
||||
...process.env,
|
||||
TEMP: tempRoot,
|
||||
TMP: tempRoot,
|
||||
TMPDIR: tempRoot,
|
||||
FONTCONFIG_FILE: '',
|
||||
FONTCONFIG_PATH: '',
|
||||
FONTCONFIG_CACHE: fontCacheDir,
|
||||
XDG_CACHE_HOME: tempRoot,
|
||||
...(extraEnv || {}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 动态检测 Python 可执行文件路径
|
||||
* 优先级:系统 PATH 中的 python > py > python3
|
||||
* @returns Promise<string> Python 可执行文件路径
|
||||
*/
|
||||
export const getPythonPath = async (): Promise<string> => {
|
||||
// 如果已经缓存了有效路径,直接返回
|
||||
if (cachedPythonPath) {
|
||||
Log.debug('getPythonPath: 使用缓存路径', { path: cachedPythonPath });
|
||||
return cachedPythonPath;
|
||||
}
|
||||
|
||||
Log.info('getPythonPath: 开始检测 Python 路径');
|
||||
|
||||
if (!isWin) {
|
||||
const standalonePython = getMacOSPythonPath();
|
||||
if (standalonePython) {
|
||||
Log.info('getPythonPath: ✅ 使用独立 Python', { path: standalonePython });
|
||||
cachedPythonPath = standalonePython;
|
||||
return standalonePython;
|
||||
}
|
||||
throw new Error('macOS Python 环境尚未初始化,请等待应用完成初始化');
|
||||
}
|
||||
|
||||
await waitAppEnvReady();
|
||||
const bundledPythonPath = getPythonExecutablePath();
|
||||
if (resourceExists(bundledPythonPath)) {
|
||||
try {
|
||||
Log.info('getPythonPath: trying bundled Python', { path: bundledPythonPath });
|
||||
const isAvailable = await verifyPythonExecutable(bundledPythonPath);
|
||||
if (isAvailable) {
|
||||
Log.info('getPythonPath: using bundled Python', { path: bundledPythonPath });
|
||||
cachedPythonPath = bundledPythonPath;
|
||||
return bundledPythonPath;
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn('getPythonPath: bundled Python unavailable', {
|
||||
path: bundledPythonPath,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const pythonCommands = [];
|
||||
pythonCommands.push('py', 'python', 'python3');
|
||||
|
||||
// 逐个测试 Python 命令是否可用
|
||||
for (const command of pythonCommands) {
|
||||
try {
|
||||
Log.info('getPythonPath: 尝试命令', { command });
|
||||
|
||||
const isAvailable = await verifyPythonExecutable(command);
|
||||
if (isAvailable) {
|
||||
Log.info('getPythonPath: ✅ 找到可用的 Python', { command });
|
||||
cachedPythonPath = command;
|
||||
return command;
|
||||
}
|
||||
} catch (error) {
|
||||
Log.warn('getPythonPath: 命令不可用', { command, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// 所有尝试都失败
|
||||
const errorMsg = `❌ 无法找到可用的 Python:\n` +
|
||||
`尝试的命令: ${pythonCommands.join(', ')}\n` +
|
||||
`请安装 Python 或确保其在系统 PATH 中。`;
|
||||
Log.error('getPythonPath', errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证 Python 命令是否可执行
|
||||
* @param pythonCommand Python 命令(python, py, python3 等)
|
||||
* @returns Promise<boolean> 是否可执行
|
||||
*/
|
||||
const verifyPythonExecutable = async (pythonCommand: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
Log.info('verifyPythonExecutable: 开始验证', { pythonCommand });
|
||||
|
||||
const child = spawn(pythonCommand, ['--version'], {
|
||||
shell: false,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let hasOutput = false;
|
||||
let isResolved = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
Log.warn('verifyPythonExecutable: 超时', { pythonCommand });
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}, 3000); // 3秒超时
|
||||
|
||||
child.stdout?.on('data', () => {
|
||||
hasOutput = true;
|
||||
Log.debug('verifyPythonExecutable: 获得输出', { pythonCommand });
|
||||
});
|
||||
|
||||
child.stderr?.on('data', () => {
|
||||
hasOutput = true;
|
||||
Log.debug('verifyPythonExecutable: 获得错误输出', { pythonCommand });
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
const success = code === 0 && hasOutput;
|
||||
Log.info('verifyPythonExecutable: 完成', { pythonCommand, code, hasOutput, success });
|
||||
resolve(success);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
Log.warn('verifyPythonExecutable: 错误', { pythonCommand, error: error.message });
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
Log.error('verifyPythonExecutable: 异常', { pythonCommand, error: error.message });
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// FFmpeg路径缓存,避免重复验证
|
||||
let cachedFFmpegPath: string | null = null;
|
||||
const invalidFFmpegPaths = new Set<string>();
|
||||
const invalidFFprobePaths = new Set<string>();
|
||||
const ILLEGAL_INSTRUCTION_EXIT_CODES = new Set([3221225501]);
|
||||
const FFMPEG_RUNTIME_VERIFY_ARGS = [
|
||||
'-hide_banner',
|
||||
'-loglevel', 'error',
|
||||
'-f', 'lavfi',
|
||||
'-i', 'color=size=16x16:rate=1:color=black',
|
||||
'-frames:v', '1',
|
||||
'-c:v', 'libx264',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-f', 'null',
|
||||
'-'
|
||||
];
|
||||
|
||||
const getBundledExecutableCandidates = (primaryPath: string): string[] => {
|
||||
const binaryName = path.basename(primaryPath);
|
||||
const candidates = [
|
||||
primaryPath,
|
||||
AppEnv.resourceBundleRoot ? path.join(AppEnv.resourceBundleRoot, 'ffmpeg', binaryName) : null,
|
||||
process.resourcesPath ? path.join(process.resourcesPath, 'resources-bundles', 'ffmpeg', binaryName) : null,
|
||||
process.resourcesPath ? path.join(path.dirname(process.resourcesPath), 'resources-bundles', 'ffmpeg', binaryName) : null,
|
||||
];
|
||||
|
||||
if (isWin) {
|
||||
const parentDir = path.dirname(primaryPath);
|
||||
const backupDir = `${parentDir}-backup`;
|
||||
const backupPath = path.join(backupDir, path.basename(primaryPath));
|
||||
candidates.push(backupPath);
|
||||
}
|
||||
|
||||
return Array.from(new Set(candidates.filter(Boolean)));
|
||||
};
|
||||
|
||||
const markExecutableAsInvalid = (
|
||||
invalidSet: Set<string>,
|
||||
executablePath: string,
|
||||
tag: 'FFmpeg' | 'FFprobe',
|
||||
reason: string
|
||||
) => {
|
||||
if (!executablePath || invalidSet.has(executablePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
invalidSet.add(executablePath);
|
||||
Log.warn(`${tag}: invalid executable path`, {
|
||||
executablePath,
|
||||
reason,
|
||||
});
|
||||
};
|
||||
|
||||
const verifyRuntimeExecutable = async (
|
||||
executablePath: string,
|
||||
args: string[],
|
||||
tag: 'FFmpeg' | 'FFprobe',
|
||||
options: {
|
||||
requireOutput?: boolean;
|
||||
} = {}
|
||||
): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const child = spawn(executablePath, args, {
|
||||
shell: false,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let hasOutput = false;
|
||||
let settled = false;
|
||||
const requireOutput = options.requireOutput ?? true;
|
||||
|
||||
const finish = (result: boolean) => {
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
try {
|
||||
child.kill('SIGTERM');
|
||||
} catch (e) {}
|
||||
Log.warn(`verify${tag}Executable: timeout`, { executablePath, args });
|
||||
finish(false);
|
||||
}, 4000);
|
||||
|
||||
child.stdout?.on('data', () => {
|
||||
hasOutput = true;
|
||||
});
|
||||
|
||||
child.stderr?.on('data', () => {
|
||||
hasOutput = true;
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
const success = code === 0 && (!requireOutput || hasOutput);
|
||||
Log.info(`verify${tag}Executable: complete`, {
|
||||
executablePath,
|
||||
args,
|
||||
code,
|
||||
hasOutput,
|
||||
requireOutput,
|
||||
success,
|
||||
});
|
||||
finish(success);
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timer);
|
||||
Log.warn(`verify${tag}Executable: error`, { executablePath, args, error: error.message });
|
||||
finish(false);
|
||||
});
|
||||
} catch (error: any) {
|
||||
Log.error(`verify${tag}Executable: exception`, { executablePath, args, error: error.message });
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const verifyBundledFFmpegCandidate = async (ffmpegPath: string): Promise<boolean> => {
|
||||
return verifyRuntimeExecutable(ffmpegPath, FFMPEG_RUNTIME_VERIFY_ARGS, 'FFmpeg', {
|
||||
requireOutput: false,
|
||||
});
|
||||
};
|
||||
|
||||
const verifyBundledFFprobeCandidate = async (ffprobePath: string): Promise<boolean> => {
|
||||
return verifyRuntimeExecutable(ffprobePath, ['-version'], 'FFprobe');
|
||||
};
|
||||
|
||||
const findFallbackFFmpegPath = async (excludePaths: string[]): Promise<string | null> => {
|
||||
const excluded = new Set(excludePaths);
|
||||
const bundledCandidates = getBundledExecutableCandidates(getFFmpegExecutablePath());
|
||||
|
||||
for (const candidate of bundledCandidates) {
|
||||
if (!candidate || excluded.has(candidate) || invalidFFmpegPaths.has(candidate) || !resourceExists(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await verifyBundledFFmpegCandidate(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
markExecutableAsInvalid(invalidFFmpegPaths, candidate, 'FFmpeg', 'fallback verification failed');
|
||||
}
|
||||
|
||||
if (isDevelopment()) {
|
||||
const systemFFmpegPath = isWin ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
if (!excluded.has(systemFFmpegPath) && !invalidFFmpegPaths.has(systemFFmpegPath)) {
|
||||
if (await verifyFFmpegExecutable(systemFFmpegPath)) {
|
||||
return systemFFmpegPath;
|
||||
}
|
||||
markExecutableAsInvalid(invalidFFmpegPaths, systemFFmpegPath, 'FFmpeg', 'dev system fallback verification failed');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
// FFprobe路径缓存,避免重复验证
|
||||
let cachedFFprobePath: string | null = null;
|
||||
|
||||
/**
|
||||
* Execute FFmpeg command with the given parameters
|
||||
* @param params Array of FFmpeg command parameters
|
||||
* @param options Optional execution options
|
||||
* @returns Promise that resolves when the command completes
|
||||
*/
|
||||
export const execFFmpegCommand = async (
|
||||
params: string[],
|
||||
options?: {
|
||||
cwd?: string;
|
||||
outputEncoding?: string;
|
||||
onProgress?: (progress: any) => void;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
): Promise<string> => {
|
||||
console.log('[execFFmpegCommand] ========== START ==========');
|
||||
console.log('[execFFmpegCommand] Called with params count:', params.length);
|
||||
|
||||
const ffmpegPath = await getFFmpegPath();
|
||||
|
||||
let cwd: string;
|
||||
if (options?.cwd && typeof options.cwd === 'string') {
|
||||
cwd = options.cwd;
|
||||
} else {
|
||||
await waitAppEnvReady();
|
||||
cwd = AppEnv.appRoot || process.cwd();
|
||||
}
|
||||
|
||||
const outputEncoding = options?.outputEncoding || 'utf8';
|
||||
|
||||
if (!Array.isArray(params)) {
|
||||
const error = new Error('params must be an array');
|
||||
Log.error('execFFmpegCommand: Invalid params type', { params, type: typeof params });
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
if (typeof params[i] !== 'string') {
|
||||
const error = new Error(`params[${i}] must be a string, got ${typeof params[i]}: ${params[i]}`);
|
||||
Log.error('execFFmpegCommand: Invalid param type', { index: i, value: params[i], type: typeof params[i] });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const pathParamFlags = ['-i'];
|
||||
const normalizedParams = params.map((param, index) => {
|
||||
if (index > 0 && pathParamFlags.includes(params[index - 1])) {
|
||||
const normalized = normalizePath(param);
|
||||
if (normalized !== param) {
|
||||
Log.info('execFFmpegCommand: normalized input path', {
|
||||
original: param.substring(0, 100),
|
||||
normalized: normalized.substring(0, 100)
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (index === params.length - 1) {
|
||||
const looksLikeOutputPath = param.includes('\\') || param.includes('/') || (
|
||||
param.includes('.') && param.length > 3 && !param.startsWith('-')
|
||||
);
|
||||
if (looksLikeOutputPath) {
|
||||
const outputDir = path.dirname(param);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
const normalized = normalizeOutputPath(param);
|
||||
if (normalized !== param) {
|
||||
Log.info('execFFmpegCommand: normalized output path', {
|
||||
original: param.substring(0, 100),
|
||||
normalized: normalized.substring(0, 100)
|
||||
});
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return param;
|
||||
});
|
||||
|
||||
if (typeof cwd !== 'string') {
|
||||
const error = new Error(`cwd must be a string, got ${typeof cwd}`);
|
||||
Log.error('execFFmpegCommand: Invalid cwd type', { cwd, type: typeof cwd });
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (typeof ffmpegPath !== 'string') {
|
||||
const error = new Error(`ffmpegPath must be a string, got ${typeof ffmpegPath}`);
|
||||
Log.error('execFFmpegCommand: Invalid ffmpegPath type', { ffmpegPath, type: typeof ffmpegPath });
|
||||
throw error;
|
||||
}
|
||||
|
||||
Log.info('execFFmpegCommand', { ffmpegPath, params: normalizedParams, cwd, outputEncoding });
|
||||
|
||||
const env = buildRuntimeProcessEnv(options?.env);
|
||||
|
||||
const runProcess = (executablePath: string) => {
|
||||
return new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve, reject) => {
|
||||
try {
|
||||
const child = spawn(executablePath, normalizedParams, {
|
||||
cwd,
|
||||
shell: false,
|
||||
env,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
const output = data.toString(outputEncoding as BufferEncoding);
|
||||
stdout += output;
|
||||
|
||||
if (options?.onProgress) {
|
||||
const timeMatch = output.match(/time=(\d{2}):(\d{2}):(\d{2}\.\d{2})/);
|
||||
if (timeMatch) {
|
||||
const [, hours, minutes, seconds] = timeMatch;
|
||||
const totalSeconds = parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseFloat(seconds);
|
||||
options.onProgress({ currentTime: totalSeconds });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
const output = data.toString(outputEncoding as BufferEncoding);
|
||||
stderr += output;
|
||||
console.log('[FFmpeg stderr]', output.substring(0, 500));
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
Log.info('FFmpeg process completed', {
|
||||
executablePath,
|
||||
code,
|
||||
stderrLength: stderr.length
|
||||
});
|
||||
console.log('[FFmpeg] exited with code:', code);
|
||||
if (stderr.length > 0) {
|
||||
console.log('[FFmpeg] stderr:\n', stderr.substring(0, 2000));
|
||||
}
|
||||
resolve({ stdout, stderr, code });
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
Log.error('FFmpeg process error', { executablePath, error });
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
Log.error('execFFmpegCommand: spawn error', { executablePath, error });
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let activeFFmpegPath = ffmpegPath;
|
||||
let result = await runProcess(activeFFmpegPath);
|
||||
|
||||
if (result.code !== 0 && result.code !== null && ILLEGAL_INSTRUCTION_EXIT_CODES.has(result.code)) {
|
||||
markExecutableAsInvalid(invalidFFmpegPaths, activeFFmpegPath, 'FFmpeg', `runtime exit code ${result.code}`);
|
||||
if (cachedFFmpegPath === activeFFmpegPath) {
|
||||
cachedFFmpegPath = null;
|
||||
}
|
||||
|
||||
const fallbackPath = await findFallbackFFmpegPath([activeFFmpegPath]);
|
||||
if (fallbackPath) {
|
||||
Log.warn('execFFmpegCommand: retrying with fallback bundled FFmpeg', {
|
||||
failedPath: activeFFmpegPath,
|
||||
fallbackPath,
|
||||
code: result.code
|
||||
});
|
||||
cachedFFmpegPath = fallbackPath;
|
||||
activeFFmpegPath = fallbackPath;
|
||||
result = await runProcess(activeFFmpegPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.code === 0) {
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
Log.error(`FFmpeg process exited with code ${result.code}`, result.stderr);
|
||||
throw new Error(`FFmpeg process exited with code ${result.code}: ${result.stderr}`);
|
||||
};
|
||||
/**
|
||||
* Extract audio from video file using FFmpeg
|
||||
* @param inputPath Path to the input video file
|
||||
* @param outputPath Path to save the extracted audio
|
||||
* @param options Audio extraction options
|
||||
* @returns Promise that resolves when audio extraction completes
|
||||
*/
|
||||
export const extractAudioFromVideo = async (
|
||||
inputPath: string,
|
||||
outputPath: string,
|
||||
options?: {
|
||||
audioCodec?: string;
|
||||
sampleRate?: number;
|
||||
channels?: number;
|
||||
bitrate?: string;
|
||||
onProgress?: (progress: any) => void;
|
||||
}
|
||||
): Promise<string> => {
|
||||
const {
|
||||
audioCodec = 'mp3',
|
||||
sampleRate = 16000,
|
||||
channels = 1,
|
||||
bitrate = '128k',
|
||||
onProgress
|
||||
} = options || {};
|
||||
|
||||
// 🔧 路径已经在 execFFmpegCommand 中自动转换,这里直接传递即可
|
||||
const params = [
|
||||
'-i', inputPath,
|
||||
'-vn', // No video
|
||||
'-acodec', audioCodec,
|
||||
'-ar', sampleRate.toString(),
|
||||
'-ac', channels.toString(),
|
||||
'-ab', bitrate,
|
||||
'-y', // Overwrite output
|
||||
outputPath
|
||||
];
|
||||
|
||||
return execFFmpegCommand(params, { onProgress });
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证FFmpeg是否可执行
|
||||
* @param ffmpegPath FFmpeg路径
|
||||
* @returns Promise<boolean> 是否可执行
|
||||
*/
|
||||
const verifyFFmpegExecutable = async (ffmpegPath: string): Promise<boolean> => {
|
||||
return verifyRuntimeExecutable(ffmpegPath, ['-version'], 'FFmpeg');
|
||||
};
|
||||
/**
|
||||
* Get the path to the FFmpeg executable
|
||||
* ⚠️ 优先使用程序自带的 FFmpeg,如果无法执行则回退到系统 FFmpeg
|
||||
* @returns Promise that resolves to the FFmpeg path
|
||||
*/
|
||||
export const getFFmpegPath = async (): Promise<string> => {
|
||||
if (cachedFFmpegPath && !invalidFFmpegPaths.has(cachedFFmpegPath)) {
|
||||
Log.debug('getFFmpegPath: using cached bundled path', { path: cachedFFmpegPath });
|
||||
return cachedFFmpegPath;
|
||||
}
|
||||
|
||||
await waitAppEnvReady();
|
||||
cachedFFmpegPath = null;
|
||||
|
||||
const bundledCandidates = getBundledExecutableCandidates(getFFmpegExecutablePath());
|
||||
Log.info('getFFmpegPath: checking bundled candidates', { bundledCandidates });
|
||||
|
||||
for (const candidate of bundledCandidates) {
|
||||
if (!candidate || invalidFFmpegPaths.has(candidate) || !resourceExists(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.info('getFFmpegPath: verifying bundled candidate', { candidate });
|
||||
if (await verifyBundledFFmpegCandidate(candidate)) {
|
||||
cachedFFmpegPath = candidate;
|
||||
Log.info('getFFmpegPath: using bundled candidate', { candidate });
|
||||
return candidate;
|
||||
}
|
||||
|
||||
markExecutableAsInvalid(invalidFFmpegPaths, candidate, 'FFmpeg', 'startup verification failed');
|
||||
}
|
||||
|
||||
if (isDevelopment()) {
|
||||
const systemFFmpegPath = isWin ? 'ffmpeg.exe' : 'ffmpeg';
|
||||
Log.warn('getFFmpegPath: bundled FFmpeg unavailable in dev, trying system FFmpeg', {
|
||||
systemFFmpegPath,
|
||||
});
|
||||
if (!invalidFFmpegPaths.has(systemFFmpegPath) && await verifyFFmpegExecutable(systemFFmpegPath)) {
|
||||
cachedFFmpegPath = systemFFmpegPath;
|
||||
Log.info('getFFmpegPath: using system FFmpeg in dev mode', { systemFFmpegPath });
|
||||
return systemFFmpegPath;
|
||||
}
|
||||
markExecutableAsInvalid(invalidFFmpegPaths, systemFFmpegPath, 'FFmpeg', 'dev system fallback verification failed');
|
||||
}
|
||||
|
||||
const errorMsg = `无法找到可用的 FFmpeg: ${bundledCandidates.join(', ')}`;
|
||||
Log.error('getFFmpegPath', errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
};
|
||||
/**
|
||||
* Get FFprobe path with fallback support
|
||||
* 1. Try bundled ffprobe
|
||||
* 2. Fallback to system ffprobe
|
||||
*/
|
||||
export const getFFprobePath = async (): Promise<string> => {
|
||||
if (cachedFFprobePath && !invalidFFprobePaths.has(cachedFFprobePath)) {
|
||||
Log.debug('getFFprobePath: using cached bundled path', { path: cachedFFprobePath });
|
||||
return cachedFFprobePath;
|
||||
}
|
||||
|
||||
await waitAppEnvReady();
|
||||
cachedFFprobePath = null;
|
||||
|
||||
const bundledCandidates = getBundledExecutableCandidates(getFFprobeExecutablePath());
|
||||
Log.info('getFFprobePath: checking bundled candidates', { bundledCandidates });
|
||||
|
||||
for (const candidate of bundledCandidates) {
|
||||
if (!candidate || invalidFFprobePaths.has(candidate) || !resourceExists(candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.info('getFFprobePath: verifying bundled candidate', { candidate });
|
||||
if (await verifyBundledFFprobeCandidate(candidate)) {
|
||||
cachedFFprobePath = candidate;
|
||||
Log.info('getFFprobePath: using bundled candidate', { candidate });
|
||||
return candidate;
|
||||
}
|
||||
|
||||
markExecutableAsInvalid(invalidFFprobePaths, candidate, 'FFprobe', 'startup verification failed');
|
||||
}
|
||||
|
||||
if (isDevelopment()) {
|
||||
const systemFFprobePath = isWin ? 'ffprobe.exe' : 'ffprobe';
|
||||
Log.warn('getFFprobePath: bundled FFprobe unavailable in dev, trying system FFprobe', {
|
||||
systemFFprobePath,
|
||||
});
|
||||
if (!invalidFFprobePaths.has(systemFFprobePath) && await verifyRuntimeExecutable(systemFFprobePath, ['-version'], 'FFprobe')) {
|
||||
cachedFFprobePath = systemFFprobePath;
|
||||
Log.info('getFFprobePath: using system FFprobe in dev mode', { systemFFprobePath });
|
||||
return systemFFprobePath;
|
||||
}
|
||||
markExecutableAsInvalid(invalidFFprobePaths, systemFFprobePath, 'FFprobe', 'dev system fallback verification failed');
|
||||
}
|
||||
|
||||
const errorMsg = `无法找到可用的 FFprobe: ${bundledCandidates.join(', ')}`;
|
||||
Log.error('getFFprobePath', errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
};
|
||||
/**
|
||||
* Show item in folder using the system's file manager
|
||||
* @param filePath Path to the file to show
|
||||
* @returns boolean indicating success
|
||||
*/
|
||||
export const showItemInFolder = async (filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
Log.info('showItemInFolder', { filePath });
|
||||
shell.showItemInFolder(filePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
Log.error('showItemInFolder error', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
execFFmpegCommand,
|
||||
extractAudioFromVideo,
|
||||
getFFmpegPath,
|
||||
getPythonPath,
|
||||
showItemInFolder,
|
||||
};
|
||||
@@ -0,0 +1,361 @@
|
||||
import { ipcMain } from "electron";
|
||||
import shellApi, { buildRuntimeProcessEnv } from "./index";
|
||||
import { Log } from "../log";
|
||||
import { normalizePath } from "../../lib/path-util";
|
||||
import { ProcessCleanupManager } from "../../lib/process-cleanup";
|
||||
|
||||
console.log("[shell/main.ts] Module loaded, registering IPC handlers...");
|
||||
|
||||
// 注册 showItemInFolder handler
|
||||
ipcMain.handle("shell:showItemInFolder", async (event, filePath: string) => {
|
||||
return shellApi.showItemInFolder(filePath);
|
||||
});
|
||||
|
||||
// 注册 exec handler (用于执行系统命令如 python、nvidia-smi、taskkill 等)
|
||||
ipcMain.handle("shell:exec", async (event, command: string) => {
|
||||
console.log("[shell:exec] Handler called, command:", command.substring(0, 100) + (command.length > 100 ? '...' : ''));
|
||||
Log.info("shell:exec.start", { command: command.substring(0, 200) + '...' });
|
||||
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const { AppEnv, waitAppEnvReady } = await import('../env');
|
||||
|
||||
// 等待AppEnv初始化完成
|
||||
await waitAppEnvReady();
|
||||
|
||||
// 获取工作目录
|
||||
const cwd = AppEnv.appRoot || process.cwd();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('[shell.exec] 执行命令:', command);
|
||||
|
||||
const child = spawn(command, [], {
|
||||
cwd,
|
||||
shell: true,
|
||||
env: buildRuntimeProcessEnv(),
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
// 注册子进程到清理管理器
|
||||
if (child.pid) {
|
||||
ProcessCleanupManager.registerChildProcess(child.pid);
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
// 进程结束时注销
|
||||
if (child.pid) {
|
||||
ProcessCleanupManager.unregisterChildProcess(child.pid);
|
||||
}
|
||||
console.log('[shell.exec] 命令完成,退出码:', code);
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
console.warn('[shell.exec] 命令执行失败,退出码:', code, 'stderr:', stderr);
|
||||
resolve(stdout); // 返回stdout而不是抛出错误,保持与旧版本兼容
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
// 进程错误时注销
|
||||
if (child.pid) {
|
||||
ProcessCleanupManager.unregisterChildProcess(child.pid);
|
||||
}
|
||||
console.error('[shell.exec] 进程错误:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
Log.error("shell:exec.error", {
|
||||
error: error?.message || String(error),
|
||||
command: command.substring(0, 200) + '...'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 注册 getPythonPath handler
|
||||
ipcMain.handle("shell:getPythonPath", async (event) => {
|
||||
console.log("[shell:getPythonPath] Handler called");
|
||||
Log.info("shell:getPythonPath.start");
|
||||
|
||||
try {
|
||||
return await shellApi.getPythonPath();
|
||||
} catch (error: any) {
|
||||
Log.error("shell:getPythonPath.error", {
|
||||
error: error?.message || String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 注册 executeFFmpeg handler
|
||||
ipcMain.handle("shell:executeFFmpeg", async (event, commandOrJson: string) => {
|
||||
console.log("[shell:executeFFmpeg] Handler called, command length:", commandOrJson.length);
|
||||
Log.info("shell:executeFFmpeg.start", { commandLength: commandOrJson.length });
|
||||
|
||||
try {
|
||||
// 检查是否是JSON数组格式(args数组模式,避免路径拼接问题)
|
||||
if (commandOrJson.startsWith('[')) {
|
||||
const argsArray = JSON.parse(commandOrJson);
|
||||
if (Array.isArray(argsArray)) {
|
||||
Log.info("shell:executeFFmpeg.arrayMode", {
|
||||
argsCount: argsArray.length
|
||||
});
|
||||
return await shellApi.execFFmpegCommand(argsArray);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是JSON格式(长过滤器模式)
|
||||
if (commandOrJson.startsWith('{')) {
|
||||
const data = JSON.parse(commandOrJson);
|
||||
|
||||
if (data.type === 'ffmpeg_long_filter') {
|
||||
Log.info("shell:executeFFmpeg.longFilter", {
|
||||
inputVideo: data.inputVideo,
|
||||
outputVideo: data.outputVideo,
|
||||
filterLength: data.filterComplex.length,
|
||||
hasAudioMix: !!data.audioMixConfig // 🆕 记录是否有音效混音
|
||||
});
|
||||
|
||||
// 🔧 转换输入输出路径
|
||||
const { AppEnv, waitAppEnvReady } = await import('../env');
|
||||
await waitAppEnvReady();
|
||||
const ffmpegCwd = AppEnv.installRoot || AppEnv.appRoot || process.cwd();
|
||||
|
||||
const normalizedInputVideo = normalizePath(data.inputVideo);
|
||||
const normalizedOutputVideo = normalizePath(data.outputVideo);
|
||||
|
||||
Log.info("shell:executeFFmpeg.pathNormalized", {
|
||||
originalInput: data.inputVideo.substring(0, 100),
|
||||
normalizedInput: normalizedInputVideo.substring(0, 100),
|
||||
originalOutput: data.outputVideo.substring(0, 100),
|
||||
normalizedOutput: normalizedOutputVideo.substring(0, 100)
|
||||
});
|
||||
|
||||
// 【修复】如果filter_complex过长,写入文件使用新语法 -/filter_complex
|
||||
// 避免Windows命令行长度限制(某些情况下单个参数>32KB会失败)
|
||||
// 注意:FFmpeg N-122209 已弃用 -filter_complex_script,改用 -/filter_complex
|
||||
const { promises: fs } = await import('fs');
|
||||
const path = await import('path');
|
||||
const os = await import('os');
|
||||
|
||||
// 🆕 处理音效混音配置
|
||||
const audioMixConfig = data.audioMixConfig;
|
||||
let finalFilterContent = data.filterComplex;
|
||||
let audioInputArgs: string[] = [];
|
||||
let useAudioMix = false;
|
||||
|
||||
if (audioMixConfig && audioMixConfig.inputFiles && audioMixConfig.inputFiles.length > 0) {
|
||||
console.log(`[shell:executeFFmpeg] 🔊 检测到音效混音配置:`, {
|
||||
inputFilesCount: audioMixConfig.inputFiles.length,
|
||||
mixFilter: audioMixConfig.mixFilter
|
||||
});
|
||||
|
||||
// 🔧 转换音效文件路径并添加为额外输入
|
||||
for (const soundFile of audioMixConfig.inputFiles) {
|
||||
const normalizedSoundFile = normalizePath(soundFile);
|
||||
audioInputArgs.push('-i', normalizedSoundFile);
|
||||
Log.info("shell:executeFFmpeg.soundFileNormalized", {
|
||||
original: soundFile.substring(0, 100),
|
||||
normalized: normalizedSoundFile.substring(0, 100)
|
||||
});
|
||||
}
|
||||
|
||||
// 🔧 关键修复:修改视频滤镜链的末尾,添加 [vout] 输出标签
|
||||
// 原始滤镜链末尾没有输出标签,例如:[outN]drawtext=...
|
||||
// 需要改为:[outN]drawtext=...[vout]
|
||||
// 然后添加音频混音滤镜
|
||||
|
||||
let modifiedVideoFilter = data.filterComplex;
|
||||
|
||||
// 检查最后一个滤镜是否已有输出标签
|
||||
// 如果没有,添加 [vout]
|
||||
if (!modifiedVideoFilter.endsWith(']')) {
|
||||
// 最后一个滤镜没有输出标签,添加 [vout]
|
||||
modifiedVideoFilter = modifiedVideoFilter + '[vout]';
|
||||
console.log(`[shell:executeFFmpeg] 🔊 添加视频输出标签 [vout]`);
|
||||
} else {
|
||||
// 最后一个滤镜已有输出标签(可能是 [out0]、[out1] 等)
|
||||
// 需要提取这个标签名称,然后添加一个 null 滤镜连接到 [vout]
|
||||
// 从滤镜链末尾提取最后一个输出标签,例如 [out5]
|
||||
const lastLabelMatch = modifiedVideoFilter.match(/\[([^\[\]]+)\]$/);
|
||||
if (lastLabelMatch) {
|
||||
const lastLabel = lastLabelMatch[1];
|
||||
modifiedVideoFilter = modifiedVideoFilter + `;[${lastLabel}]null[vout]`;
|
||||
console.log(`[shell:executeFFmpeg] 🔊 通过 null 滤镜连接 [${lastLabel}] -> [vout]`);
|
||||
} else {
|
||||
// 无法提取标签,尝试直接添加 [vout]
|
||||
modifiedVideoFilter = modifiedVideoFilter + '[vout]';
|
||||
console.log(`[shell:executeFFmpeg] 🔊 无法提取最后标签,直接添加 [vout]`);
|
||||
}
|
||||
}
|
||||
|
||||
// 将音频混音滤镜添加到末尾
|
||||
finalFilterContent = modifiedVideoFilter + ';' + audioMixConfig.mixFilter;
|
||||
useAudioMix = true;
|
||||
|
||||
console.log(`[shell:executeFFmpeg] 🔊 组合滤镜长度: ${finalFilterContent.length}`);
|
||||
}
|
||||
|
||||
const tempDir = os.tmpdir();
|
||||
const filterScriptPath = path.join(tempDir, `ffmpeg_filter_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.txt`);
|
||||
|
||||
console.log(`[shell:executeFFmpeg] 将filter_complex写入文件: ${filterScriptPath}`);
|
||||
await fs.writeFile(filterScriptPath, finalFilterContent, 'utf-8');
|
||||
|
||||
// 🆕 根据是否有音效混音,构建不同的参数
|
||||
let args: string[];
|
||||
if (useAudioMix) {
|
||||
// 有音效时:添加音效输入,映射视频和音频输出
|
||||
// 使用新语法 -/filter_complex 而不是已弃用的 -filter_complex_script
|
||||
args = [
|
||||
'-y',
|
||||
'-i', normalizedInputVideo, // 🔧 使用转换后的路径
|
||||
...(data.stickerInputs || []), // 🆕 添加贴纸输入
|
||||
...audioInputArgs, // 音效文件输入(已转换)
|
||||
'-/filter_complex', filterScriptPath,
|
||||
'-map', '[vout]',
|
||||
'-map', '[aout]',
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'medium',
|
||||
'-crf', '18',
|
||||
'-c:a', 'aac', // 需要重新编码音频
|
||||
'-b:a', '192k',
|
||||
normalizedOutputVideo // 🔧 使用转换后的路径
|
||||
];
|
||||
console.log(`[shell:executeFFmpeg] 🔊 使用音效混音模式执行 FFmpeg`);
|
||||
} else {
|
||||
// 无音效时:原始逻辑
|
||||
// 注意:新版 FFmpeg 使用 -/filter_complex 参数从文件读取
|
||||
args = [
|
||||
'-y',
|
||||
'-i', normalizedInputVideo, // 🔧 使用转换后的路径
|
||||
...(data.stickerInputs || []), // 🆕 添加贴纸输入
|
||||
'-/filter_complex', filterScriptPath,
|
||||
'-c:v', 'libx264',
|
||||
'-preset', 'medium',
|
||||
'-crf', '18',
|
||||
'-c:a', 'copy',
|
||||
normalizedOutputVideo // 🔧 使用转换后的路径
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await shellApi.execFFmpegCommand(args, { cwd: ffmpegCwd });
|
||||
try { await fs.unlink(filterScriptPath); } catch (e) {}
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
const exitCode = error?.message ? parseInt(error.message.match(/code\s+(\d+)/)?.[1] || '0') : 0;
|
||||
if (exitCode !== 0) {
|
||||
Log.warn("shell:executeFFmpeg.filterFileMode.failed", {
|
||||
exitCode,
|
||||
trying: "filter_complex_script fallback"
|
||||
});
|
||||
try {
|
||||
const fallbackArgs = args.map(a => a === '-/filter_complex' ? '-filter_complex_script' : a);
|
||||
const result2 = await shellApi.execFFmpegCommand(fallbackArgs, { cwd: ffmpegCwd });
|
||||
try { await fs.unlink(filterScriptPath); } catch (e) {}
|
||||
return result2;
|
||||
} catch (error2: any) {
|
||||
Log.warn("shell:executeFFmpeg.filter_complex_script.failed", {
|
||||
error: error2?.message?.substring(0, 200),
|
||||
trying: "inline filter_complex fallback"
|
||||
});
|
||||
try {
|
||||
const inlineArgs = args
|
||||
.filter(a => a !== '-/filter_complex' && a !== '-filter_complex_script' && a !== filterScriptPath)
|
||||
;
|
||||
const inlineIdx = inlineArgs.indexOf('-y') >= 0 ? 1 : 0;
|
||||
inlineArgs.splice(inlineIdx, 0, '-filter_complex', finalFilterContent);
|
||||
const result3 = await shellApi.execFFmpegCommand(inlineArgs, { cwd: ffmpegCwd });
|
||||
try { await fs.unlink(filterScriptPath); } catch (e) {}
|
||||
return result3;
|
||||
} catch (error3) {
|
||||
try { await fs.unlink(filterScriptPath); } catch (e) {}
|
||||
throw error3;
|
||||
}
|
||||
}
|
||||
}
|
||||
try { await fs.unlink(filterScriptPath); } catch (e) {}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 普通命令模式:解析命令字符串为参数数组
|
||||
const args = parseFFmpegCommand(commandOrJson);
|
||||
|
||||
Log.info("shell:executeFFmpeg.normalCommand", {
|
||||
argsCount: args.length,
|
||||
command: commandOrJson.substring(0, 200) + '...'
|
||||
});
|
||||
|
||||
return await shellApi.execFFmpegCommand(args);
|
||||
|
||||
} catch (error: any) {
|
||||
Log.error("shell:executeFFmpeg.error", {
|
||||
error: error?.message || String(error),
|
||||
command: commandOrJson.substring(0, 200) + '...'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[shell/main.ts] IPC handlers registered!");
|
||||
|
||||
/**
|
||||
* 解析FFmpeg命令字符串为参数数组
|
||||
*/
|
||||
function parseFFmpegCommand(command: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = '';
|
||||
let inQuote = false;
|
||||
let quoteChar = '';
|
||||
|
||||
const cmdStr = command.startsWith('ffmpeg ') ? command.substring(7) : command;
|
||||
|
||||
for (let i = 0; i < cmdStr.length; i++) {
|
||||
const char = cmdStr[i];
|
||||
|
||||
if ((char === '"' || char === "'") && !inQuote) {
|
||||
inQuote = true;
|
||||
quoteChar = char;
|
||||
} else if (char === quoteChar && inQuote) {
|
||||
inQuote = false;
|
||||
quoteChar = '';
|
||||
} else if (char === ' ' && !inQuote) {
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
current = '';
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export const shell = {
|
||||
execFFmpegCommand: shellApi.execFFmpegCommand,
|
||||
extractAudioFromVideo: shellApi.extractAudioFromVideo,
|
||||
getPythonPath: shellApi.getPythonPath,
|
||||
showItemInFolder: shellApi.showItemInFolder,
|
||||
};
|
||||
|
||||
export default shell;
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
const showItemInFolder = async (filePath: string) => {
|
||||
return ipcRenderer.invoke("shell:showItemInFolder", filePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行FFmpeg命令
|
||||
* @param command FFmpeg命令字符串 或 JSON格式的长过滤器配置
|
||||
* @returns Promise<string> FFmpeg输出
|
||||
*/
|
||||
const executeFFmpeg = async (command: string): Promise<string> => {
|
||||
console.log('[shell.executeFFmpeg] 调用IPC执行FFmpeg:', {
|
||||
commandLength: command.length,
|
||||
isJson: command.startsWith('{')
|
||||
});
|
||||
return ipcRenderer.invoke("shell:executeFFmpeg", command);
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行系统命令(如 python、nvidia-smi、taskkill 等)
|
||||
* @param command 要执行的命令字符串
|
||||
* @returns Promise<string> 命令输出
|
||||
*/
|
||||
const exec = async (command: string): Promise<string> => {
|
||||
console.log('[shell.exec] 调用IPC执行系统命令:', {
|
||||
command: command.substring(0, 100) + (command.length > 100 ? '...' : '')
|
||||
});
|
||||
return ipcRenderer.invoke("shell:exec", command);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 Python 可执行文件路径
|
||||
* @returns Promise<string> Python 可执行文件路径
|
||||
*/
|
||||
const getPythonPath = async (): Promise<string> => {
|
||||
console.log('[shell.getPythonPath] 调用IPC获取Python路径');
|
||||
return ipcRenderer.invoke("shell:getPythonPath");
|
||||
};
|
||||
|
||||
export const shell = {
|
||||
showItemInFolder,
|
||||
executeFFmpeg,
|
||||
exec,
|
||||
getPythonPath,
|
||||
};
|
||||
|
||||
export default shell;
|
||||
Reference in New Issue
Block a user