javascriptnext.jsffmpeg

Next.js App Router API Route: @ffmpeg-installer/ffmpeg throws "Module not found" and "server relative imports not implemented" error


I'm trying to convert a WebM video file to MP4 using fluent-ffmpeg and @ffmpeg-installer/ffmpeg in a Next.js 14 API route (using the App Router). The same code worked perfectly in a standalone Node.js script, but when moved to Next.js, I get an error during runtime.

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

It worked when I was doing it in node js however when I tried to convert it to next I get the following error:

 ⨯ ./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.

Questions Why does @ffmpeg-installer/ffmpeg fail with this import resolution error in Next.js?

Any help or workaround would be greatly appreciated!

Node version: v22.21.0
"next": "15.2.4", 
"@ffmpeg-installer/ffmpeg": "^1.1.0", 
"fluent-ffmpeg": "^2.1.3", 

I am using turbopack and I am using npm version 10.9.4


Solution

  • After much struggle, I was able to get this to work. But I had to use ffmpeg static, and you have to use that as the path. There were some issues of its own with that. I hope this helps someone struggling with a similar issue.

    Next.js wasn’t bundling ffmpeg-static, and fluent-ffmpeg didn’t know where the binary was located.
    Two things fixed it:


    ✅ 1. Update your API Route to explicitly set the ffmpeg binary path

    You must tell fluent-ffmpeg where the ffmpeg-static binary is, and you must run the route in a Node.js runtime (NOT Edge).

    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 ffmpegStatic from "ffmpeg-static"; // ✅ ADD THIS
    import { writeFile, unlink, mkdir, readFile } from "fs/promises";
    import { existsSync } from "fs";
    import path from "path";
    import { tmpdir } from "os";
    
    export const runtime = "nodejs"; // ✅ Required for ffmpeg
    
    // 🔥 Tell fluent-ffmpeg which binary to use
    ffmpeg.setFfmpegPath(ffmpegStatic as string);
    
    const s3Client = new S3Client({
      region: process.env.AWS_REGION!,
      credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
      }
    });
    
    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`);
    
        const buffer = Buffer.from(await file.arrayBuffer());
        await writeFile(inputPath, buffer);
    
        // Convert using ffmpeg
        await new Promise<void>((resolve, reject) => {
          ffmpeg(inputPath)
            .setFfmpegPath(ffmpegStatic as string)
            .outputOptions(["-c:v libx264", "-preset fast", "-crf 23", "-r 30"])
            .toFormat("mp4")
            .on("end", resolve)
            .on("error", reject)
            .save(outputPath);
        });
    
        const key = `${user.id}/${Date.now()}.mp4`;
        const convertedBuffer = await readFile(outputPath);
    
        await s3Client.send(
          new PutObjectCommand({
            Bucket: process.env.S3_BUCKET_NAME!,
            Key: key,
            Body: convertedBuffer,
            ContentType: "video/mp4"
          })
        );
    
        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 }
        );
      }
    }
    

    ✅ 2. Add ffmpeg-static + fluent-ffmpeg to external server packages

    You need to update your nextjs config file. Next.js must not bundle these — it must load them from node_modules.

    /** @type {import('next').NextConfig} */
    const nextConfig = {
      experimental: {
        serverComponentsExternalPackages: ["ffmpeg-static", "fluent-ffmpeg"]
      }
    };
    
    export default nextConfig;
    

    🎉 After this, ffmpeg finally ran correctly inside the Next.js route.

    Hopefully this saves someone hours of debugging!