DEV Community

How to Use FFmpeg with Bun

Using Bun.spawn with a Local FFmpeg Binary

If FFmpeg is installed on the machine, you can call it directly using Bun.spawn. This is Bun's native subprocess API. It's similar in spirit to Node's child_process.spawn, but the interface is cleaner and returns a Subprocess object you can await directly.

const proc = Bun.spawn(
  [
    "ffmpeg",
    "-i", "input.mp4",
    "-c:v", "libx264",
    "-crf", "23",
    "-preset", "medium",
    "-c:a", "aac",
    "-b:a", "192k",
    "output.mp4",
  ],
  { stderr: "pipe" },
);

const exitCode = await proc.exited;
if (exitCode !== 0) {
  const stderr = await new Response(proc.stderr).text();
  console.error("FFmpeg failed:", stderr);
} else {
  console.log("Transcoding complete");
}

No permission flags needed. Unlike Deno, Bun doesn't sandbox subprocess execution. You call Bun.spawn and it runs. That makes local development faster but also means your code has full system access by default.

This approach works well for local scripts, CLI tools, and servers where you control the environment. But it breaks down in production. You need FFmpeg installed on every machine. Most serverless and edge runtimes don't support spawning native binaries. And you're responsible for managing FFmpeg versions, codec libraries, and OS-level dependencies yourself.

Using FFmpeg WASM in Bun

ffmpeg.wasm compiles FFmpeg to WebAssembly, so it runs without a native binary. Bun's Node.js compatibility means it just works:

import { FFmpeg } from "@ffmpeg/ffmpeg";
import { fetchFile } from "@ffmpeg/util";

const ffmpeg = new FFmpeg();
await ffmpeg.load();
await ffmpeg.writeFile("input.mp4", await fetchFile("./input.mp4"));
await ffmpeg.exec(["-i", "input.mp4", "-c:v", "libx264", "output.mp4"]);
const data = await ffmpeg.readFile("output.mp4");
await Bun.write("output.mp4", data);

No FFmpeg binary needed. No system dependencies. But the tradeoffs are significant. Processing is 10-20x slower than native FFmpeg because WebAssembly can't match native CPU performance for heavy compute work. Memory is constrained. Not all codecs are available in the WASM build. And large files (anything over a few hundred MB) will likely crash.

ffmpeg.wasm is fine for lightweight operations like extracting a single frame or trimming a short clip. Don't try to transcode a full-length video with it.

Using the FFmpeg Micro Cloud API

If you don't want to manage FFmpeg binaries or accept WASM performance penalties, a cloud API is the cleanest path. FFmpeg Micro processes video through standard HTTP requests, which Bun handles natively with fetch(). No npm packages. No binary dependencies. Just HTTP.

const response = await fetch("https://api.ffmpeg-micro.com/v1/transcodes", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${Bun.env.FFMPEG_MICRO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    inputs: [{ url: "https://example.com/video.mp4" }],
    outputFormat: "mp4",
    preset: { quality: "high", resolution: "1080p" },
  }),
});

const job = await response.json();
console.log(`Job ${job.id} status: ${job.status}`);

One fetch call. FFmpeg Micro handles the transcoding infrastructure, codec management, and scaling. Your Bun app just sends an HTTP request.

For advanced use cases where you need specific FFmpeg flags, use the options array instead of presets:

const response = await fetch("https://api.ffmpeg-micro.com/v1/transcodes", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${Bun.env.FFMPEG_MICRO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    inputs: [{ url: "https://example.com/video.mp4" }],
    outputFormat: "webm",
    options: [
      { option: "-c:v", argument: "libvpx-vp9" },
      { option: "-crf", argument: "30" },
      { option: "-b:v", argument: "0" },
      { option: "-c:a", argument: "libopus" },
    ],
  }),
});

Polling for Completion

Transcodes are async. Poll the status endpoint until the job finishes:

async function waitForTranscode(jobId: string): Promise<Record<string, unknown>> {
  const apiKey = Bun.env.FFMPEG_MICRO_API_KEY;
  while (true) {
    const res = await fetch(`https://api.ffmpeg-micro.com/v1/transcodes/${jobId}`, {
      headers: { "Authorization": `Bearer ${apiKey}` },
    });
    const job = await res.json();
    if (job.status === "completed" || job.status === "failed") {
      return job;
    }
    await Bun.sleep(2000);
  }
}

Once completed, grab the download URL:

const downloadRes = await fetch(
  `https://api.ffmpeg-micro.com/v1/transcodes/${job.id}/download`,
  { headers: { "Authorization": `Bearer ${apiKey}` } },
);
const { url } = await downloadRes.json();
console.log("Download:", url);

This works identically on your local machine, in a Docker container, or on any hosting platform that runs Bun. No environment-specific setup.

Which Approach Should You Use

  • Bun.spawn if you're writing a local script or CLI tool where you control the machine and FFmpeg is already installed. Bun's fast startup makes it great for quick scripts that shell out to FFmpeg.
  • ffmpeg.wasm if you need in-process video handling for small files and can tolerate much slower performance.
  • A cloud API like FFmpeg Micro if you're building a production app, deploying to serverless infrastructure, or want to avoid managing FFmpeg binaries entirely. The API approach is the only option on platforms that don't support native binaries, and it keeps your Bun app lightweight.

Common Pitfalls

  • Using child_process instead of Bun.spawn. Bun supports Node's child_process for compatibility, but Bun.spawn is faster and returns a cleaner API. The proc.exited promise is simpler than wiring up event listeners on a ChildProcess object.
  • Forgetting to handle stderr. FFmpeg writes all its logging (including progress) to stderr, not stdout. If you don't set stderr: "pipe" in Bun.spawn, you won't see error messages when a transcode fails. Bun defaults to inheriting the parent's stderr, which works for debugging but swallows output if you need to capture it programmatically.
  • Using Bun.file for reading FFmpeg output before it's flushed. Bun.file is fast, but if you read an output file before FFmpeg has fully flushed and closed it, you'll get a partial file. Always await proc.exited before reading the output.
  • Assuming Bun.spawn works everywhere. Bun's subprocess API needs a real OS process. On platforms like Cloudflare Workers or other V8 isolate runtimes, subprocess spawning isn't available. If your deployment target restricts system calls, use a cloud API instead.

FAQ

Can I use fluent-ffmpeg with Bun?

Yes. Bun's Node.js compatibility layer supports fluent-ffmpeg out of the box. Install it with bun add fluent-ffmpeg and it works the same as in Node. But you still need a local FFmpeg binary installed. If you want Bun-native code without the Node compatibility layer, use Bun.spawn directly or the FFmpeg Micro API for cloud processing.

How does Bun compare to Node.js for FFmpeg video processing?

The FFmpeg CLI itself is identical. The difference is in how you call it. Node.js uses child_process.spawn, Bun uses Bun.spawn. Both need the binary installed. Bun's faster startup makes short-lived scripts noticeably quicker, and native TypeScript means no build step. The cloud API approach is the same across both runtimes since it's just HTTP fetch. For a Node.js comparison, check the Node.js FFmpeg guide.

Does Bun.spawn work on serverless platforms?

It depends on the platform. Bun can run on traditional servers and in Docker, where Bun.spawn works fine. But serverless environments like Cloudflare Workers or edge functions typically run in V8 isolates that don't support subprocess execution. If you're deploying to those platforms, the cloud API approach is your best option.

What video formats does the FFmpeg Micro API support?

FFmpeg Micro accepts any format FFmpeg can read as input. For output, you can specify mp4, webm, or mov in the outputFormat field. The preset mode supports quality levels (high, medium, low) and resolution options (480p, 720p, 1080p, 4k). Check the full API docs for the complete list.

Do I need to pay to use FFmpeg Micro with Bun?

FFmpeg Micro has a free tier with enough processing time to build and test your integration. You can sign up for a free API key and start processing video in minutes. For a hands-on walkthrough of FFmpeg concepts, the Learn FFmpeg training covers everything from basics to automation.

Comments

No comments yet. Start the discussion.