Другое

Исправление ошибки 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.

Реализация кода

javascript
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 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:

javascript
/** @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‑маршрутов, можно использовать динамический импорт с условной загрузкой:

javascript
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, самый надёжный вариант – перенести обработку видео на отдельный сервер:

javascript
// В вашем 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 на сервере.

javascript
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:

javascript
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:

javascript
// 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. Конфигурация, зависящая от окружения

javascript
// 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. Правильная обработка ошибок и очистка

javascript
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. Оптимизация производительности

Для больших видеофайлов реализуйте потоковую обработку и обработку чанками:

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

Примеры реализации

Полностью работающий пример с выделенным сервисом

javascript
// 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

javascript
// 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 возникает из‑за несовместимости правил разрешения модулей между сборщиком и нативными модулями. Ключевые выводы:

  1. Причина: Next.js App Router не может разрешить сложные относительные пути, используемые @ffmpeg-installer/ffmpeg.
  2. Лучшие решения:
    • Перенести обработку видео на отдельный сервер/сервис.
    • Использовать ffmpeg.wasm для клиентской обработки.
    • Настроить алиасы webpack как временное решение.
  3. Рекомендованный подход: Для продакшн‑приложений выделить отдельный сервис для обработки видео на Node.js с fluent‑ffmpeg и проксировать запросы из Next.js к этому сервису.
  4. Альтернатива: Для простых случаев ffmpeg.wasm обеспечивает удобное решение, работающее в браузере.

Выбор зависит от конкретных требований, но обычно перенос обработки видео с сервера Next.js на отдельный сервис обеспечивает наибольшую надёжность и масштабируемость.

Источники

  1. Next.js Module Not Found Documentation
  2. Reddit Discussion on FFmpeg in Next.js
  3. ffmpeg.wasm GitHub Issues
  4. streampot.io Service Announcement
  5. Next.js Webpack Configuration Examples
Авторы
Проверено модерацией
Модерация