DEV Community

Qasim Ali
Qasim Ali

Posted on

mediaforge — A Modern TypeScript FFmpeg Wrapper (fluent-ffmpeg is Dead, Long Live mediaforge)

 If you've used Node.js for any kind of video or audio processing in the past few years, you've probably reached for fluent-ffmpeg. It was great. It abstracted ffmpeg's brutal CLI into something readable, and the community loved it.

But fluent-ffmpeg hasn't been meaningfully maintained since 2020. It has no TypeScript support. It uses callbacks. Its API is inconsistent. And it breaks in subtle ways on modern Node.js.

There's been a real gap. Until now.


Introducing mediaforge

mediaforge is a fully typed TypeScript wrapper for the system ffmpeg binary — built from scratch for Node.js 18+, with a fluent builder API, zero native bindings, and built-in helpers for everything you'd actually want to do.

npm install mediaforge
Enter fullscreen mode Exit fullscreen mode

It uses whatever ffmpeg is installed on the system — no bundled binaries, no native compilation. Works on Linux, macOS, and Windows.


What it looks like

The core builder API will feel familiar if you've used fluent-ffmpeg:

import { ffmpeg } from 'mediaforge';

await ffmpeg('input.mp4')
  .output('output.mp4')
  .videoCodec('libx264')
  .crf(22)
  .audioCodec('aac')
  .audioBitrate('128k')
  .run();
Enter fullscreen mode Exit fullscreen mode

But where it really shines is everything built on top of that.

Screenshots

import { screenshots, frameToBuffer } from 'mediaforge';

// 5 evenly-spaced screenshots
const { files } = await screenshots({
  input: 'video.mp4',
  folder: './thumbs',
  count: 5,
});

// Single frame as Buffer — no file written
const buf = await frameToBuffer({ input: 'video.mp4', timestamp: 30 });
Enter fullscreen mode Exit fullscreen mode

Two-pass encoding

await twoPassEncode({
  input: 'input.mp4',
  output: 'output.mp4',
  videoCodec: 'libx264',
  videoBitrate: '2M',
  audioCodec: 'aac',
  onPass1Complete: () => console.log('Pass 1 done'),
});
Enter fullscreen mode Exit fullscreen mode

Pipe & Stream I/O

// Stream ffmpeg output directly to HTTP response
streamOutput({
  input: 'movie.mp4',
  outputFormat: 'mp4',
  outputArgs: ['-c', 'copy', '-movflags', 'frag_keyframe+empty_moov'],
}).pipe(res);
Enter fullscreen mode Exit fullscreen mode

Audio normalization (EBU R128)

const result = await normalizeAudio({
  input: 'raw.mp4',
  output: 'normalized.mp4',
  targetI: -23,   // integrated loudness (LUFS)
  targetLra: 7,   // loudness range
  targetTp: -2,   // true peak
});
console.log(`Input was ${result.inputI} LUFS`);
Enter fullscreen mode Exit fullscreen mode

HLS & Adaptive Bitrate

await adaptiveHls({
  input: 'input.mp4',
  outputDir: './hls',
  variants: [
    { label: '1080p', resolution: '1920x1080', videoBitrate: '4M' },
    { label: '720p',  resolution: '1280x720',  videoBitrate: '2M' },
    { label: '360p',  resolution: '854x480',   videoBitrate: '800k' },
  ],
}).run();
Enter fullscreen mode Exit fullscreen mode

What makes it different from fluent-ffmpeg

Feature fluent-ffmpeg mediaforge
TypeScript ❌ None ✅ Full
Promise/async ⚠️ Callbacks only ✅ Native
ESM support ❌ Broken ✅ ESM + CJS
Screenshots ❌ Manual ✅ Built-in
HLS/DASH ❌ Manual ✅ Built-in
Audio normalize ❌ Manual ✅ EBU R128
Watermarks ❌ Manual ✅ Built-in
Stream I/O ⚠️ Limited ✅ Full pipe API
Codec detection ❌ None ✅ Runtime registry
Maintenance ❌ Abandoned ✅ Active

Runtime capability detection

One of my favourite features — you can ask the installed ffmpeg what it actually supports at runtime, not from a hardcoded list:

import { CapabilityRegistry } from 'mediaforge';

const reg = new CapabilityRegistry('ffmpeg');
console.log(reg.hasCodec('libx265'));     // true/false
console.log(reg.canEncode('libsvtav1')); // true/false
console.log(reg.hasFilter('scale'));     // true/false
console.log(reg.hasHwaccel('cuda'));     // true/false
Enter fullscreen mode Exit fullscreen mode

This powers the compatibility guards — you can auto-select the best available codec for each environment:

const codec = await builder.selectVideoCodec([
  { codec: 'h264_nvenc', featureKey: 'nvenc' },  // NVIDIA GPU
  { codec: 'h264_vaapi' },                        // Intel/AMD on Linux
  { codec: 'libx264' },                           // software fallback
]);
Enter fullscreen mode Exit fullscreen mode

Progress events that actually work

const proc = ffmpeg('input.mp4')
  .output('output.mp4')
  .videoCodec('libx264')
  .spawn({ parseProgress: true });

proc.emitter.on('progress', (info) => {
  console.log(`${info.percent?.toFixed(1)}% — ${info.fps} fps — ${info.speed}x speed`);
});
Enter fullscreen mode Exit fullscreen mode

54 built-in filters

scale, crop, pad, overlay, drawtext, fade, zoompan, colorkey, chromakey, yadif, thumbnail, loudnorm, equalizer, atempo, rubberband, silencedetect... and more. All typed, all discoverable via autocomplete.


Links

Would love feedback — especially from anyone who's been stuck with fluent-ffmpeg. What features do you wish it had?

Top comments (0)