NestJS中用FFmpeg处理前端上传音频:解决[object Object]错误
解决NestJS中FFmpeg处理上传音频的文件路径问题
问题核心
你直接将Express.Multer.File对象拼入FFmpeg命令,对象被转为字符串[object Object],导致FFmpeg找不到对应文件,抛出No such file or directory错误。因为当前未将上传文件存储到磁盘/内存,FFmpeg无法直接读取上传的文件内容。
可行解决方案
根据文件大小选择合适的处理方式,以下两种方案均无需依赖Multer的磁盘/内存存储配置,直接基于上传的file.buffer处理:
方案1:内存流处理(适合小体积音频)
利用FFmpeg的标准输入输出,直接将上传文件的Buffer通过管道传给FFmpeg,无需写入磁盘,效率更高。推荐使用child_process.spawn而非exec,避免命令注入风险,同时更适合流处理:
import { spawn } from 'child_process'; import { promisify } from 'util'; import { pipeline } from 'stream'; const pipelineAsync = promisify(pipeline); type Section = { from: number, to: number }; export const setupAudioFileSection = async (file: Express.Multer.File, section: Section) => { // 封装音频切割函数 const cutAudio = async (start: number, duration?: number): Promise<Buffer> => { return new Promise((resolve, reject) => { // 构建FFmpeg参数数组,避免字符串拼接注入风险 const args = ['-i', 'pipe:0', '-y', '-ss', start.toString()]; if (duration) args.push('-t', duration.toString()); args.push('-f', 'mp3', 'pipe:1'); // 输出到标准输出 const ffmpeg = spawn('ffmpeg', args); const chunks: Buffer[] = []; ffmpeg.stdout.on('data', (chunk) => chunks.push(chunk)); ffmpeg.stderr.on('data', (data) => console.error(`FFmpeg日志: ${data}`)); ffmpeg.on('close', (code) => { code === 0 ? resolve(Buffer.concat(chunks)) : reject(new Error(`FFmpeg执行失败,码值: ${code}`)); }); ffmpeg.on('error', reject); // 将上传文件的Buffer写入FFmpeg标准输入 ffmpeg.stdin.write(file.buffer); ffmpeg.stdin.end(); }); }; try { // 并行处理三段音频切割 const [file1Buf, file2Buf, file3Buf] = await Promise.all([ cutAudio(0, section.from), cutAudio(section.from, section.to - section.from), // -t参数是持续时间,不是结束时间 cutAudio(section.to), ]); // 此处可将Buffer上传至S3或进行其他处理 // 示例:await s3Client.upload({ Bucket: '你的桶名', Key: `片段2-${Date.now()}.mp3`, Body: file2Buf }).promise(); return file2Buf; } catch (error) { console.error('音频切割失败:', error); throw new Error('setupAudioFileSection error'); } };
方案2:临时磁盘文件处理(适合大体积音频)
将上传文件写入服务器临时目录,处理完成后自动删除,避免内存溢出:
import { exec } from 'child_process'; import { promisify } from 'util'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; const execAsync = promisify(exec); type Section = { from: number, to: number }; export const setupAudioFileSection = async (file: Express.Multer.File, section: Section) => { // 创建临时文件路径 const tempDir = os.tmpdir(); const tempFilePath = path.join(tempDir, `${Date.now()}-${file.originalname}`); try { // 写入上传文件到临时磁盘 await fs.writeFile(tempFilePath, file.buffer); // 执行FFmpeg切割命令(注意-t是持续时间) const { stdout: file1Out } = await execAsync(`ffmpeg -i "${tempFilePath}" -y -ss 0 -t ${section.from} -f mp3 pipe:1`); const { stdout: file2Out } = await execAsync(`ffmpeg -i "${tempFilePath}" -y -ss ${section.from} -t ${section.to - section.from} -f mp3 pipe:1`); const { stdout: file3Out } = await execAsync(`ffmpeg -i "${tempFilePath}" -y -ss ${section.to} -f mp3 pipe:1`); // 将输出转为Buffer,可上传至S3 const file2Buf = Buffer.from(file2Out); return file2Buf; } catch (error) { console.error('音频切割失败:', error); throw new Error('setupAudioFileSection error'); } finally { // 无论成功失败,删除临时文件 try { await fs.unlink(tempFilePath); } catch (unlinkErr) { console.error('临时文件删除失败:', unlinkErr); } } };
关键注意事项
- FFmpeg参数修正:原代码中
-t参数误用为结束时间,FFmpeg的-t实际代表持续时间,需计算为section.to - section.from。 - 命令安全:使用
spawn的参数数组或给文件路径加引号,避免用户输入导致的命令注入。 - S3集成优化:若已将文件上传至S3,也可直接让FFmpeg读取S3签名URL(需确保权限),无需本地存储,但会增加网络开销。
- 性能与稳定性:大文件优先选择磁盘临时文件方案,避免内存占用过高;高并发场景建议引入任务队列(如BullMQ)异步处理音频切割。
内容的提问来源于stack exchange,提问作者penetrator.go
相关产品推荐
相关产品推荐

