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

Next.js浏览器端用FFmpeg合成音视频时遇WebAssembly链接错误求助

浏览器端Next.js项目中FFmpeg WebAssembly加载错误解决

问题场景

在Next.js开发的网页应用中,需在浏览器端将三张图片与一段音频合成为视频,要求每张图片展示时长均等且总时长与音频长度匹配。实现代码如下:

import {FFmpeg} from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';


export async function createVideo(ImageFiles, audioFile) {

  try {
    const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.4/dist/umd';
    const ffmpeg = new FFmpeg({ log: true});

    console.log('Loading ffmpeg core');
    await ffmpeg.load({
      corePath: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmPath: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    });
    await ffmpeg.load();
    console.log('Finished loading ffmpeg core');

    for (let i = 0; i < ImageFiles.length; i++) {
      ffmpeg.writeFile(
        `image${i+1}.jpg`,
        await fetchFile(ImageFiles[i].imageUrl)
      );
    }

    ffmpeg.FS('writeFile', 'audio.mp3', await fetchFile(audioFile));


    const durationPerImage = (await getAudioDuration(ffmpeg, 'audio.mp3')) / ImageFiles.length;
    let filterComplex = '';
    for (let i = 0; i < ImageFiles.length - 1; i++) {filterComplex += `[${i}:v]trim=duration=${durationPerImage},setpts=PTS-STARTPTS[v${i}]; `;
    }
    filterComplex += `${ImageFiles.slice(0, -1).map((_, i) => `[v${i}]`).join('')}concat=n=${ImageFiles.length - 1}:v=1:a=0,format=yuv420p[v];`;

    await ffmpeg.run(
      '-framerate', '1', '-loop', '1', '-t', durationPerImage, '-i', 'image%d.jpg', '-i', 'audio.mp3',
      '-filter_complex', filterComplex, '-map', '[v]', '-map', '1:a',
      '-c:v', 'libx264', '-tune', 'stillimage', '-c:a', 'aac', '-b:a', '192k', 'output.mp4'
    );

    const data = ffmpeg.FS('readFile', 'output.mp4');

    const videoURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
    return videoURL;
  } catch (error) {
    console.error('Error creating video:', error);
    throw new Error('Failed to create video');
  }
}

async function getAudioDuration(ffmpeg, audioFilename) {
  await ffmpeg.run('-i', audioFilename, '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', 'duration.txt');
  const data = ffmpeg.FS('readFile', 'duration.txt');
  const durationString = new TextDecoder().decode(data);
  const duration = Math.floor(parseFloat(durationString.trim())); 
  return duration;
}

运行时出现如下错误:

CreateVideo.js:65  Error creating video: RuntimeError: Aborted(LinkError: WebAssembly.instantiate(): Import #70 module="a" function="qa": function import requires a callable). Build with -sASSERTIONS for more info.

解决方法

1. 移除重复的FFmpeg加载调用

代码中连续执行了两次await ffmpeg.load(),第一次调用已传入核心文件路径完成初始化,第二次重复调用会导致WebAssembly实例冲突,引发导入错误。只需保留第一次加载调用即可。

2. 统一FFmpeg核心版本

确保@ffmpeg/ffmpeg与@ffmpeg/core版本完全匹配,版本不兼容会导致WebAssembly导入失败。建议升级到兼容的稳定版本(例如@ffmpeg/core@0.12.7,需对应@ffmpeg/ffmpeg@0.12.7)。

3. 优化音频时长获取逻辑

原代码通过生成临时文件读取时长的方式效率低且易引发文件冲突,改用ffmpeg.probe方法直接获取音频元数据,无需额外生成文件。

4. 修正滤镜链逻辑

原滤镜链仅处理前n-1张图片,导致最后一张图片未被加入视频,修正后需处理所有图片并拼接完整视频流。

5. 新增时长对齐参数

添加-shortest参数确保视频时长严格匹配音频时长,避免计算误差导致的时长不一致。

修正后的完整代码

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

export async function createVideo(ImageFiles, audioFile) {
  try {
    // 确保core版本与@ffmpeg/ffmpeg版本一致,此处以0.12.7为例
    const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.7/dist/umd';
    const ffmpeg = new FFmpeg({ log: true });

    console.log('Loading ffmpeg core');
    // 仅调用一次load完成初始化
    await ffmpeg.load({
      corePath: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
      wasmPath: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
    });
    console.log('Finished loading ffmpeg core');

    // 写入所有图片文件
    for (let i = 0; i < ImageFiles.length; i++) {
      await ffmpeg.writeFile(
        `image${i + 1}.jpg`,
        await fetchFile(ImageFiles[i].imageUrl)
      );
    }

    // 写入音频文件
    await ffmpeg.writeFile('audio.mp3', await fetchFile(audioFile));

    // 通过probe直接获取音频时长,无需临时文件
    const probeResult = await ffmpeg.probe('audio.mp3');
    const audioDuration = parseFloat(probeResult.format.duration);
    const durationPerImage = audioDuration / ImageFiles.length;

    // 构建包含所有图片的滤镜链
    let filterComplex = '';
    for (let i = 0; i < ImageFiles.length; i++) {
      filterComplex += `[${i}:v]trim=duration=${durationPerImage},setpts=PTS-STARTPTS[v${i}]; `;
    }
    // 拼接所有处理后的视频流
    const videoStreams = ImageFiles.map((_, i) => `[v${i}]`).join('');
    filterComplex += `${videoStreams}concat=n=${ImageFiles.length}:v=1:a=0,format=yuv420p[v];`;

    // 执行FFmpeg命令,添加-shortest确保时长对齐
    await ffmpeg.run(
      '-framerate', '1', '-loop', '1', '-t', durationPerImage.toString(), '-i', 'image%d.jpg',
      '-i', 'audio.mp3',
      '-filter_complex', filterComplex,
      '-map', '[v]', '-map', '1:a',
      '-c:v', 'libx264', '-tune', 'stillimage',
      '-c:a', 'aac', '-b:a', '192k',
      '-shortest',
      'output.mp4'
    );

    // 生成视频URL
    const data = ffmpeg.FS('readFile', 'output.mp4');
    const videoURL = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));

    // 清理临时文件,释放内存
    await ffmpeg.FS('unlink', 'audio.mp3');
    ImageFiles.forEach((_, i) => ffmpeg.FS('unlink', `image${i + 1}.jpg`));
    ffmpeg.FS('unlink', 'output.mp4');

    return videoURL;
  } catch (error) {
    console.error('Error creating video:', error);
    throw new Error('Failed to create video');
  }
}

内容的提问来源于stack exchange,提问作者Anil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 20:35:01