Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | import fs from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; import { FFmpegProcessor } from '../../services/ffmpeg.processor'; const execAsync = promisify(exec); export class AudioMerger { // 合并多个音频文件(支持 OSS URL) static async merge(inputFiles: string[], outputPath: string): Promise<string> { if (inputFiles.length === 0) { throw new Error('没有音频文件需要合并'); } if (inputFiles.length === 1) { // 单个文件:远程URL直接用FFmpegProcessor上传 if (inputFiles[0].startsWith('http')) { const outputExt = outputPath.split('.').pop() || 'mp3'; return await FFmpegProcessor.mergeAudio(inputFiles, outputExt); } // 本地单文件:检查是否需要格式转换 const inputExt = inputFiles[0].split('.').pop()?.toLowerCase(); const outputExt = outputPath.split('.').pop()?.toLowerCase(); if (inputExt !== outputExt) { // 格式不同(如 WAV→MP3),用 FFmpeg 转码 let cmd: string; if (outputExt === 'mp3') { cmd = `ffmpeg -i "${inputFiles[0]}" -c:a libmp3lame -b:a 192k -y "${outputPath}"`; } else { cmd = `ffmpeg -i "${inputFiles[0]}" -c copy -y "${outputPath}"`; } await execAsync(cmd, { timeout: 120000 }); console.log(`✅ 音频格式转换成功: ${inputExt} → ${outputExt}`); } else { // 格式相同,直接复制 if (inputFiles[0] !== outputPath) { fs.copyFileSync(inputFiles[0], outputPath); } } return outputPath; } // 检测是否有远程 URL const hasRemoteFiles = inputFiles.some(f => f.startsWith('http')); if (hasRemoteFiles) { // 使用 FFmpegProcessor 处理远程文件 const outputExt = outputPath.split('.').pop() || 'mp3'; return await FFmpegProcessor.mergeAudio(inputFiles, outputExt); } // 本地文件,使用原有逻辑 return this.mergeLocalFiles(inputFiles, outputPath); } // 合并本地音频文件(原有逻辑) private static async mergeLocalFiles(inputFiles: string[], outputPath: string): Promise<string> { try { // 在输出文件同目录创建临时列表文件(避免 Windows /tmp 路径问题) const outputDir = path.dirname(outputPath); const listFile = path.join(outputDir, 'ffmpeg_concat_list.txt'); // 使用相对路径避免 Windows 反斜杠问题 const listContent = inputFiles.map(f => { const relativePath = path.relative(outputDir, f).replace(/\\/g, '/'); return `file '${relativePath}'`; }).join('\n'); fs.writeFileSync(listFile, listContent); // 检查输出格式 const outputExt = outputPath.split('.').pop()?.toLowerCase(); // 构建 FFmpeg 命令(在工作目录中执行,使相对路径生效) let cmd: string; if (outputExt === 'mp3') { // MP3 需要转码(WAV/PCM -> MP3) cmd = `ffmpeg -f concat -safe 0 -i "${listFile}" -c:a libmp3lame -b:a 192k -y "${outputPath}"`; } else { // 其他格式直接复制 cmd = `ffmpeg -f concat -safe 0 -i "${listFile}" -c copy -y "${outputPath}"`; } await execAsync(cmd, { timeout: 300000, cwd: outputDir }); // 清理临时列表文件 try { fs.unlinkSync(listFile); } catch {} console.log(`✅ 音频合并成功: ${outputPath}`); return outputPath; } catch (error) { console.error('❌ 音频合并失败:', error); throw new Error('音频合并失败'); } } // 获取音频时长(支持 OSS URL) static async getDuration(filePath: string): Promise<number> { // 检测是否为远程 URL if (filePath.startsWith('http')) { return await FFmpegProcessor.getDuration(filePath); } // 本地文件,使用原有逻辑 try { const cmd = `ffprobe -i "${filePath}" -show_entries format=duration -v quiet -of csv="p=0"`; const { stdout } = await execAsync(cmd, { timeout: 10000 }); // 保留一位小数精度(不再 Math.round 丢失精度) return Math.round((parseFloat(stdout.trim()) || 0) * 10) / 10; } catch (error) { console.error('❌ 获取音频时长失败:', error); return 0; } } } |