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;