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

如何修复Next.js中ffmpeg.load()的Failed to fetch TypeError?

问题分析与解决方案

错误根源

你遇到的TypeError: Failed to fetch发生在ffmpeg.load()时,核心问题有两个:

  1. 架构错误:将前端WebAssembly版的@ffmpeg/ffmpeg放在了服务端API路由中。该库是为浏览器环境设计的,在服务端执行时无法正确加载WebAssembly文件(缺少浏览器的fetch环境、路径配置错误等)。
  2. 客户端直接调用服务端函数:Next.js中客户端组件不能直接导入服务端组件的函数,违反了前后端分离的架构规则。

另外还有两个潜在问题:

  • 浏览器中没有系统字体文件,drawtext滤镜指定的Arial.ttf会找不到。
  • 未配置Next.js以支持WebAssembly文件加载。

解决方案一:前端浏览器处理(推荐小视频场景)

将FFmpeg逻辑完全移到客户端组件中,适配浏览器环境:

1. 配置Next.js支持WebAssembly

修改next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    config.experiments = {
      asyncWebAssembly: true,
      layers: true,
    };
    return config;
  },
};

module.exports = nextConfig;

2. 准备字体文件

将Arial.ttf放入项目public/font/目录下(没有目录就新建),确保FFmpeg能加载到字体。

3. 修改客户端组件

'use client';
import { useState } from 'react';
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';

const ffmpeg = createFFmpeg({ log: true });

export default function VideoWatermark() {
  const [inputFile, setInputFile] = useState<File>();
  const [outputFile, setOutputFile] = useState<string>();
  const [text, setText] = useState<string>('');
  const [isLoading, setIsLoading] = useState(false);

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files?.length) {
      setInputFile(event.target.files[0]);
      setOutputFile(undefined);
    }
  };

  const handleWatermark = async () => {
    if (!inputFile || !text) return;
    setIsLoading(true);

    try {
      // 加载FFmpeg核心
      if (!ffmpeg.isLoaded()) await ffmpeg.load();

      // 加载字体文件到FFmpeg文件系统
      const fontRes = await fetch('/font/Arial.ttf');
      const fontBlob = await fontRes.blob();
      ffmpeg.FS('writeFile', 'Arial.ttf', await fetchFile(fontBlob));

      // 写入用户上传的视频
      ffmpeg.FS('writeFile', inputFile.name, await fetchFile(inputFile));

      // 执行水印命令(转义文字中的单引号避免语法错误)
      await ffmpeg.run(
        '-i', inputFile.name,
        '-filter_complex', 
        `drawtext=text='${text.replace(/'/g, "\\'")}':fontfile=Arial.ttf:fontcolor=white@0.8:fontsize=40:x=(w-text_w)/2:y=(h-text_h)/2`,
        '-c:v', 'libx264', '-c:a', 'aac', // 指定兼容浏览器的编解码器
        'output.mp4'
      );

      // 生成预览URL
      const outputData = ffmpeg.FS('readFile', 'output.mp4');
      const videoUrl = URL.createObjectURL(
        new Blob([outputData.buffer], { type: 'video/mp4' })
      );
      setOutputFile(videoUrl);
    } catch (err) {
      console.error('水印添加失败:', err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <input type='file' accept='video/*' onChange={handleFileChange} />
      <br />
      <input
        value={text}
        type='text'
        placeholder='输入水印文字'
        onChange={(e) => setText(e.target.value)}
      />
      <br />
      <button onClick={handleWatermark} disabled={isLoading}>
        {isLoading ? '处理中...' : '添加水印'}
      </button>
      {outputFile && (
        <div style={{ marginTop: '20px' }}>
          <video controls width='600'>
            <source src={outputFile} type='video/mp4' />
          </video>
        </div>
      )}
    </div>
  );
}

解决方案二:服务端处理(推荐大视频场景)

如果视频文件较大,建议用服务端FFmpeg处理(需服务器安装FFmpeg环境):

1. 安装服务端FFmpeg依赖

npm install fluent-ffmpeg

2. 创建服务端API路由(app/api/addwatermark/route.ts)

import { NextResponse } from 'next/server';
import ffmpeg from 'fluent-ffmpeg';
import { writeFile, readFile, unlink } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';

export async function POST(request: Request) {
  const formData = await request.formData();
  const videoFile = formData.get('video') as File;
  const text = formData.get('text') as string;

  if (!videoFile || !text) {
    return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
  }

  // 创建临时文件路径
  const inputPath = join(tmpdir(), `input-${Date.now()}.mp4`);
  const outputPath = join(tmpdir(), `output-${Date.now()}.mp4`);

  try {
    // 写入上传的视频到临时文件
    const buffer = Buffer.from(await videoFile.arrayBuffer());
    await writeFile(inputPath, buffer);

    // 执行FFmpeg水印命令(注意服务器字体路径,这里用系统默认字体示例)
    await new Promise<void>((resolve, reject) => {
      ffmpeg(inputPath)
        .outputOptions([
          `-vf drawtext=text='${text.replace(/'/g, "\\'")}':fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf:fontcolor=white@0.8:fontsize=40:x=(w-text_w)/2:y=(h-text_h)/2`,
          '-c:v libx264', '-c:a aac'
        ])
        .save(outputPath)
        .on('end', resolve)
        .on('error', reject);
    });

    // 返回处理后的视频
    const outputBuffer = await readFile(outputPath);
    return new NextResponse(outputBuffer, {
      headers: {
        'Content-Type': 'video/mp4',
        'Content-Disposition': `inline; filename=watermarked-${videoFile.name}`
      }
    });
  } catch (err) {
    console.error('服务端处理失败:', err);
    return NextResponse.json({ error: '处理失败' }, { status: 500 });
  } finally {
    // 清理临时文件
    try {
      await unlink(inputPath);
      await unlink(outputPath);
    } catch (cleanupErr) {
      console.error('临时文件清理失败:', cleanupErr);
    }
  }
}

3. 修改客户端组件调用API

'use client';
import { useState } from 'react';

export default function VideoWatermark() {
  const [inputFile, setInputFile] = useState<File>();
  const [outputFile, setOutputFile] = useState<string>();
  const [text, setText] = useState<string>('');
  const [isLoading, setIsLoading] = useState(false);

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files?.length) {
      setInputFile(event.target.files[0]);
      setOutputFile(undefined);
    }
  };

  const handleWatermark = async () => {
    if (!inputFile || !text) return;
    setIsLoading(true);

    try {
      const formData = new FormData();
      formData.append('video', inputFile);
      formData.append('text', text);

      const res = await fetch('/api/addwatermark', {
        method: 'POST',
        body: formData
      });

      if (!res.ok) throw new Error('请求失败');

      const blob = await res.blob();
      const videoUrl = URL.createObjectURL(blob);
      setOutputFile(videoUrl);
    } catch (err) {
      console.error('水印添加失败:', err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <input type='file' accept='video/*' onChange={handleFileChange} />
      <br />
      <input
        value={text}
        type='text'
        placeholder='输入水印文字'
        onChange={(e) => setText(e.target.value)}
      />
      <br />
      <button onClick={handleWatermark} disabled={isLoading}>
        {isLoading ? '处理中...' : '添加水印'}
      </button>
      {outputFile && (
        <div style={{ marginTop: '20px' }}>
          <video controls width='600'>
            <source src={outputFile} type='video/mp4' />
          </video>
        </div>
      )}
    </div>
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 02:17:55