Исправление ошибки FFmpeg в Next.js App Router
Решаем ошибки «Module not found» и «server relative imports not implemented» @ffmpeg-installer/ffmpeg в Next.js App Router. Инструкция конвертации видео.
Next.js App Router API Route: @ffmpeg-installer/ffmpeg выдаёт ошибку «Module not found» и «server relative imports not implemented»
Я пытаюсь преобразовать видеофайл WebM в MP4 с помощью fluent‑ffmpeg и @ffmpeg-installer/ffmpeg в API‑маршруте Next.js 14 (используя App Router). Такой же код работал без проблем в автономном Node.js‑скрипте, но при переносе в Next.js возникает ошибка во время выполнения.
Сообщение об ошибке
⨯ ./node_modules/@ffmpeg-installer/ffmpeg
Module not found: Can't resolve './ROOT/node_modules/@ffmpeg-installer/ffmpeg/node_modules/@ffmpeg-installer/win32-x64/package.json'
server relative imports are not implemented yet. Please try an import relative to the file you are importing from.
Реализация кода
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/session";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import ffmpeg from "fluent-ffmpeg";
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
import { writeFile, unlink, mkdir, readFile } from "fs/promises";
import { existsSync } from "fs";
import path from "path";
import { tmpdir } from "os";
const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
}
});
ffmpeg.setFfmpegPath(ffmpegInstaller.path);
export async function POST(request: NextRequest) {
try {
const user = await getSession();
if (!user) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 }
);
}
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ success: false, message: "No file found" },
{ status: 400 }
);
}
const tempDir = path.join(tmpdir(), "video-conversion");
if (!existsSync(tempDir)) {
await mkdir(tempDir, { recursive: true });
}
const inputPath = path.join(tempDir, `${Date.now()}-input.webm`);
const outputPath = path.join(tempDir, `${Date.now()}-output.mp4`);
// Write uploaded file to disk
const buffer = Buffer.from(await file.arrayBuffer());
await writeFile(inputPath, buffer);
// Convert video using ffmpeg
await new Promise<void>((resolve, reject) => {
ffmpeg(inputPath)
.outputOptions(["-c:v libx264", "-preset fast", "-crf 23", "-r 30"])
.toFormat("mp4")
.on("end", () => resolve())
.on("error", err => reject(err))
.save(outputPath);
});
// Upload to S3
const key = `${user.id}/${Date.now()}.mp4`;
const convertedBuffer = await readFile(outputPath);
await s3Client.send(
new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET!,
Key: key,
Body: convertedBuffer,
ContentType: "video/mp4"
})
);
// Cleanup temp files
await Promise.all([
unlink(inputPath).catch(() => {}),
unlink(outputPath).catch(() => {})
]);
return new NextResponse(convertedBuffer, {
headers: {
"Content-Type": "video/mp4",
"Content-Length": convertedBuffer.length.toString()
}
});
} catch (error) {
console.error("Video conversion failed:", error);
return NextResponse.json(
{ success: false, message: "Video conversion failed" },
{ status: 500 }
);
}
}
Детали окружения
- Node версия: v22.21.0
- Next.js версия: 15.2.4
- @ffmpeg-installer/ffmpeg версия: ^1.1.0
- fluent‑ffmpeg версия: ^2.1.3
- npm версия: 10.9.4
- Используется Turbopack
Вопрос
Почему @ffmpeg-installer/ffmpeg выдаёт ошибку разрешения импорта в Next.js App Router, и какие возможные обходные пути можно применить для реализации функционала конвертации видео в API‑маршрутах Next.js?
Ошибка, с которой вы столкнулись, возникает из‑за того, что Next.js App Router применяет более строгие правила разрешения модулей по сравнению с Pages Router и обычными скриптами Node.js. Пакет @ffmpeg-installer/ffmpeg содержит платформенно‑специфические бинарники, которые используют сложные пути импорта, которые Next.js‑билдер не может корректно разрешить.
Содержание
- Понимание причины
- Немедленные обходные пути
- Альтернативные решения
- Лучшие практики работы с видео в Next.js
- Примеры реализации
- Вывод
Понимание причины
Ошибка возникает потому, что Next.js App Router использует другую стратегию разрешения модулей, чем традиционные приложения Node.js. Согласно документации Next.js, проблема связана с тем, как сборщик обрабатывает нативные модули с сложными путями импорта.
Пакет @ffmpeg-installer/ffmpeg содержит платформенно‑специфические исполняемые файлы, которые ссылаются на внутренние пути модулей, например ./ROOT/node_modules/@ffmpeg-installer/ffmpeg/node_modules/@ffmpeg-installer/win32-x64/package.json. Билдер Next.js не понимает эти относительные пути, что приводит к ошибке «server relative imports are not implemented yet».
Немедленные обходные пути
1. Использовать алиасы конфигурации Next.js
Вы можете настроить Next.js так, чтобы обрабатывать проблемные импорты, добавив алиасы в next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
if (isServer) {
// Исправляем путь ffmpeg-installer
config.resolve.alias['@ffmpeg-installer/ffmpeg'] = require('@ffmpeg-installer/ffmpeg').path;
config.resolve.alias['ffmpeg'] = require('@ffmpeg-installer/ffmpeg').path;
}
return config;
},
};
module.exports = nextConfig;
2. Динамический импорт с загрузкой только на сервере
Поскольку проблема возникает именно в серверном коде API‑маршрутов, можно использовать динамический импорт с условной загрузкой:
export async function POST(request: NextRequest) {
// Загружаем модули ffmpeg только на сервере
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'development') {
const { exec } = await import('child_process');
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
// Используем exec вместо fluent‑ffmpeg, чтобы избежать проблем с импортом
// Реализация будет иной, но результат тот же
}
}
3. Перенести обработку видео на выделенный сервер
Как отмечено в обсуждениях на Reddit, самый надёжный вариант – перенести обработку видео на отдельный сервер:
// В вашем API‑маршруте Next.js
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
// Проксируем запрос на ваш выделенный сервер обработки видео
const response = await fetch('https://your-video-processing-server.com/convert', {
method: 'POST',
body: formData,
});
return NextResponse.json(await response.json());
}
Альтернативные решения
1. Использовать ffmpeg.wasm для клиентской обработки
Для обработки видео в браузере можно использовать ffmpeg.wasm, который полностью работает в клиенте. Согласно документации ffmpeg.wasm, это избавляет от необходимости устанавливать FFmpeg на сервере.
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({ log: true });
export async function POST(request: NextRequest) {
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
const formData = await request.formData();
const file = formData.get("file") as File;
ffmpeg.FS('writeFile', 'input.webm', await fetchFile(file));
await ffmpeg.run('-i', 'input.webm', '-c:v', 'libx264', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
return new NextResponse(data.buffer, {
headers: {
'Content-Type': 'video/mp4',
}
});
}
2. Использовать сервис обработки видео
Можно воспользоваться специализированными сервисами, например streampot.io, которые предоставляют FFmpeg‑as‑a‑Service:
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
// Отправляем на streampot.io или аналогичный сервис
const response = await fetch('https://api.streampot.io/convert', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STREAMPOT_API_KEY}`,
},
body: formData,
});
const convertedVideo = await response.arrayBuffer();
return new NextResponse(convertedVideo, {
headers: {
'Content-Type': 'video/mp4',
}
});
}
3. Обработка видео в Docker‑контейнере
Разверните обработку видео в Docker‑контейнере и общайтесь с ним по HTTP:
// API‑маршрут Next.js, который ставит задачу в очередь
export async function POST(request: NextRequest) {
const formData = await request.formData();
// Сохраняем файл временно
const tempId = Date.now().toString();
await writeFile(`/tmp/${tempId}`, Buffer.from(await formData.get("file").arrayBuffer()));
// Отправляем запрос на контейнер
const processingResponse = await fetch('http://video-processor:3000/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputPath: `/tmp/${tempId}`,
outputPath: `/tmp/${tempId}.mp4`,
}),
});
// Возвращаем обработанное видео
const processedVideo = await readFile(`/tmp/${tempId}.mp4`);
return new NextResponse(processedVideo, {
headers: {
'Content-Type': 'video/mp4',
}
});
}
Лучшие практики работы с видео в Next.js
1. Конфигурация, зависящая от окружения
// config/ffmpeg.js
let ffmpegPath;
let ffmpeg;
if (typeof window === 'undefined') {
// Серверная часть
ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
ffmpeg = require('fluent-ffmpeg');
ffmpeg.setFfmpegPath(ffmpegPath);
} else {
// Клиентская часть – используем ffmpeg.wasm
ffmpeg = null;
}
module.exports = { ffmpeg, ffmpegPath };
2. Правильная обработка ошибок и очистка
export async function POST(request: NextRequest) {
const tempFiles = [];
try {
// Ваш код обработки видео
tempFiles.push(inputPath, outputPath);
} catch (error) {
console.error('Video processing failed:', error);
throw error;
} finally {
// Убедитесь, что происходит очистка
for (const file of tempFiles) {
try {
await unlink(file);
} catch (cleanupError) {
console.error('Cleanup failed:', cleanupError);
}
}
}
}
3. Оптимизация производительности
Для больших видеофайлов реализуйте потоковую обработку и обработку чанками:
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
// Обрабатываем чанками для экономии памяти
const chunkSize = 1024 * 1024; // 1 МБ
let offset = 0;
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
await processChunk(chunk);
offset += chunkSize;
}
}
Примеры реализации
Полностью работающий пример с выделенным сервисом
// app/api/convert/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/session";
export async function POST(request: NextRequest) {
try {
const user = await getSession();
if (!user) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 }
);
}
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ success: false, message: "No file found" },
{ status: 400 }
);
}
// Отправляем запрос на сервис обработки видео
const convertResponse = await fetch('https://api.streampot.io/convert', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STREAMPOT_API_KEY}`,
'Content-Type': 'multipart/form-data',
},
body: formData,
});
if (!convertResponse.ok) {
throw new Error('Video conversion failed');
}
const convertedVideo = await convertResponse.arrayBuffer();
// Возвращаем видео напрямую
return new NextResponse(convertedVideo, {
headers: {
'Content-Type': 'video/mp4',
'Content-Length': convertedVideo.byteLength.toString(),
}
});
} catch (error) {
console.error("Video conversion failed:", error);
return NextResponse.json(
{ success: false, message: "Video conversion failed" },
{ status: 500 }
);
}
}
Реализация с ffmpeg.wasm
// app/api/convert-wasm/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({ log: true });
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json(
{ success: false, message: "No file found" },
{ status: 400 }
);
}
// Загружаем ffmpeg, если ещё не загружен
if (!ffmpeg.isLoaded()) {
await ffmpeg.load();
}
// Записываем входной файл во виртуальную файловую систему
ffmpeg.FS('writeFile', 'input.webm', await fetchFile(file));
// Конвертируем видео
await ffmpeg.run(
'-i', 'input.webm',
'-c:v', 'libx264',
'-preset', 'fast',
'-crf', '23',
'-r', '30',
'output.mp4'
);
// Читаем выходной файл
const outputData = ffmpeg.FS('readFile', 'output.mp4');
// Возвращаем конвертированное видео
return new NextResponse(outputData.buffer, {
headers: {
'Content-Type': 'video/mp4',
'Content-Length': outputData.byteLength.toString(),
}
});
} catch (error) {
console.error("Video conversion failed:", error);
return NextResponse.json(
{ success: false, message: "Video conversion failed" },
{ status: 500 }
);
}
}
Вывод
Ошибка @ffmpeg-installer/ffmpeg в Next.js App Router возникает из‑за несовместимости правил разрешения модулей между сборщиком и нативными модулями. Ключевые выводы:
- Причина: Next.js App Router не может разрешить сложные относительные пути, используемые
@ffmpeg-installer/ffmpeg. - Лучшие решения:
- Перенести обработку видео на отдельный сервер/сервис.
- Использовать
ffmpeg.wasmдля клиентской обработки. - Настроить алиасы webpack как временное решение.
- Рекомендованный подход: Для продакшн‑приложений выделить отдельный сервис для обработки видео на Node.js с
fluent‑ffmpegи проксировать запросы из Next.js к этому сервису. - Альтернатива: Для простых случаев
ffmpeg.wasmобеспечивает удобное решение, работающее в браузере.
Выбор зависит от конкретных требований, но обычно перенос обработки видео с сервера Next.js на отдельный сервис обеспечивает наибольшую надёжность и масштабируемость.