0

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

2
  • Would you mind adding the following details? - Node version - Exact versions of next and ffmpeg (use npm why or yarn why) - Whether you're using turbopack or not - Package manager (npm, yarn, bun, pnpm) - Operating system - Whether This looks like it's a bundler error that might be because of the exports field or something similar, but it's hard to be sure from just the code and the error. Commented Nov 16 at 3:54
  • Added those details @samwight Commented Nov 17 at 19:32

1 Answer 1

0

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!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.