如何修复Next.js中ffmpeg.load()的Failed to fetch TypeError?
问题分析与解决方案
错误根源
你遇到的TypeError: Failed to fetch发生在ffmpeg.load()时,核心问题有两个:
- 架构错误:将前端WebAssembly版的
@ffmpeg/ffmpeg放在了服务端API路由中。该库是为浏览器环境设计的,在服务端执行时无法正确加载WebAssembly文件(缺少浏览器的fetch环境、路径配置错误等)。 - 客户端直接调用服务端函数: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
相关产品推荐
相关产品推荐

