You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
  }
};

关键注意事项

  1. FFmpeg参数修正:原代码中-t参数误用为结束时间,FFmpeg的-t实际代表持续时间,需计算为section.to - section.from。
  2. 命令安全:使用spawn的参数数组或给文件路径加引号,避免用户输入导致的命令注入。
  3. S3集成优化:若已将文件上传至S3,也可直接让FFmpeg读取S3签名URL(需确保权限),无需本地存储,但会增加网络开销。
  4. 性能与稳定性:大文件优先选择磁盘临时文件方案,避免内存占用过高;高并发场景建议引入任务队列(如BullMQ)异步处理音频切割。

内容的提问来源于stack exchange,提问作者penetrator.go

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 09:26:15